Redis分布式锁不止setnx那么简单

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

在分布式的场景下,分布式锁是经常遇到的。而Redis的分布式锁用的较为频繁。但是Redis的分布式锁是怎么实现的呢?

在我们公司的业务代码中经常会出现这样的代码:

或者

这样的代码用来当分布式锁或者处理幂等性等问题。这样有没有什么问题呢?

我想应该不止我们公司有人写成那样的代码吧!因为我们公司的并发不大,反正遇到的问题并不多。我们一起来探索一下吧!

02 分布式锁

分布式锁大家已经耳熟能详了,其核心特性无非就那么几个:

  • 互斥性:同一时刻只有一个客户端能持有锁
  • 安全性:锁只能由持有者释放,防止误删
  • 容错性:在部分节点故障时仍能正常工作
  • 避免死锁:必须有超时机制,防止客户端崩溃导致锁无法释放

我们根据以上特性分析一下上面代码中setnx+expireset可能会出现什么样的问题。

2.1 setnx + expire

我们还原一下伪代码:

java 复制代码
let redisService = xxxxx;
try{
    if (1 == redisService.setnx(key, value)){
        redisService.expire(key, expire);
        // 后续业务....
    }
    
}finally {
    redisService.delete(key);          
}

setnx固然可以是保持互斥性,但是和expire方法是两个独立的方法。所以如果expire方法异常,就会导致setnx设为永久有效。虽然有redisService.delete兜底,但是如果出现服务器宕机,此处的代码几无法执行,而从造成死锁。

如果执行业务逻辑的时候由于网络抖动,分布式锁已经过期,业务还在阻塞中。这时第二个请求相同的Key就可以加锁了,业务逻辑还没有执行完,结果第一个方法的恢复正常执行了delete(key)的操作,就会误删掉第二个请求的key,也就是释放了第二个请求的锁。

还有就是由于业务复杂导致分布式锁过期,业务还没有执行完,就会自动释放锁。导致加锁失败。

简单总结问题:

  • setnxexpire没有原子性操作
  • 释放锁没有校验是不是自己的锁,可能会误删锁。
  • 锁没有续约

2.2 set

set的方法是setnx + expire的结合,属于原子性操作。我们来看看伪代码:

java 复制代码
let redisService = xxxxx;
try{
    if ("OK" == redisService.setnx(key, value, "NX", "EX", 600)){
        // 后续业务....
    }
    
}finally {
    redisService.delete(key);          
}

这个方法比上面的好一点,保证了原子性。但是其他两个问题依然存在:

  • 释放锁没有校验是不是自己的锁,可能会误删锁。
  • 锁没有续约

2.3 优化

误删锁的问题容易解决,只需要在删除的时候,判断存入的Value是自己的Value即可。

java 复制代码
String value = "xxxx";
if (value.equals(redisService.get(key)) {
    redisService.delete(key); 
}

锁的续约如何解决呢?我们耳熟能详的看门狗模式。简单来说就是启用一个线程,隔一段时间就去看当前客户端的Key有没有过期,快过期的时候,重新设置时间。

伪代码如下:

java 复制代码
new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (redisService.expire(key) <= expire) {
                    // 续约
					redisService.expire(key, expire2);
                }
            }
        }, 10);

思路也是比较简单。但是难点就在续约的时间,检查锁是否过期的间隔。我们将通过几个开源案例了解一下具体的实现。

03 Lock4j

3.1 简介

Lock4j是一个分布式锁组件,其提供了多种不同的支持以满足不同性能和环境的需求,立志打造一个简单但富有内涵的分布式锁组件。其特性就是简单易用,功能强大,扩展性强,支持redission,redisTemplate,zookeeper。可混用,支持扩展。

该开源项目来自苞米豆社区,你可能没听过。但是Mybatis-Plus你一定听过,它的作者和Lock4j的作者是同一人。

3.2 RedisTemplate

StringRedisTemplateRedisTemplateSpringBoot自带的客户端。我们看看Lock4J是如何实现分布式锁的。

实现类:com.baomidou.lock.executor.RedisTemplateLockExecutor

从源码可以看出,对于分布式锁的加锁、释放锁以及续约,为了保证原子性,都是用了Lua脚本。

加锁

通过ForkJoin线程池启动线程,加锁后根据参数执行是否续约。然后等待加锁的结果,并返回加锁的结果。

释放锁

释放锁就比较简单了。根据Lua脚本,先查询对比,然后根据结果删除对应的key

续约

同样是先获取判断,然后在续约。都是通过Lua脚本处理的。但是要这里要说的是,通过new Timer()启动了一个调度任务,每到过期时间的三分之一,就会触发续约逻辑。

问题

我们会发现,在续约的时候启动的线程资源并没有被回收。如果加锁的线程很多,就会有很多任务在空跑。消耗了资源。小编在Gitee也返现了有人提出这个问题,并尝试优化:

gitee.com/baomidou/lo...

3.3 Redisson

Redisson同样是一款经典的Redis客户端。该客户端自己实现分布式锁,并启用了看门狗模式。

加锁

直接通过RedissonClient客户端获取锁并尝试加锁,返回加锁结果。

释放锁

判断线程是否持有,有的话就释放锁。

续约

几乎没有代码。因为Redisson本身自带看门狗机制。我们追一下Redisson的源码。当执行lock()->tryAcquire()->tryAcquireAsync()->scheduleExpirationRenewal()->renewExpiration()

①我们可以看到依然通过TimerTask这样的调度任务。

②不续约的话就释放资源:

zookeeper的分布式锁我们暂且讨论,因为不是基于Redis的。

04 小结

分布式锁不是想象的那么简单,但也不是那么复杂。使用Redis分布式锁,直接Redssion客户端,使用完善的功能。使用RedisSet或者setnx,可以满足日常并发不大的场景,设置合理的过期时间,就能够满足大部分的场景了。

相关推荐
bcbnb4 小时前
如何解析iOS崩溃日志:从获取到符号化分析
后端
许泽宇的技术分享4 小时前
当AI学会“说人话“:Azure语音合成技术的魔法世界
后端·python·flask
用户69371750013844 小时前
4.Kotlin 流程控制:强大的 when 表达式:取代 Switch
android·后端·kotlin
用户69371750013844 小时前
5.Kotlin 流程控制:循环的艺术:for 循环与区间 (Range)
android·后端·kotlin
vx_bisheyuange4 小时前
基于SpringBoot的宠物商城网站的设计与实现
spring boot·后端·宠物
bcbnb4 小时前
全面解析网络抓包工具使用:Wireshark和TCPDUMP教程
后端
leonardee5 小时前
Spring Security安全框架原理与实战
java·后端
回家路上绕了弯5 小时前
包冲突排查指南:从发现到解决的全流程实战
分布式·后端
爱分享的鱼鱼5 小时前
部署Vue+Java Web应用到云服务器完整指南
前端·后端·全栈
麦麦麦造5 小时前
比 pip 快 100 倍!更现代的 python 包管理工具,替代 pip、venv、poetry!
后端·python