必须用 redis 的 zincrby + zrevrange 实现排行榜,因其支持原子性加分、自动排序、高性能分页;数据库 order by 易锁表、慢查询、回表io高;误用 incr/set 会丢失排名能力;需 lua 防重、hmget 批量查详情、空榜兜底。

直接用 Redis 的 ZINCRBY + ZREVRANGE 实现,别在 PHP 层做排序或求和再塞进缓存——性能差、并发错、扩展难。
为什么不能用数据库 order by + limit 做实时榜
查一次排行榜就扫全表 or 走索引排序?当文章/用户量过万,ORDER BY score DESC LIMIT 20 会越来越慢;加了索引也扛不住高频写入下的锁竞争。更关键的是:点赞、浏览、转发这些行为是离散高频的,数据库不是为这类场景设计的。
- MySQL 单次查询无法天然支持「原子性加分 + 防重」,得靠事务+SELECT FOR UPDATE,QPS 上不去
- 分页跳转(如第10页)要
OFFSET 200,越往后越慢 - 榜单展示需要分数+ID+附加信息(如标题),查完 ID 还得回表,IO 成倍增加
必须用 ZINCRBY 而不是 INCR 或 SET
ZINCRBY 是唯一能同时满足「累加分数」和「自动按分排序」的操作。误用 INCR 只会得到一个孤立数字,完全丢失排名能力;用 SET 存 JSON 更是自废武功。
-
$redis->zIncrBy('rank:article:hot', 1, (string)$articleId)——$articleId务必转成字符串,整型可能被截断 - key 必须带业务前缀,比如
rank:article:hot和rank:article:like分开,避免混用冲突 - score 建议用整型(如浏览+1、点赞+5、转发+10),浮点虽支持但易引发精度比对问题
查榜必须用 ZREVRANGE WITHSCORES,且禁止 PHP 层二次排序
Redis 的 zset 本身已按 score 排好序,ZREVRANGE 直接取 top N 是 O(log N + M) 复杂度;若用 ZRANGE 拿到的是最低分段,方向就反了。
- 正确调用:
$redis->zRevRange('rank:article:hot', 0, 9, ['WITHSCORES' => true]) - 返回格式是关联数组:
['123' => '456', '456' => '321'],键是 member(如 articleId),值是当前总分 - 前端需要标题等字段?别在循环里查 DB —— 提前用
HMGET批量捞 hash 数据,例如$redis->hMGet('article:123', ['title', 'cover']) - 绝对不要对返回结果再
usort(),纯属浪费 CPU
防重复点赞必须用 Lua 脚本,ZSCORE + ZINCRBY 分两步会出竞态
两个请求几乎同时查 ZSCORE 都返回 null,接着都执行 ZINCRBY,结果就是同一个人点了两次,分数多加一。
- 原子脚本示例:
if redis.call("zscore", KEYS[1], ARGV[1]) == false then return redis.call("zincrby", KEYS[1], 1, ARGV[1]) else return 0 end - ThinkPHP6 中调用:
$redis->eval($script, ['rank:article:like'], [$articleId]) - 返回
0表示已存在,非零是新分数;前端据此控制按钮状态 - 空榜单穿透风险:查
ZREVRANGE返回空时,顺手设个SET empty:rank:article:hot:123 "1" EX 60,下次先查这个标记
真正麻烦的从来不是“怎么写”,而是 key 设计是否隔离、member 类型是否统一、空值兜底是否覆盖所有分支——这些细节不卡死,上线后就会在凌晨三点弹告警。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











