Redis分布式锁常见坑点分析

日常开发中,基于 redis 天然支持分布式锁,大家在线上分布式项目中都使用过 Redis 锁 。本文主要针对日常开发中加锁过程中某些异常场景进行讲解与分析 。本文讲解示例代码都在 https://Github.com/wayn111/newbee-mall-pro 项目 test 目录下 RedisLockTest 类中 。
版本声明:

  • Spring Boot 版本 3.0.2
  • 演示项目地址:https://github.com/wayn111/newbee-mall-pro
  • github地址:http://github.com/wayn111 欢迎大家关注,点个star
一、任务超时 , 锁已经过期这个异常场景说实话发生概率很低,大部分情况下加锁时任务执行都会很快,锁还没到期 , 任务自己就会删除锁 。除非说任务调用第三方接口不稳定导致超时、数据库查询突然变得非常慢就可能会产生这个异常场景 。
那怎么处理这个异常嘞?大部分人可能都会回答添加一个定时任务 , 在定时任务内检测锁快过期时 , 进行续期操作 。OK,这么做好像是可以解决这个异常,那么博主在这里给出自己的见解 。
1.1 先说一个暴论:如果料想到有这类异常产生,为什么不在加锁时,就把加锁过期时间设置大一点不管所续期还是增大加锁时长,都会导致一个问题,其他线程会迟迟获取不到锁,一直被阻塞 。那结果都一样 , 为什么不直接增大加锁时间?
?想法是好的,但是实际上,加锁时间的设置是我们主观臆断的 , 我们无法保证这个加锁代码的执行时间一定在我们的锁过期时间内 。作为一个严谨的程序员,我们需要对我们的代码有客观认知 , 任务执行可能几千上亿万次都是正常,但就是那么一次它执行超时了 , 可能由于外部依赖、当前运行环境的异常导致 。
?
1.2 直接不设置过期时间,任务不执行完 , 不释放锁如果在加锁时就不设置过期时间的话,理论上好像是可以解决这个问题 , 任务不执行完,锁就不会释放 。但是作为程序员 , 总觉得哪里怪怪的,任务不执行完,锁就不会释放!
?仔细想想,我们一般在 try 中进行加锁 在 finally 进行锁释放 , 这个好像也没毛病哦 。但是实际针对一些极端异常场景下,如果任务执行过程中,服务器宕机、程序突然被杀掉、网络断连等都可能造成这个锁释放不了,另一个任务就一直获取不到锁 。
?
这个方案程序正常的情况下 , 可以满足我们的要求,但是一旦发生异常将导致锁无法释放的后果,也就是说只要我们解决这个锁在异常场景下无法释放的问题 , 这个方案还是OK的 。博主这里直接给出方案:
在不设置过期时间的加锁操作成功时,给一个默认过期时间比如三十秒 , 同时启动一个定时任务,给我们的锁进行自动续期,每隔 默认过期时间 / 3 秒后执行一次续期操作 , 发生锁剩余时长小于 默认过期时间 / 2 就重新赋值过期时长为三十秒 。这样的话,可以保证锁必须由任务执行完才能释放,当程序异常发生时,仍然能保证锁会在三十秒内释放 。
1.3 设置过期时间 , 任务不执行完,不释放锁这个方案本质上与方案二的解决方案相同 , 还是启动定时任务进行续期操作,流程这里不做多余讲述 。需要注意的就是加锁指定过期时间会比较符合我们的客观认知 。实际上他的底层逻辑跟方案二相同,无非就是定时任务执行间隔,锁剩余时长续期判断要根据过期时间来计算 。
「综合来看:方案三会最合适,符合我们的客观认知,跟我们之前对 Redis 的使用逻辑较为相近 。」
二、线程B加锁执行中未释放锁,线程A释放了线程B的锁
?说实话我仔细思考了一下这个异常场景 , 发现这个异常是个伪命题,如果线程 B 正在执行时,线程 A 怎么能获取到线程B的锁!线程 A 获取不到线程 B 的锁,谈何来去释放线程 B 的锁!如果线程 A 能获取到线程 B 的锁那么这个分布式锁的代码一开始就已经错了 。
?
这里回到这个异常场景本身 , 我们可以给每个线程设置请求ID,加锁成功将请求ID设置为加锁 key 的对应 value,线程释放锁时需要判断当前线程的请求ID与 加锁 key 的对应 value 是否相同,相同则可以释放锁,不相同则不允许释放 。


推荐阅读