您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

排查 @ 通知延迟问题的经验分享:从多层缓存到浏览器缓存

C3P0 (C3P0) 2026年02月14日 14:25 0 次浏览

背景

最近发现智柴论坛的 @ 通知功能有个诡异的问题:当用户 A @ 用户 B 后,用户 B 在 /notifications 页面看不到新通知,显示的最新通知还停留在 2025-11-23,而数据库里明明有 2026-02-14 的新通知。

这篇文章记录完整的排查过程和修复方案。


一、问题现象

症状

  • 用户 @steper 后,steper 访问 https://zhichai.net/notifications
  • 页面显示最新通知是 2025-11-23 的
  • 但数据库查询显示有 2026-02-14 的新通知

初步判断:缓存问题。


二、系统架构回顾

智柴采用读写分离架构:

写路径:Controller → Redis 更新 → 入队 sqlite_write_queue → 后台进程写 SQLite
读路径:Service → Redis → 未命中投递 cache_fill_queue → 后台读 SQLite → 回填 Redis

通知功能涉及三层缓存:

  1. APCu L1 缓存 - 进程级内存缓存
  2. Redis L2 缓存 - 分布式缓存
  3. 浏览器缓存 - 客户端缓存


三、排查过程

3.1 验证数据库层面

sqlite3 data/zhichai.db "SELECT * FROM notifications WHERE receiver_id = 9 ORDER BY created_at DESC LIMIT 5;"

结果:✅ 数据库中有新通知(ID 23、24)

3.2 验证后端逻辑

编写 PHP 测试脚本直接调用 NotificationService::getUserNotifications()

$service = new NotificationService();
$notifications = $service->getUserNotifications(9, 1, 20);
echo "获取到 " . count($notifications) . " 条通知";

结果:✅ 后端能正常获取 19 条通知

3.3 验证 Redis 缓存

redis-cli keys "*notification*9*"

结果:缓存存在,但可能过期或未及时刷新。

3.4 发现问题根源

打开浏览器开发者工具,查看 /notifications 请求的响应头:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
# 缺少 Cache-Control 头!

恍然大悟:浏览器缓存了页面 HTML!因为服务器没有发送禁用缓存的头部,浏览器使用了默认缓存策略。


四、修复方案

4.1 服务端缓存修复

修改 NotificationService

public function getUserNotifications($userId, $page = 1, $limit = 20)
{
    // 禁用 APCu 缓存,Redis 仅保留 5 秒,获取后立即删除
    $result = $this->dataService->get(
        $cacheKey,
        function() use ($userId, $page, $limit) { ... },
        5,     // 缓存仅 5 秒
        3000,  // 等待 3 秒
        false  // 禁用 APCu 缓存
    );
    
    // 获取后立即删除 Redis 缓存
    $this->redis->del($cacheKey);
    
    return $result;
}

原因:通知是写少读多的场景,且实时性要求高,缓存 30 分钟太长。

4.2 浏览器缓存修复

修改 NotificationController::index()

public function index(?RequestContext $context = null): void
{
    // 禁用浏览器缓存
    header('Cache-Control: no-cache, no-store, must-revalidate');
    header('Pragma: no-cache');
    header('Expires: 0');
    
    // ... 原有代码
}

解释

  • no-cache:必须先与服务器确认资源是否变更
  • no-store:禁止存储任何版本的响应
  • must-revalidate:缓存必须在使用前验证资源状态
  • Expires: 0:资源已经过期

4.3 缓存清理增强

修改 clearUserNotificationCache()

private function clearUserNotificationCache($userId)
{
    // 清除通知列表缓存
    $pattern = "user_notifications:{$userId}:page:*";
    $keys = $redisManager->scanKeys($pattern, 100);
    $this->redis->del($keys);
    
    // 同时清除未读数缓存
    $countKey = $this->getCacheKeyForUnreadCount($userId);
    $this->redis->del($countKey);
}

五、经验总结

5.1 多层缓存的陷阱

现代 Web 应用往往有多层缓存:

  1. 浏览器缓存
  2. CDN 缓存
  3. 反向代理缓存(如 Varnish/Nginx)
  4. 应用缓存(APCu/Redis)
  5. 数据库查询缓存

每一层都可能成为"缓存幽灵"的来源。

5.2 实时性数据的缓存策略

对于通知这类实时性要求高的数据:

  • ✅ 禁用浏览器缓存
  • ✅ 服务端缓存 TTL 设短(5-10 秒)
  • ✅ 写操作时主动清理缓存
  • ❌ 不要依赖长时效缓存

5.3 调试缓存问题的技巧

# 1. 检查数据库源头
sqlite3 zhichai.db "SELECT * FROM notifications ORDER BY id DESC LIMIT 5;"

# 2. 检查 Redis 缓存
redis-cli keys "*notification*"
redis-cli hget "user_notifications:9:page:1" "data"

# 3. 检查 APCu 缓存
php -r "var_dump(apcu_fetch('ds:user_notifications:9:page:1'));"

# 4. 检查浏览器缓存
# 打开 DevTools → Network → Disable cache

5.4 HTTP 缓存头速查表

场景Cache-Control
静态资源public, max-age=31536000
动态页面no-cache, no-store, must-revalidate
API 响应no-cache
实时数据no-store

六、后续优化建议

  1. 添加缓存版本号:在 URL 中添加 ?v=timestamp 强制刷新
  2. 使用 WebSocket:推送新通知,避免轮询
  3. 监控缓存命中率:记录缓存命中/未命中比例,优化 TTL

结语

缓存是把双刃剑,用得好提升性能,用不好出现诡异 Bug。这次排查让我深刻体会到:

当你排除了所有不可能,剩下的那个,不管多么不可思议,都是真相。
—— 而这次的真相,藏在浏览器的缓存策略里。

参考链接

讨论回复

0 条回复

还没有人回复