Redis 分布式锁真的安全吗?从误释放到 fencing token
Redis 分布式锁容易写出一个能运行的版本,却很难证明在暂停、超时和故障转移下仍正确。锁的目标不是“Redis 中有一个 Key”,而是保证共享资源不会接受过期持有者的冲突写入。
1. 最小正确获取方式
SET lock:order:123 7f5b... NX PX 30000
NX:Key 不存在才设置;PX:同一条命令设置租约,避免加锁后崩溃形成永久锁;- value:每次获取生成唯一 token,用于证明所有权。
不能用 SETNX 成功后再 EXPIRE,两条命令之间进程可能崩溃。
2. 解锁必须比较 token
客户端 A 的锁过期后,B 获得同名锁。若 A 此时直接 DEL,就会删掉 B 的锁。比较与删除必须原子执行:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
客户端库应使用经过审查的实现,不要在业务代码中到处复制半套锁逻辑。
3. 租约过期后的“幽灵持有者”
即使 token 解锁正确,仍有更深的问题:
A 获锁 → 长时间 GC/机器暂停 → 锁过期
B 获锁 → 更新数据库
A 恢复 → 继续以为自己持锁 → 覆盖 B 的结果
Redis 只能阻止 A 再次拥有锁 Key,不能撤销 A 已经获得的执行权限。自动续期(watchdog)可以降低正常长任务过期概率,却无法彻底解决网络分区、进程暂停或续期线程失调。
业务执行时间应有上界,并在关键步骤再次验证所有权;但“检查后再写”之间仍可能失效,所以严格场景需要资源端参与防护。
4. fencing token 才能拒绝旧持有者
锁服务每次发放单调递增令牌:
A 获得 token=41
B 后来获得 token=42
存储系统已接受 42 后,拒绝所有 token < 42 的写
数据库可在资源行保存最后 token,并用条件更新:
UPDATE account
SET balance = ?, fencing_token = 42
WHERE id = ? AND fencing_token < 42;
这把安全判断放到了真正的资源端。仅使用 UUID 能识别“是不是我的锁”,却不能比较新旧持有者。
5. 主从切换的窗口
Redis 异步复制下可能发生:主节点确认 A 加锁,但锁尚未复制到从节点,主节点故障;从节点晋升后,B 又成功加锁。此时两人都认为自己成功。
WAIT 可要求此前写入被一定数量副本确认,降低故障转移时丢失锁的概率,但成功返回不代表副本已落盘,也不能把异步复制 Redis 变成严格共识系统。Redis 7.2+ 的 WAITAOF 可等待本地/副本 AOF fsync,但同样不提供线性一致的共识锁语义,并且受 AOF 配置与拓扑约束。若业务要求任何故障下都只允许一个持有者,应评估基于共识协议的协调服务,或直接利用数据库唯一约束、行锁和条件更新。
6. Redlock 争议应该如何理解
Redlock 尝试在多个独立 Redis 节点上获取多数锁,并根据耗时判断剩余租约。它提高对单节点故障的容忍度,但安全性依赖时钟、延迟、暂停模型和业务假设。争论重点不是算法名字,而是你的系统是否能接受租约锁的故障模型。
选择前写清楚:
- 锁失败是否只是重复执行一次任务,还是会造成资金错误?
- 下游是否幂等、带版本号或 fencing token?
- 能否容忍短时间双持有者?
- 发生网络分区和长暂停时希望如何处理?
7. 锁并不能替代幂等
客户端可能执行成功但响应丢失,重试后再次执行业务;锁释放后重复消息也会重新获得锁。订单创建、扣款等操作仍需业务幂等键、唯一约束或状态机 CAS。
INSERT INTO payment(request_id, order_id, amount)
VALUES (?, ?, ?);
-- request_id UNIQUE
锁减少并发冲突,幂等约束决定重复请求的最终结果。
8. 检查清单
- 获取是否使用单条
SET NX PX? - value 是否每次唯一,解锁是否 Lua 比较后删除?
- 租约是否覆盖合理执行时间,并处理暂停与续期失败?
- 下游是否支持幂等、版本检查或 fencing token?
- 是否理解主从异步复制与故障转移窗口?
- 获取失败是等待、跳过还是降级,是否有超时与抖动?
- 业务是否真的需要分布式锁,能否用唯一约束解决?
Redis 锁适合降低并发重复、保护可容忍且已明确故障窗口的任务。客户端还应使用单调时钟计算租约剩余时间,限制获取总超时,并确认所用库在 Cluster 重定向、连接重试和脚本缓存失效时的语义。对于强正确性资源,必须让数据库或资源服务能够拒绝旧持有者,而不是把全部希望寄托在一个会过期的 Key 上。