必须用zincrby配合lua脚本实现原子操作,因复杂逻辑(如积分达标判断、日上限限制)若拆分为zscore+zadd多步将引发竞态;lua在redis单线程中执行,可确保判断、计算、更新全在一次调用中完成。

Redis 用 Lua 脚本做排行榜积分增减,核心就一条:必须用 ZINCRBY 配合原子性脚本,不能拆成 ZSCORE + ZADD 两步查改。
为什么非得写 Lua 脚本?
直接调 ZINCRBY 看似够用,但真实场景常需「先判断当前积分是否达标,再决定加多少」或「叠加奖励分时限制单日上限」——这些逻辑一旦拆成客户端多次往返,就会出现竞态:两个请求同时读到 99 分,各自加 1,结果只变成 100 而不是 101。
Lua 脚本在 Redis 单线程中执行,天然原子。只要把判断、计算、更新全塞进一个 EVAL 或 EVALSHA 里,就彻底规避 race condition。
EVAL 脚本里怎么安全读写有序集合?
关键点是别用 redis.call("ZSCORE", ...) 拿值后在 Lua 里算完再 redis.call("ZINCRBY", ...) —— 这看似一步,实则两次 Redis 内部操作,中间可能被其他脚本插入。
正确做法是:用 ZINCRBY 先增,再立刻 ZSCORE 读新值做后续判断(注意顺序),或直接用 ZINCRBY 的返回值(它返回增量后的 score):
return redis.call("ZINCRBY", KEYS[1], ARGV[1], ARGV[2])
常见组合逻辑示例(带日积分上限):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用
redis.call("ZSCORE", KEYS[1], ARGV[2])读昨日分(需提前存好昨日 key) - 用
redis.call("ZINCRBY", KEYS[1], ARGV[1], ARGV[2])增当前总分 - 用
redis.call("ZINCRBY", KEYS[2], ARGV[1], ARGV[2])同步增日榜(KEYS[2] 是 daily:rank) - 最后用
redis.call("ZCARD", KEYS[1])返回总人数(可选)
参数传错或 key 不存在会怎样?
ZINCRBY 对不存在的 member 自动创建,不存在的 key 也自动创建,基本不报错;但 ZSCORE 查不到 member 会返回 nil,Lua 里直接拿 nil + 1 会报错 attempt to perform arithmetic on a nil value。
务必加空值防护:
local score = redis.call("ZSCORE", KEYS[1], ARGV[2])
if not score then score = 0 end
local new_score = score + tonumber(ARGV[1])
另外,ARGV 全是字符串,数值运算前必须用 tonumber() 转,否则拼接成字符串(比如 "10" + "5" 得 15,但 "10" + "abc" 就崩)。
性能和缓存怎么权衡?
短小脚本(EVAL 直接发没问题;高频调用建议先 SCRIPT LOAD 得到 SHA1,再用 EVALSHA,省去每次传输脚本的开销。
但别为了“省流量”把复杂逻辑硬塞进 Lua:比如要查 10 个用户的 rank、再批量更新、再发消息——这种该拆成多个原子操作 + 客户端协调,硬塞进 Lua 会阻塞 Redis 主线程,拖慢所有请求。
真正该进 Lua 的,永远只是「读-算-写」闭环里那两三步强依赖原子性的动作。










