利用RedisLua脚本实现分布式信号量控制,通过原子操作避免INCR与EXPIRE非原子问题。acquire脚本检查计数并自增且设置过期,release脚本递减计数。集群环境需添加hashtag,客户端需处理网络异常与僵尸锁问题。
先说说最常见的一个坑:直接用 INCR 加 EXPIRE 来做信号量,看起来简单,但线上大概率会翻车。问题就出在“原子性”上——这两条命令不是一起执行的,中间随时可能被其他请求打断,或者网络超时只执行了一半。结果就是:key 有值但没设过期时间,或者设了过期时间但自增没成功,后续请求一拥而入,超卖、并发越界全都来了。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
INCR + EXPIRE 实现信号量?两条命令非原子执行,中间被中断或超时后,key 要么有值但没过期时间,要么过期时间设了但没成功自增。后续请求一查,发现还能加,接着就超卖了。只有在 Lua 脚本里,Redis 才能保证“检查 → 加锁 → 设过期”三步走完,中间不被任何操作插队。
EVAL 脚本里怎么安全地实现 acquire 和 release?acquire 脚本的逻辑很直接:先看当前计数是否小于最大许可数,满足条件才执行 INCR 并设置过期时间;release 则是先检查 key 是否存在、是不是数字类型,再 DECR,归零后可以顺手删掉 key。这里有个容易踩的坑:不能先用 GET 拿到值再自己判断,必须用 redis.call('GET', KEYS[1]) 配合类型检查,否则空值或字符串值会让 INCR 直接报错 ERR value is not an integer or out of range。
一个常见的 acquire 脚本写法参考:
if redis.call('EXISTS', KEYS[1]) == 0 then
redis.call('SET', KEYS[1], 0)
redis.call('EXPIRE', KEYS[1], ARGV[2])
end
local current = tonumber(redis.call('GET', KEYS[1]))
if current and current < tonumber(ARGV[1]) then
redis.call('INCR', KEYS[1])
return 1
else
return 0
end
EVALSHA 替代 EVAL 有什么坑?脚本的 SHA1 值需要提前用 SCRIPT LOAD 加载,否则直接 EVALSHA 会返回 NOSCRIPT 错误。更关键的是集群环境:所有 KEYS 必须落在同一个 slot,否则 Redis Cluster 直接拒绝执行。解决方案是给信号量 key 加上固定 hash tag,比如 semaphore:{order},这样集群才能正确路由。
容易忽略的细节还有:
SCRIPT LOAD 返回的 SHA 是字符串,调用 EVALSHA 时必须原样传入,大小写敏感KEYS 数组传入,硬编码 key 名会导致集群路由失败DEL 粗暴删除,可能误删别人刚 acquire 的计数,应该只 DECR 不删Redis 执行 Lua 后返回整数:1 表示成功,0 表示拒绝。但网络中断、timeout、LOADING 状态等不会返回 0,而是直接抛异常。这时候不能简单重试,否则可能重复 acquire——必须配合唯一 client token(比如 UUID)写入 value,或者用 HASH 结构记录持有者,否则你根本分不清“到底有没有拿到锁”。
更现实的做法是:把信号量 key 的 value 设计成 current_count:timestamp:client_id 这类复合结构,方便 debug 和主动清理过期持有者。当然,脚本会因此变复杂,要不要这么做,取决于你是否真的需要强持有追溯能力。
真正难处理的是:acquire 成功但客户端在后续业务逻辑中崩溃,又没来得及 release。这时只能靠 EXPIRE 时间兜底,所以 ARGV[2](TTL)不能设太长,也不能太短——要略大于最长业务耗时,才能确保不留下“僵尸锁”。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述