必须用Lua脚本保证排队计数器原子性,因INCR单命令原子但多步逻辑(检查上限、增计数、存ID)存在竞态;Lua在Redis服务端串行执行,可安全实现进队限流与出队同步减数。

直接用 INCR 或 INCRBY 处理排队计数器在高并发下会丢数据,必须用 Lua 脚本保证原子性。
为什么不能只靠 INCR?
单个 INCR 看似原子,但排队逻辑往往不止加一:比如“进队前检查上限”“进队后写入玩家ID列表”“超限时返回错误”。这些步骤一旦拆成多条命令,中间就可能被其他客户端插入,导致超限或漏计数。
常见错误现象:ERR value is not an integer or out of range(误把非数字值当计数器)、QUEUED 响应后没执行(误用 pipeline 代替原子脚本)、计数器数值比实际排队人数少。
- Redis 的单命令原子性 ≠ 多命令组合原子性
-
EVAL执行 Lua 脚本时,整个脚本在服务端串行运行,无竞态 - 脚本内不能调用异步命令(如
PUBLISH),也不能 sleep 或阻塞
一个安全的进队+限流 Lua 脚本
典型场景:最多容纳 100 人排队,进队前检查,成功则计数 +1 并存入 queue:players 列表,失败返回 -1。
local max_size = tonumber(ARGV[1])
local queue_key = KEYS[1]
local counter_key = KEYS[2]
local current = tonumber(redis.call('GET', counter_key) or '0')
if current >= max_size then
return -1
end
redis.call('INCR', counter_key)
redis.call('RPUSH', queue_key, ARGV[2])
return current + 1
调用方式:EVAL <script> 2 queue:players queue:counter 100 player_12345</script>
-
KEYS[1]和KEYS[2]必须显式传入,不能硬编码键名(否则无法集群模式运行) - 所有 Redis 命令必须用
redis.call(),不能用redis.pcall()除非你主动处理错误返回 - 脚本里不要用
math.random()—— 它不是纯函数,在 Redis Cluster 中会导致哈希不一致
出队时如何同步更新计数器?
出队不只是 LPOP,还得把计数器减 1。同样必须原子:先取玩家、再删、再减计数,三步缺一不可。
local queue_key = KEYS[1]
local counter_key = KEYS[2]
local player = redis.call('LPOP', queue_key)
if not player then
return nil
end
redis.call('DECR', counter_key)
return player
调用:EVAL <script> 2 queue:players queue:counter</script>
- 注意
LPOP返回nil时,DECR仍会执行(Redis 会把空字符串转成 0 再减 1 → -1),所以要先判断再操作 - 如果业务需要“出队并通知”,别在 Lua 里
PUBLISH;改用出队后由客户端触发通知,避免脚本超时 - 计数器用
GET查看时,务必确认它和LLEN queue:players一致 —— 不一致说明有脚本没跑完或 key 混用了
最易被忽略的是键命名空间隔离:不同服、不同活动的排队队列必须用不同 KEYS,否则脚本在多个上下文复用时会互相污染。Lua 脚本本身无状态,但它的行为完全取决于你传入的 key 和 arg —— 错一个字符,数据就错一片。











