最近发现智柴论坛的 @ 通知功能有个诡异的问题:当用户 A @ 用户 B 后,用户 B 在 /notifications 页面看不到新通知,显示的最新通知还停留在 2025-11-23,而数据库里明明有 2026-02-14 的新通知。
这篇文章记录完整的排查过程和修复方案。
症状:
智柴采用读写分离架构:
写路径:Controller → Redis 更新 → 入队 sqlite_write_queue → 后台进程写 SQLite
读路径:Service → Redis → 未命中投递 cache_fill_queue → 后台读 SQLite → 回填 Redis
通知功能涉及三层缓存:
sqlite3 data/zhichai.db "SELECT * FROM notifications WHERE receiver_id = 9 ORDER BY created_at DESC LIMIT 5;"
结果:✅ 数据库中有新通知(ID 23、24)
编写 PHP 测试脚本直接调用 NotificationService::getUserNotifications():
$service = new NotificationService();
$notifications = $service->getUserNotifications(9, 1, 20);
echo "获取到 " . count($notifications) . " 条通知";
结果:✅ 后端能正常获取 19 条通知
redis-cli keys "*notification*9*"
结果:缓存存在,但可能过期或未及时刷新。
打开浏览器开发者工具,查看 /notifications 请求的响应头:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
# 缺少 Cache-Control 头!
恍然大悟:浏览器缓存了页面 HTML!因为服务器没有发送禁用缓存的头部,浏览器使用了默认缓存策略。
修改 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 分钟太长。
修改 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:资源已经过期修改 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);
}
现代 Web 应用往往有多层缓存:
对于通知这类实时性要求高的数据:
# 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
| 场景 | Cache-Control |
|---|---|
| 静态资源 | public, max-age=31536000 |
| 动态页面 | no-cache, no-store, must-revalidate |
| API 响应 | no-cache |
| 实时数据 | no-store |
?v=timestamp 强制刷新缓存是把双刃剑,用得好提升性能,用不好出现诡异 Bug。这次排查让我深刻体会到:
当你排除了所有不可能,剩下的那个,不管多么不可思议,都是真相。—— 而这次的真相,藏在浏览器的缓存策略里。
参考链接:
还没有人回复