不知道大家有没有这种感觉:简历上写着“熟悉Redis”、“了解高并发”,觉得自己准备得挺充分。可一旦面试官真的问起:“如果让你设计一个分布式锁,你会怎么做?
哪怕脑子里蹦出了
setnx、Lua脚本这些词儿,可一旦被追问细节——比如“看门狗机制具体是怎么跑的?”或者“主从切换导致锁丢失怎么解决?”,立马就开始支支吾吾,半天说不清楚。明明平时都在用,真到了嘴边却像卡壳了一样,那种尴尬感真的太真实了。别慌,今天这篇文章不整虚的。咱们把那些高大上的名词拆开揉碎,从最基础的互斥性到复杂的红锁算法,带你一步步还原一个真正能抗住生产环境考验的分布式锁。
一、基础版锁:实现互斥性
核心需求:保证同一时间只有一个线程执行业务
分布式锁最根本的要求就是互斥性:同一个业务代码块,在多服务、多线程场景下,同一时间只能有一个线程执行。
Redis天然支持这个特性,核心命令就是 SetNX(SET if Not Exists):只有当key不存在时,才能创建成功,返回成功;key已存在则创建失败。
实现逻辑:
-
线程执行业务前,尝试通过
SetNX创建锁key -
创建成功:抢到锁,正常执行业务代码
-
创建失败:没抢到锁,放弃执行或等待重试
线程宕机导致永久死锁
如果只写 SetNX 加锁,会出现致命问题:如果抢到锁的线程突然宕机、程序崩溃,没来得及手动删除锁key,这个锁就会永久存在Redis中,后续所有线程都永远抢不到锁,直接死锁,业务彻底瘫痪!
解决方案:给锁设置过期时间
加锁的同时,必须给锁key设置合理的过期时间。哪怕线程意外宕机,锁到期后会自动释放,避免永久死锁问题。
二、解决锁误删问题
基础版锁+过期时间后,又会出现第二个经典问题:锁误删,删掉了其他线程的有效锁。

