redis和分布式锁

Redis 分布式锁这个话题,网上文章确实写烂了。但大部分要么停留在 SETNX 那一套,要么直接甩一个 Redisson 让你用,中间那层"为什么"讲不清楚。更关键的是,很少有人告诉你:分布式锁本身就有它解决不了的问题

我尽量把这事说透。

从最原始的实现说起

早期大家用 SETNX + EXPIRE 两条命令:

复制代码
SETNX lock_key 1
EXPIRE lock_key 30

问题很明显:这俩命令不是原子的。客户端执行完 SETNX 拿到锁,还没来得及设过期时间就挂了,这把锁就永远释放不了。

Redis 2.6.12 之后,SET 命令支持了 NX 和 PX 参数,一条命令搞定:

复制代码
SET lock_key unique_value NX PX 30000

NX 是"不存在才设置",PX 是过期时间(毫秒)。原子性问题解决了。

但还有个坑:unique_value 必须是客户端唯一的,比如 UUID。

为什么?假设 A 拿到锁,过期时间 30 秒,但业务逻辑跑了 35 秒。第 30 秒锁自动过期,B 拿到了锁。第 35 秒 A 执行完了,去删锁------把 B 的锁给删了。后面 C 又能拿到锁,和 B 同时操作,出事了。

所以释放锁不能直接 DEL,得先验证是不是自己的锁:

复制代码
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

这段 Lua 脚本为什么能保证原子性?因为 Redis 是单线程执行命令的,一段 Lua 脚本在执行过程中不会被其他命令打断。这是 Redis 做分布式锁的底层基础。

到这一步,单节点 Redis 的分布式锁"基本"可用了。我说"基本",是因为还有几个深坑。

第一个深坑:锁过期了业务没跑完

这是生产中最常见的问题。

锁过期时间设短了,业务没执行完锁就没了,别人进来了;设长了,一旦客户端挂掉,其他人要干等。

解决方案是看门狗机制:拿到锁之后,起一个后台线程定期给锁续期。只要业务还在跑,锁就不会过期;业务结束了,主动释放锁,看门狗停掉。

Redisson 就是这么干的,默认锁 30 秒,每 10 秒续一次。

但这里有个坑很多人不知道:看门狗续期依赖网络连接。如果你的客户端和 Redis 之间网络断了,续期请求发不出去,锁照样会过期。而你的业务代码可能还在本地跑着,完全不知道锁已经没了。

我见过一个案例:服务 A 拿到锁开始处理订单,中间网络抖动了 15 秒,Redisson 续期失败,锁过期。服务 B 拿到锁也开始处理同一笔订单。网络恢复后,两边都在写数据库,订单状态乱了。

这不是 Redisson 的 bug,是分布式锁的本质局限。

第二个深坑:主从切换丢锁

用 Redis Sentinel 做高可用,主节点挂了从节点顶上,听起来很美好。

但 Redis 主从复制是异步的。

场景:客户端在主节点上拿到锁,主节点还没来得及把这个写操作同步到从节点,就挂了。从节点升为主节点,这时候锁的信息根本不存在。另一个客户端来请求,又拿到了"同一把锁"。

两个客户端都认为自己持有锁,同时操作共享资源,出事。

这个问题在 Redis Cluster 里同样存在,因为 Cluster 内部也是异步复制。

有多大概率?概率很小,但不是零。你的系统跑得越久、并发越高,迟早会遇到。

RedLock:看起来很美的方案

Redis 作者 Antirez 提出了 RedLock 算法试图解决上面的问题:部署 N 个完全独立 的 Redis 实例(不是集群,是独立的),获取锁时依次向每个实例申请,只有在大多数(N/2+1)实例上都获取成功,才算真正拿到锁。

思路是:就算一两个实例挂了或者数据丢了,只要大多数还在,锁的状态就是可靠的。

但分布式系统专家 Martin Kleppmann 写了篇文章直接开怼,标题就叫"How to do distributed locking"。核心论点不是说 RedLock 实现有 bug,而是说这个思路从根上就有问题

他举了个例子:

  1. 客户端 A 获取锁成功
  2. 客户端 A 发生长时间 GC 暂停(或者网络延迟)
  3. 锁过期了,客户端 B 获取锁成功
  4. 客户端 A 从 GC 中恢复,它不知道锁已经过期,继续操作共享资源
  5. 客户端 A 和 B 同时在操作,出事

注意,这个问题不是 RedLock 特有的,所有基于过期时间的分布式锁都有这个问题。RedLock 没解决它,只是让"锁丢失"的概率变低了一点。

真正的安全:Fencing Token

Martin Kleppmann 提出的解决方案叫 fencing token

