面试被问懵系列:关于分布式锁,我们到底该掌握到什么程度?

不知道大家有没有这种感觉:简历上写着“熟悉Redis”、“了解高并发”,觉得自己准备得挺充分。可一旦面试官真的问起:“如果让你设计一个分布式锁,你会怎么做?

哪怕脑子里蹦出了 setnx、Lua脚本这些词儿,可一旦被追问细节——比如“看门狗机制具体是怎么跑的?”或者“主从切换导致锁丢失怎么解决?”,立马就开始支支吾吾,半天说不清楚。明明平时都在用,真到了嘴边却像卡壳了一样,那种尴尬感真的太真实了。

别慌,今天这篇文章不整虚的。咱们把那些高大上的名词拆开揉碎,从最基础的互斥性到复杂的红锁算法,带你一步步还原一个真正能抗住生产环境考验的分布式锁。

一、基础版锁:实现互斥性

核心需求:保证同一时间只有一个线程执行业务

分布式锁最根本的要求就是互斥性:同一个业务代码块,在多服务、多线程场景下,同一时间只能有一个线程执行。

Redis天然支持这个特性,核心命令就是 SetNX(SET if Not Exists):只有当key不存在时,才能创建成功,返回成功;key已存在则创建失败。

实现逻辑

  • 线程执行业务前,尝试通过SetNX 创建锁key

  • 创建成功:抢到锁,正常执行业务代码

  • 创建失败:没抢到锁,放弃执行或等待重试

线程宕机导致永久死锁

如果只写 SetNX 加锁,会出现致命问题:如果抢到锁的线程突然宕机、程序崩溃,没来得及手动删除锁key,这个锁就会永久存在Redis中,后续所有线程都永远抢不到锁,直接死锁,业务彻底瘫痪!

解决方案给锁设置过期时间

加锁的同时,必须给锁key设置合理的过期时间。哪怕线程意外宕机,锁到期后会自动释放,避免永久死锁问题。


二、解决锁误删问题

基础版锁+过期时间后,又会出现第二个经典问题:锁误删,删掉了其他线程的有效锁。

我们来模拟一个真实线上场景:

  1. 线程1成功抢到锁,锁过期时间设为10秒

  2. 线程1业务执行很慢,超过10秒没跑完,锁自动过期被Redis释放

  3. 线程2立马抢到锁,开始执行业务

  4. 此时线程1业务终于跑完了,执行解锁逻辑,直接删除锁key

  5. 结果:线程1删掉了线程2的锁,线程2的业务直接裸奔,并发安全彻底失效!

解决方案:校验锁归属 + Lua原子解锁

问题根源很简单:所有线程的锁key都一样,解锁时不判断归属,直接删key。

第一步优化:锁key的value存唯一标识

加锁时,value不要写固定值,存入UUID+线程ID的唯一字符串,代表这把锁的专属持有者。

第二步优化:解锁先校验,再删除

线程解锁前,先获取锁的value,判断是否等于自己的唯一标识,只有是自己的锁,才允许删除

这里又有一个新坑:判断、删除是两步操作,非原子!

再给大家模拟一个极端并发场景:

  1. 线程1执行完业务,获取锁value,判断是自己的锁

  2. 判断完成的瞬间,线程1阻塞、CPU切换线程

  3. 此时线程1的锁刚好过期自动释放,线程2成功抢到锁

  4. 线程1恢复执行,直接执行删除逻辑,误删线程2的锁

解决方案:Lua脚本解锁

将获取value、判断归属、删除key三段逻辑写在同一个Lua脚本中,Redis会一次性执行完整个脚本,中间不会被其他线程打断,完美保证解锁的原子性,彻底杜绝锁误删问题。


三、锁续期(看门狗机制)

核心痛点:业务没跑完,锁先过期了

上面的优化搞定了误删问题,但还有一个绕不开的问题:锁过期时间不好把控

如果我们把过期时间设太短,复杂业务还没执行完,锁就自动释放,出现并发问题;设太长,一旦线程宕机,锁需要很久才能释放,影响业务可用性。

举个例子:订单结算、批量数据处理业务,执行时间不确定,可能5秒跑完,也可能因为数据量很大执行20秒。固定的过期时间,完全无法适配。

解决方案:Redisson看门狗续期机制