我们来模拟一个真实线上场景:
-
线程1成功抢到锁,锁过期时间设为10秒
-
线程1业务执行很慢,超过10秒没跑完,锁自动过期被Redis释放
-
线程2立马抢到锁,开始执行业务
-
此时线程1业务终于跑完了,执行解锁逻辑,直接删除锁key
-
结果:线程1删掉了线程2的锁,线程2的业务直接裸奔,并发安全彻底失效!
解决方案:校验锁归属 + Lua原子解锁
问题根源很简单:所有线程的锁key都一样,解锁时不判断归属,直接删key。
第一步优化:锁key的value存唯一标识
加锁时,value不要写固定值,存入UUID+线程ID的唯一字符串,代表这把锁的专属持有者。
第二步优化:解锁先校验,再删除
线程解锁前,先获取锁的value,判断是否等于自己的唯一标识,只有是自己的锁,才允许删除。
这里又有一个新坑:判断、删除是两步操作,非原子!
再给大家模拟一个极端并发场景:
-
线程1执行完业务,获取锁value,判断是自己的锁
-
判断完成的瞬间,线程1阻塞、CPU切换线程
-
此时线程1的锁刚好过期自动释放,线程2成功抢到锁
-
线程1恢复执行,直接执行删除逻辑,误删线程2的锁
解决方案:Lua脚本解锁
将获取value、判断归属、删除key三段逻辑写在同一个Lua脚本中,Redis会一次性执行完整个脚本,中间不会被其他线程打断,完美保证解锁的原子性,彻底杜绝锁误删问题。
三、锁续期(看门狗机制)
核心痛点:业务没跑完,锁先过期了
上面的优化搞定了误删问题,但还有一个绕不开的问题:锁过期时间不好把控。
如果我们把过期时间设太短,复杂业务还没执行完,锁就自动释放,出现并发问题;设太长,一旦线程宕机,锁需要很久才能释放,影响业务可用性。
举个例子:订单结算、批量数据处理业务,执行时间不确定,可能5秒跑完,也可能因为数据量很大执行20秒。固定的过期时间,完全无法适配。
解决方案:Redisson看门狗续期机制
主流框架Redisson给出的最优解就是看门狗线程,核心逻辑超简单:
线程成功加锁后,启动一个守护线程,定时(默认每10秒)检查当前锁是否还被持有,如果业务没执行完、锁还存在,就自动给锁续期(重置过期时间)。
为什么要用守护线程?
如果是普通线程,哪怕业务线程执行完毕、宕机消失,续期线程还会一直运行,不停给锁续期,导致锁永远无法释放,造成死锁。
而守护线程会跟随业务线程生命周期:业务线程结束/宕机,看门狗守护线程自动消亡,不再续期,锁到期正常释放,完美闭环。
四、可重入锁,避免递归死锁
方法嵌套、递归调用死锁
实际开发中,我们经常遇到方法嵌套调用、递归调用的场景:同一个线程,已经拿到了分布式锁,后续又调用了需要同一把锁的方法。
如果锁不支持可重入,线程再次加锁时就会加锁失败,直接自己卡死自己,造成死锁。
就像我们本地的
synchronized、ReentrantLock都是可重入锁,分布式锁也必须支持可重入!
解决:借鉴本地锁计数器机制
本地锁的可重入核心是计数器:
-
synchronized:依靠对象监视器计数器 -
ReentrantLock:依靠AQS的state计数器
每重入一次,计数器+1;每释放一次锁,计数器-1;计数器归0时,才真正释放锁。
Redisson可重入锁实现
Redisson用Redis的Hash结构完美实现可重入锁,结构设计超巧妙:
-
Hash的key:业务锁的唯一标识(比如lock:order)
-
Hash的field:UUID+线程ID(保证集群环境下线程标识唯一,避免不同机器线程ID重复)
-
Hash的value:锁重入计数器
执行逻辑:
-
同一线程首次加锁:计数器置1
-
同一线程重复加锁:计数器+1
-
每次解锁:计数器-1
-
计数器减为0:删除整个Hash结构,彻底释放锁
五、阻塞锁
普通非阻塞锁的问题
我们前面写的基础锁,都是非阻塞锁:抢锁失败直接返回,不等待。
但真实业务中,大部分场景需要阻塞锁:抢锁失败后,不要直接放弃,等待锁释放后重新尝试抢锁,和
ReentrantLock阻塞逻辑一致。
两种阻塞锁实现方案
自旋重试
抢锁失败的线程,不断循环重试抢锁,直到抢到锁或超时。
优点:实现简单、逻辑清晰
缺点:高并发场景下,大量线程空轮询,极度消耗CPU资源,性能很差

Redis发布订阅
这是生产环境最优方案,Redisson默认使用此机制:
-
线程抢锁失败后,订阅锁对应的消息频道,然后阻塞等待
-
持有锁的线程解锁时,发布「锁释放」消息到频道
-
所有阻塞等待的线程收到消息,被唤醒,重新竞争抢锁
-
同时配置最大等待超时时间,避免线程无限阻塞
完美解决自旋CPU浪费问题,兼顾性能和可用性。
六、Redis主从架构锁丢失问题
主从架构致命漏洞
我们平时用的Redis基本都是一主多从架构,保证高可用,但这会导致分布式锁互斥性彻底失效!
场景复现:
主线程在主节点加锁成功,执行业务
此时锁数据还没同步到从节点,主节点突然宕机
集群自动将从节点升级为新主节点
新主节点没有锁数据,其他线程可以直接加锁成功
结果:多个线程同时执行业务,锁彻底失效!
两种解决方案:联锁 vs 红锁
Redisson 联锁
部署多台独立的Redis主节点,必须所有主节点全部加锁成功,才算真正抢到锁。
优点:安全性极高,单节点宕机不丢锁
缺点:容错率极低,任意一台主节点网络延迟高、宕机,都会导致加锁失败,且失败后需要回滚其他节点数据,性能损耗大
Redis官方红锁(RedLock)
同样部署多台独立主节点,采用过半机制:超过半数节点加锁成功,就判定加锁生效。
依靠过半机制保证:同一时间只会有一个线程满足加锁条件,彻底解决主从锁丢失问题,兼顾安全性和一定的可用性。
缺点:多主节点部署运维成本高,集群数据一致性难以保证,生产环境使用较少。

734

被折叠的 条评论
为什么被折叠?



