首页未读数必须通过redis缓存获取,失败再查数据库并回填,严禁模板中执行count(*);新消息插入需在事务内同步incr redis计数,批量已读则直接set为0,所有操作须统一缓存协议。

首页直接读 Redis 缓存,别查数据库。 每次渲染首页都执行 COUNT(*) 查未读数,1000 个用户同时刷首页,MySQL 就会卡住。Redis 是唯一能扛住的方案,且必须配合原子更新和 fallback 机制。
为什么不能在首页模板里写 SELECT COUNT(*) FROM message WHERE to_user_id = ? AND status = 0
这是最典型的性能雷区。ThinkPHP6 的模板(无论 think 还是 php 驱动)不拦 SQL,但你在控制器或公共 service 层调用这个查询,等于把高并发压力直接引向 DB。尤其首页是全站访问入口,QPS 最高,COUNT(*) 在没索引或数据量大时会触发全表扫描。
- 联合索引
(to_user_id, status, created_at)虽能加速列表查询,但对COUNT(*)效果有限——MySQL 仍需遍历索引树统计行数 - status 字段若允许 NULL,
WHERE status = 0会跳过所有 NULL 行,但索引可能失效 - TP6 模型层没有内置“未读计数缓存”逻辑,
User::where(...)->count()就是裸查
怎么在首页控制器里安全获取未读数
核心是:先读 Redis,失败再查 DB 并回填。不要在模板中做任何数据库操作。
- 用
app('cache')->store('redis')->getRedis()获取原生 Redis 实例(Cache::get()不支持 key pattern,也不保证原子性) - 未读 key 格式统一为
unread:{user_id},避免拼接错误 - 前端登录后已写入用户 ID 到 session 或 token,控制器可直接取
$this->request->user->id(假设你用了 JWT 中间件) - 示例代码片段:
use think\facade\Cache; $userId = $this->request->user->id; $key = 'unread:' . $userId; $count = app('cache')->store('redis')->getRedis()->get($key); if ($count === false) { // fallback:查 DB 并 SET 回填 $count = \app\model\Message::where('to_user_id', $userId) ->where('status', 0) ->count(); app('cache')->store('redis')->getRedis()->setex($key, 300, (int)$count); // 5 分钟过期 } $this->assign('unread_count', (int)$count);
消息入库时怎么确保 Redis 和 DB 一致
不靠定时同步,而靠写时事务保障。漏掉这步,首页显示的未读数永远不准。
- 新消息插入必须在事务内完成:
DB::transaction(function () use ($data) { Message::create($data); app('cache')->store('redis')->getRedis()->incr('unread:'.$data['to_user_id']); }); - 批量标记已读(如“全部已读”按钮)也要原子更新:
UPDATE message SET status = 1 WHERE to_user_id = ? AND status = 0执行后,立刻SET unread:{user_id} 0 - 绝对不用
DECR或DEL来减未读数——用户点一次“已读”,DB 更新成功但 Redis 减失败,就会出现负数或残留 - Redis key 过期时间设为 300 秒(5 分钟),既防雪崩,又给 DB 异常留出缓冲窗口
真正难的不是代码怎么写,而是所有写消息、读消息、清未读的操作路径,都得经过同一套缓存协议。漏掉任意一个入口(比如后台脚本直连 DB 插消息、WebSocket 服务单独发通知),未读数就立刻失真。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











