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,可以满足日常并发不大的场景,设置合理的过期时间,就能够满足大部分的场景了。

相关推荐
期待のcode4 分钟前
springboot热部署
java·spring boot·后端
expect7g5 分钟前
Paimon源码解读 -- FULL_COMPACTION_DELTA_COMMITS
大数据·后端·flink
踏浪无痕16 分钟前
周末拆解:QLExpress 如何做到不编译就能执行?
后端·算法·架构
222you18 分钟前
Spring框架的介绍和IoC入门
java·后端·spring
用户61512656173327 分钟前
Java生态新纪元:虚拟线程、模式匹配与未来的编程范式
后端
风雨同舟的代码笔记31 分钟前
Java并发编程基石:深入解析AQS原理与应用实战
后端
曾富贵31 分钟前
【后端进阶】并发竞态与锁选型
后端
a程序小傲1 小时前
京东Java面试被问:ZGC的染色指针如何实现?内存屏障如何处理?
java·后端·python·面试
vx_bisheyuange1 小时前
基于SpringBoot的老年一站式服务平台
java·spring boot·后端·毕业设计
Tony Bai1 小时前
Jepsen 报告震动 Go 社区:NATS JetStream 会丢失已确认写入
开发语言·后端·golang