不能直接用 INCR + EXPIRE 实现信号量,因为二者非原子执行,中间可能中断导致 key 有值无过期或设了过期却未自增,引发超卖;必须用 Lua 脚本将检查、增减、设过期封装为原子操作。
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])endlocal current = tonumber(redis.call('GET', KEYS[1]))if current and current < tonumber(ARGV[1]) then redis.call('INCR', KEYS[1]) return 1else return 0end
EVALSHA 替代 EVAL 有什么坑?脚本 SHA1 值需要提前 SCRIPT LOAD,否则 EVALSHA 会返回 NOSCRIPT 错误。更关键的是:集群模式下,所有 KEYS 必须落在同一个 slot,否则 Redis Cluster 会拒绝执行——这意味着信号量 key 必须带固定 hash tag,比如 semaphore:{order},否则 EVALSHA 在集群中直接失败。
容易忽略的点:
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)不能设太长,也不能太短——要略大于最长业务耗时。