跳过导航

Redis 分布式锁真的安全吗?从误释放到 fencing token

约 5 分钟...次浏览
专栏Redis 深度专题第 3 篇

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 上。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。