应使用ZINCRBY而非INCR实现点赞排序:以articleId为member、变化量为score存入zset;查榜用ZREVRANGE WITHSCORES;防重复点赞需Lua原子脚本;空榜单须设空标记防穿透。

点赞时用 ZINCRBY 而不是 INCR
ThinkPHP 本身不封装 Redis 的 Zset 操作,直接调用底层连接更可靠。很多人误以为给用户点赞就是对某个 key 做 INCR,结果发现排序失效——因为 INCR 只能存单个数值,没法按分数自动排序。
真正该用的是 ZINCRBY:它把「用户ID」当 member、「点赞数变化量」当 score,写入一个 zset,天然支持按分数升序/降序查榜。
- 场景:用户 A 点赞文章 B,需增加文章 B 在排行榜中的分数
- 操作:
$redis->zIncrBy('rank:article', 1, $articleId) - 注意:
$articleId必须是字符串(如'123'),整型可能被截断或类型转换异常 - 别漏掉 key 前缀(如
rank:article),否则多个业务混在一起会互相干扰
查榜必须用 ZREVRANGE + WITHSCORES
默认 ZRANGE 是从小到大排,点赞榜要“最多点赞在前”,得用 ZREVRANGE。而且只取 ID 不够,得同时拿到分数做展示,否则前端还得挨个查分,IO 浪费严重。
- 正确写法:
$redis->zRevRange('rank:article', 0, 9, ['WITHSCORES' => true]) - 返回是关联数组:
['123' => '45', '456' => '32', ...],键是 articleId,值是当前总分 - 如果用
ZRANGE,你会拿到点赞最少的几条,和预期完全相反 - 别在 PHP 层手动 sort —— Redis 已排序,再 sort 是纯 CPU 白耗
去重点赞得靠 ZSCORE 判断 + 事务控制
同一个用户反复点同一文章,不能重复加分。但 Redis zset 本身不阻止重复 ZINCRBY,它只是累加。所以得先查当前 score,再决定是否执行加分。
立即学习“PHP免费学习笔记(深入)”;
- 错误做法:先
ZSCORE再ZINCRBY,中间可能被并发请求插队 - 稳妥做法:用 Lua 脚本原子执行(ThinkPHP 的
eval方法) - 示例脚本:
if redis.call("zscore", KEYS[1], ARGV[1]) == false then return redis.call("zincrby", KEYS[1], 1, ARGV[1]) else return 0 end - 调用:
$redis->eval($script, ['rank:article'], [$articleId]) - 返回 0 表示已点过,非 0 是新加分后的总分
缓存穿透风险:空榜单不能直接返回 null
如果某类文章还没人点赞,ZREVRANGE 返回空数组。这时候如果业务逻辑没兜底,前端可能报错或空白。更麻烦的是,攻击者故意刷大量不存在的 $articleId,每次都会穿透到 Redis 查询空结果,压垮后端。
- 简单防御:查完为空时,往 Redis 写个空标记(如
empty:rank:article:999,过期 1 分钟) - 别用空字符串或 0 做默认值——它们可能是合法分数,无法区分真假
- ThinkPHP 中可统一包装查询方法,在
zRevRange后加一层空值判断和缓存写入 - 注意:Zset 本身不支持设置空成员,所以这个兜底必须额外用 string 类型 key 实现
zset 的 score 是浮点数,精度问题、负分场景、超大排行榜的分页性能——这些都在真实压测里才露出来,别只在本地跑通就认为没问题。











