不能直接用 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])
end
local current = tonumber(redis.call('GET', KEYS[1]))
if current and current
<h3>用 <code>EVALSHA</code> 替代 <code>EVAL</code> 有什么坑?</h3>
<p>脚本 SHA1 值需要提前 <code>SCRIPT LOAD</code>,否则 <code>EVALSHA</code> 会返回 <code>NOSCRIPT</code> 错误。更关键的是:集群模式下,所有 <code>KEYS</code> 必须落在同一个 slot,否则 Redis Cluster 会拒绝执行——这意味着信号量 key 必须带固定 hash tag,比如 <code>semaphore:{order}</code>,否则 <code>EVALSHA</code> 在集群中直接失败。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理"><img
src="https://img.php.cn/upload/skill/000/000/081/178960683454849.jpg" alt="Redis Skill - 高性能缓存管理" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="overflowclass">Redis Skill - 高性能缓存管理</a>
<p class="overflowclass">Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。</p>
</div>
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
<p>容易忽略的点:</p>
-
SCRIPT LOAD返回的 SHA 是字符串,调用EVALSHA时必须原样传入,大小写敏感 - 脚本中所有 key 必须通过
KEYS数组传入,硬编码 key 名会导致集群路由失败 - 释放操作如果用
DEL粗暴删除,可能误删别人刚 acquire 的计数,应只DECR不删
客户端如何处理 acquire 失败和连接异常?
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)不能设太长,也不能太短——要略大于最长业务耗时。










