分布式锁避坑指南:从 Redis SETNX 到 Redisson 看锁续期与并发安全
【摘要】 在分布式与高并发架构中,为了防止多节点同时修改同一共享资源(如商品超卖、重复计费、状态机乱序),分布式锁是必不可少的基础设施。很多团队在早期往往基于 Redis 简单封装一套分布式锁,但往往因忽略网络抖动、GC 停顿等边界场景,引发线上数据脏写。本文从最基础的命令演进讲起,剖析分布式锁的经典陷阱及工业级解决方案。 1. 基础演进:为什么单靠 SETNX 远远不够? 1.1 缺陷阶段一:加锁与...
在分布式与高并发架构中,为了防止多节点同时修改同一共享资源(如商品超卖、重复计费、状态机乱序),分布式锁是必不可少的基础设施。很多团队在早期往往基于 Redis 简单封装一套分布式锁,但往往因忽略网络抖动、GC 停顿等边界场景,引发线上数据脏写。
本文从最基础的命令演进讲起,剖析分布式锁的经典陷阱及工业级解决方案。
1. 基础演进:为什么单靠 SETNX 远远不够?
1.1 缺陷阶段一:加锁与设置超时非原子操作
早期常见的伪代码写法:
SETNX lock_key 1
EXPIRE lock_key 30
- 风险:如果在执行
SETNX成功之后、EXPIRE之前服务崩溃或重启,该锁将永远不会过期,导致死锁。
1.2 阶段二:原子性加锁
Redis 2.6.12 之后,官方支持在 SET 命令中直接携带条件和过期时间:
SET lock_key unique_value NX PX 30000
NX:仅在 Key 不存在时设置成功。PX 30000:设置毫秒级过期时间(30 秒)。unique_value:随机生成的唯一客户端标识(如 UUID + 线程 ID),严禁使用固定常量。
2. 释放锁陷阱:误删他人锁与 Lua 脚本方案
2.1 误删场景复现
- 客户端 A 成功获取锁,超时时间为 10 秒。
- 客户端 A 遭遇网络阻塞或耗时 Full GC,执行时间超过 10 秒。
- 锁自然过期失效,客户端 B 成功竞争到该锁。
- 客户端 A 恢复执行完毕,直接调用
DEL lock_key。 - 后果:客户端 A 删除了属于客户端 B 的有效锁,此时客户端 C 又能获取锁,造成严重的数据并发混乱。
2.2 正确姿势:Lua 脚本校验并释放
释放锁必须保证“判断 value 属于自己”与“执行删除”两个动作原子生效:
-- 比较传入的 ARG[1] 是否与 key 的值相等,相等才执行 DEL
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
调用执行:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key unique_value
3. 锁续期机制:如何应对耗时超预期?
即便解决了释放锁原子性,业务未跑完但锁提前失效的问题依然存在。工业界成熟的解法是引入看门狗(Watchdog)自动续期机制(以 Redisson 为代表):
- 后台守护线程:当客户端成功持有锁且未显式指定锁超时时,启动后台定时任务。
- 定时续期:默认锁租期为 30 秒,看门狗在达到租期三分之一(10 秒)时触发,执行 Lua 脚本将 Key 的过期时间重置回 30 秒。
- 安全释放:业务执行完毕显式释放锁,或者应用实例崩溃后守护线程自然消亡,锁将在剩余租期结束后自动失效,既保证不会永久死锁,又防止业务提前“裸奔”。
4. 主从切换与 CAP 取舍
在典型的 Redis 主从 / 哨兵 / 集群架构中:
- 客户端在 Master 节点加锁成功。
- Master 尚未将该 Key 同步给 Slave 时突然宕机。
- 哨兵或集群将 Slave 提升为新 Master。
- 新 Master 上并不存在该锁,其他客户端可重复加锁。
工程决策权衡:
- 一般业务场景:Redis 单节点 / 主从方案已能覆盖 99% 的防重需求,轻微并发穿透可通过数据库唯一索引(Unique Constraint)或乐观锁(Version 字段)作为最终兜底防线。
- 强一致金融级场景:若对一致性要求极高且不可容忍任何锁丢失,应考虑基于 Raft / Paxos 强一致协议的组件(如 ZooKeeper、etcd),或者结合业务状态机进行二次校验。
5. 核心实践 CheckList
- [ ] 原子性加锁:是否使用了
SET ... NX PX保证加锁原子性? - [ ] 唯一身份标识:Value 是否绑定了唯一的请求/线程标识,而非固定值?
- [ ] 原子性解锁:是否使用 Lua 脚本先校验所属权再删除?
- [ ] 续期兜底:长耗时任务是否配置了锁续期或合理预估了超时阈值?
- [ ] 底层数据库兜底:下游存储是否建立了唯一索引,避免锁失效时发生静默脏写?
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)