思路是这样的:每次获取锁的时候,锁服务返回一个单调递增的 token(比如 1, 2, 3...)。客户端拿着这个 token 去操作共享资源,资源端会记录见过的最大 token,如果收到的 token 比记录的小,直接拒绝。

回到刚才的例子:

  1. 客户端 A 获取锁,拿到 token=33
  2. 客户端 A 发生 GC 暂停
  3. 锁过期,客户端 B 获取锁,拿到 token=34
  4. 客户端 B 带着 token=34 写入存储,存储记录 max_token=34
  5. 客户端 A 恢复,带着 token=33 去写入,存储发现 33 < 34,拒绝

这才是真正安全的方案。但问题是:Redis 本身不支持 fencing token,你的存储层也得配合改造

ZooKeeper 天然支持这个------它的 zxid 就是单调递增的,每次创建临时节点都能拿到。这也是为什么在强一致性要求的场景下,ZooKeeper 比 Redis 更适合做分布式锁。

那 Redis 分布式锁还能用吗?

能用,但你得清楚它的边界。

Redis 分布式锁本质上是一种尽力而为的协调机制,它能防止大部分并发冲突,但不能保证 100% 互斥。在以下场景它工作得很好:

  • 防止重复执行(幂等性兜底)
  • 控制并发数量(限流)
  • 避免缓存击穿(热点数据重建)
  • 分布式任务调度(允许偶尔重复执行)

这些场景的共同特点是:就算锁偶尔失效,业务也能兜住

但如果你的场景是:

  • 扣库存、扣余额(超卖就是资损)
  • 金融交易(重复扣款就是事故)

单靠 Redis 分布式锁是不够的,你需要:

  • 数据库层面的乐观锁/悲观锁
  • 唯一约束防重
  • 业务层幂等设计
  • 或者换 ZooKeeper/etcd

实际踩过的坑

坑一:Redisson 连接池耗尽导致续期失败

高并发场景下,Redisson 连接池打满了,看门狗的续期请求拿不到连接,锁悄悄过期。业务代码完全没感知,继续往下跑。后来我们加了监控:如果续期失败,主动抛异常中断业务。

坑二:锁的粒度太粗

一开始图省事,整个订单处理流程用一把锁。结果锁持有时间太长,其他请求全在排队,接口超时。后来拆成多把细粒度的锁:校验库存一把、扣减库存一把、创建订单一把。并发能力直接上来了。

坑三:忘记处理获取锁失败的情况

代码里写了 lock.tryLock(),但没处理返回 false 的情况,直接往下跑了。等于锁白加。这种低级错误 code review 没发现,上线后出了并发问题才查到。

选型建议

单 Redis + Lua 脚本:简单场景够用,但要清楚它的局限性。

Redisson:Java 项目首选,功能全,但要理解它的机制,别当黑盒用。

RedLock:不推荐。复杂度高,但并没有真正解决分布式锁的本质问题。

ZooKeeper/etcd:强一致性场景,愿意用性能换可靠性的时候用。

数据库行锁 :别忘了这个选项,简单场景下 SELECT ... FOR UPDATE 也能用,而且天然支持事务。

最后说一句:分布式锁用多了就是性能瓶颈。能用队列串行化的别用锁,能用 CAS 乐观控制的别用锁,能把热点 key 打散的别让大家抢同一把锁。

锁是用来兜底的,不是用来当主力的。

相关推荐
徐徐同学2 小时前
cpolar为IT-Tools 解锁公网访问,远程开发再也不卡壳
java·开发语言·分布式
视界先声2 小时前
国产分布式存储替代VMware vSphere?:20+功能对比,一文了解SmartX
分布式
露天赏雪8 小时前
Java 高并发编程实战:从线程池到分布式锁,解决生产环境并发问题
java·开发语言·spring boot·分布式·后端·mysql
没有bug.的程序员10 小时前
Spring Boot 事务管理:@Transactional 失效场景、底层内幕与分布式补偿实战终极指南
java·spring boot·分布式·后端·transactional·失效场景·底层内幕
LuminescenceJ11 小时前
GoEdge 开源CDN 架构设计与工作原理分析
分布式·后端·网络协议·网络安全·rpc·开源·信息与通信
组合缺一14 小时前
论 AI Skills 分布式发展的必然性:从单体智能到“云端大脑”的跃迁
java·人工智能·分布式·llm·mcp·skills
shepherd12615 小时前
深度剖析SkyWalking:从内核原理到生产级全链路监控实战
分布式·后端·skywalking
h7ml17 小时前
基于 RabbitMQ 构建异步化淘客订单处理流水线:解耦、削峰与失败重试
分布式·rabbitmq·ruby
夜月蓝汐17 小时前
分布式监控SkyWalking链路追踪
分布式·skywalking