主流框架Redisson给出的最优解就是看门狗线程,核心逻辑超简单:

线程成功加锁后,启动一个守护线程,定时(默认每10秒)检查当前锁是否还被持有,如果业务没执行完、锁还存在,就自动给锁续期(重置过期时间)。

为什么要用守护线程?

如果是普通线程,哪怕业务线程执行完毕、宕机消失,续期线程还会一直运行,不停给锁续期,导致锁永远无法释放,造成死锁。

守护线程会跟随业务线程生命周期:业务线程结束/宕机,看门狗守护线程自动消亡,不再续期,锁到期正常释放,完美闭环。


四、可重入锁,避免递归死锁

方法嵌套、递归调用死锁

实际开发中,我们经常遇到方法嵌套调用、递归调用的场景:同一个线程,已经拿到了分布式锁,后续又调用了需要同一把锁的方法。

如果锁不支持可重入,线程再次加锁时就会加锁失败,直接自己卡死自己,造成死锁

就像我们本地的 synchronizedReentrantLock 都是可重入锁,分布式锁也必须支持可重入!

解决:借鉴本地锁计数器机制

本地锁的可重入核心是计数器

  • synchronized:依靠对象监视器计数器

  • ReentrantLock:依靠AQS的state计数器

每重入一次,计数器+1;每释放一次锁,计数器-1;计数器归0时,才真正释放锁

Redisson可重入锁实现

Redisson用Redis的Hash结构完美实现可重入锁,结构设计超巧妙:

  • Hash的key:业务锁的唯一标识(比如lock:order)

  • Hash的field:UUID+线程ID(保证集群环境下线程标识唯一,避免不同机器线程ID重复)

  • Hash的value:锁重入计数器

执行逻辑

  1. 同一线程首次加锁:计数器置1

  2. 同一线程重复加锁:计数器+1

  3. 每次解锁:计数器-1

  4. 计数器减为0:删除整个Hash结构,彻底释放锁


五、阻塞锁

普通非阻塞锁的问题

我们前面写的基础锁,都是非阻塞锁:抢锁失败直接返回,不等待。

但真实业务中,大部分场景需要阻塞锁:抢锁失败后,不要直接放弃,等待锁释放后重新尝试抢锁,和 ReentrantLock 阻塞逻辑一致。

两种阻塞锁实现方案

自旋重试

抢锁失败的线程,不断循环重试抢锁,直到抢到锁或超时。

优点:实现简单、逻辑清晰

缺点:高并发场景下,大量线程空轮询,极度消耗CPU资源,性能很差

Redis发布订阅

这是生产环境最优方案,Redisson默认使用此机制:

  1. 线程抢锁失败后,订阅锁对应的消息频道,然后阻塞等待

  2. 持有锁的线程解锁时,发布「锁释放」消息到频道

  3. 所有阻塞等待的线程收到消息,被唤醒,重新竞争抢锁

  4. 同时配置最大等待超时时间,避免线程无限阻塞

完美解决自旋CPU浪费问题,兼顾性能和可用性。


六、Redis主从架构锁丢失问题

主从架构致命漏洞

我们平时用的Redis基本都是一主多从架构,保证高可用,但这会导致分布式锁互斥性彻底失效

场景复现:

  1. 主线程在主节点加锁成功,执行业务

  2. 此时锁数据还没同步到从节点,主节点突然宕机

  3. 集群自动将从节点升级为新主节点

  4. 新主节点没有锁数据,其他线程可以直接加锁成功

  5. 结果:多个线程同时执行业务,锁彻底失效!

两种解决方案:联锁 vs 红锁

Redisson 联锁

部署多台独立的Redis主节点,必须所有主节点全部加锁成功,才算真正抢到锁

优点:安全性极高,单节点宕机不丢锁

缺点:容错率极低,任意一台主节点网络延迟高、宕机,都会导致加锁失败,且失败后需要回滚其他节点数据,性能损耗大

Redis官方红锁(RedLock)

同样部署多台独立主节点,采用过半机制:超过半数节点加锁成功,就判定加锁生效。

依靠过半机制保证:同一时间只会有一个线程满足加锁条件,彻底解决主从锁丢失问题,兼顾安全性和一定的可用性。

缺点:多主节点部署运维成本高,集群数据一致性难以保证,生产环境使用较少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值