Redis分布式锁手动实现
java中锁机制
在 Java 中,锁是用来同步并发访问共享资源的机制。它确保了在一个时间点,只有一个线程可以执行某个代码块或方法,从而防止了数据的不一致和竞态条件。Java 提供了多种锁机制,包括内置锁(synchronized 关键字)和显式锁(如 ReentrantLock
)。
1. 内置锁(synchronized)
Java 的每个对象都有一个内置锁。当一个线程进入一个对象的 synchronized
方法或代码块时,它会自动获得该对象的锁,并在退出该方法或代码块时释放锁。其他尝试进入该对象的 synchronized
方法或代码块的线程将被阻塞,直到锁被释放。
使用示例:
public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; }
}
2. 显式锁(ReentrantLock)
ReentrantLock
是一个更灵活的锁机制,它提供了比内置锁更多的功能,如可中断的获取锁、尝试获取锁、定时获取锁等。与内置锁不同,ReentrantLock
必须显式地获取和释放。
示例代码:
import java.util.concurrent.locks.ReentrantLock; public class Counter { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } }
}
3. 读写锁(ReadWriteLock)
ReadWriteLock
是一种特殊的锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这可以提高并发性能,因为读取操作通常不会修改数据,因此可以安全地并发执行。Java 的 java.util.concurrent.locks
包中提供了 ReadWriteLock
接口及其实现类 ReentrantReadWriteLock
。
redis_67">基于redis的分布式锁
但是在微服务多个不同的进程之间这些标志位是不共享的,因此需要一个为分布式服务,存储共享锁标志。常见的分布式锁:redis分布式锁
,zookeeper分布式锁
,数据库的分布式锁
等。
基于分布式锁现在已经有很多开源的实现,我们可以直接引用就行,基于redis的redission,基于zookeeper的 Curator框架,Spring框架也为此为我们提供了统一的分布式锁的定义接口。
基于上述框架的分布式锁机制,我们有机会再细聊。
1.创建一个Spring-boot项目
创建Spring-boot 项目,在pom.xml中导入以下依赖
<!-- redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
<!--junit--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency>
<!--spring-boot-test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
在application.yml中配置你的redis链接
spring:redis:host: XXXX #your hostport: 6379 #default portpassword: XXXXX #your passwordtimeout: 60000 #redis client timeOutdatabase: 0 #default database 0
2.实现
2.1简单实现v1(×)
我们简单想一下锁的基本实现,锁的目的就是,对于公共资源,程序A持有资源,程序B在访问该资源并获取时,会获取不到。
基于redis存储的<key,value>形式的数据,我们的设计:
所有消费者程序可以持有公共的key,在程序A访问时,我们可以在redis中存储一条数据,当程序B 进行访问时,在redis中判断key,如果存在表示已经有人持有锁了,没有则我们放入这个key去获取锁,执行完业务逻辑将这个key删除。
原理大概就是这样,我们一起将其付诸于实践
对于
key
,我们可以自定义,在此我们使用key: lock:consumer对于
value
,也可以自定义,在此我们使用value:“1”
LockDemoSimple1
示例代码如下:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/16 16:29* @注释 最简易的分布式锁实现 版本1*/
@Component
public class LockDemoSimple1 {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/**** 尝试加锁*/public boolean trySimpleLock1() {String lockKey = "lock:consumer";// 尝试获取锁String value = redisTemplate.opsForValue().get(lockKey);if (value == null) {// 加锁redisTemplate.opsForValue().set(lockKey, "1");return true;}return false;}/***** 释放锁*/public void releaseSimpleLock1() {String lockKey = "lock:consumer";redisTemplate.delete(lockKey);}
编写测试类进行测试:
//测试自定义锁@Testpublic void testSimpleLock1() throws Exception {for (int i = 0; i < 5; i++) {System.out.println("线程:" + i + "开始执行,尝试获取锁,获取结果为:" + lockDemoSimple1.trySimpleLock1());}}
运行结果:
可以看到,redis中0号数据库中看到了存入的数据
接下来我们执行一下释放锁的测试方法,会发现redis0号数据库中数据被删除了
@Testpublic void testSimpleLock1Release() throws Exception {lockDemoSimple1.releaseSimpleLock1();}
我们模拟一下正式的运行环境,testSimpleLock1A
和testSimpleLock1B
两个测试方法分别代表分布式系统中的两个程序,优先运行程序A,然后运行程序B。
//测试自定义锁v1@Testpublic void testSimpleLock1A() throws Exception {try {if (lockDemoSimple1.trySimpleLock1()) {System.out.println("程序A:执行业务逻辑,睡100秒钟");Thread.sleep(100000);} else {System.out.println("程序A:获取锁失败");}} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println("程序A:释放锁");lockDemoSimple1.releaseSimpleLock1();}}@Testpublic void testSimpleLockB() throws Exception {try {if (lockDemoSimple1.trySimpleLock1()) {System.out.println("程序B:执行业务逻辑,睡100秒钟");Thread.sleep(100000);} else {System.out.println("程序B:获取锁失败");}} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println("程序B:释放锁");lockDemoSimple1.releaseSimpleLock1();}}
运行后截图:
观察上面的两个图,你会发现他们使用同一个
Key
在没获取到锁的时候也会去释放锁,删除key
,这样会使testSimpleLock1A
在执行业务逻辑期间,它的锁被testSimpleLock1B
获取失败后,释放掉了;如果后续还会有testSimpleLock1C
程序启动,C程序又能去获取资源了,很明显这里设计是存在问题的。因此,我们又想到了另一个方案:
我们需要一个标识来标记这个锁属于某个程序,如果不是它的,执行释放锁操作就不能进行操作。
2.2简单实现v2[预防非法释放](×)
那么怎么去创建标识呢,我这里想到了UUID ,在合理的概率下,全球范围内每个生成的 UUID 都是唯一的
简单介绍一下UUID:
UUID(Universally Unique Identifier,通用唯一识别码)是一种由标准算法生成的128位数字,用于唯一标识信息元素。UUID由以下几部分构成:
- 时间戳:通常使用当前时间或时钟序列作为UUID的第一个组成部分,以确保每个UUID的唯一性。这个时间戳是自1582年10月15日午夜(即格林威治标准时间0点)以来的纳秒数。
- 时钟序列号:表示当前计数器的值,当时间戳发生变化时,时钟序列号会重新开始计数。
- 全局唯一标识:通常为一个计算机名、网络地址或MAC地址等固定值,用于标识生成UUID的计算机或网络环境。
- 变量节点号:一般是当前计算机的MAC地址或其他唯一标识符,用于增加UUID的随机性和唯一性。
- 版本号:表明UUID的版本,是一个随机值。目前有四个版本的UUID生成算法。
UUID的长度为16字节,可以表示2^128个唯一的值,因此生成重复的UUID在理论上具有极低的概率。这使得UUID在需要唯一标识符的场景中非常有用。
那么我们一起实现一下第二版程序,代码如下:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/16 16:29* @注释 最简易的分布式锁实现 版本2 预防非法释放锁*/
@Component
public class LockDemoSimple2 {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private String value;private String lockKey = "lock:consumer";/**** 尝试加锁*/public boolean trySimpleLock1() {// 尝试获取锁String value = redisTemplate.opsForValue().get(lockKey);if (value == null) {// 加锁UUID uuid = UUID.randomUUID();this.value = uuid.toString();redisTemplate.opsForValue().set(lockKey, this.value);return true;}return false;}/***** 释放锁*/public void releaseSimpleLock1() {if (value != null && value.equals(redisTemplate.opsForValue().get(lockKey))) {System.out.println("释放自己的锁");redisTemplate.delete(lockKey);} else {System.out.println("不是我自己的锁,我不释放");}}}
依旧创建测试方法进行测试(代码没差,除了修改注入的🔒):
//测试自定义锁v2@Testpublic void testSimpleLock1A() throws Exception {try {if (lockDemoSimple2.trySimpleLock1()) {System.out.println("程序A:执行业务逻辑,睡100秒钟");Thread.sleep(100000);} else {System.out.println("程序A:获取锁失败");}} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println("程序A:释放锁");lockDemoSimple2.releaseSimpleLock1();}}@Testpublic void testSimpleLockB() throws Exception {try {if (lockDemoSimple2.trySimpleLock1()) {System.out.println("程序B:执行业务逻辑,睡100秒钟");Thread.sleep(100000);} else {System.out.println("程序B:获取锁失败");}} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println("程序B:释放锁");lockDemoSimple2.releaseSimpleLock1();}}
在程序A在持有资源,进行业务逻辑处理时,程序B获取不到锁,同时redis 0号数据库可以看到数据。
在程序A在持有资源,处理完业务逻辑处理,并释放自己的锁时,redis 0号数据库可以看到数据消失,此时重新启动程序B,B也能获取锁,进行业务逻辑处理。
此时我们解决了非法释放锁的问题,那么我们再看看加锁的这段逻辑,看看是否仍然存在一些问题。
/**** 尝试加锁*/public boolean trySimpleLock1() {// 尝试获取锁String value = redisTemplate.opsForValue().get(lockKey);if (value == null) {// 加锁UUID uuid = UUID.randomUUID();this.value = uuid.toString();redisTemplate.opsForValue().set(lockKey, this.value);return true;}return false;}
虽然redis是单线程的,但是如果两个程序同时读到key为lock:consumer的没有设置值的情况,可能会出现以下覆盖值的情况
因此我们需要将查看redis的值是否存在
和设置值
弄成一个不可分割的操作,类似于事务,而redis也为我们提供了这个命令setnx key value
,只有在不存在的时候才会去设置值,存在就不设置值了。
2.3简单实现v3[保证原子性](×)
将判断是否存在和设置值的操作合并在一起,保证操作的原子性
*** @version 1.0* @Author jerryLau* @Date 2024/4/16 16:29* @注释 最简易的分布式锁实现 版本3 保证原子性*/
@Component
public class LockDemoSimple3 {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private String value;private String lockKey = "lock:consumer";/**** 尝试加锁*/public boolean trySimpleLock1() {// 尝试获取锁String uuid = UUID.randomUUID().toString();// 原子性操作setNXif (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid)) {// 加锁this.value = uuid;return true;}return false;}/***** 释放锁*/public void releaseSimpleLock1() {if (value != null && value.equals(redisTemplate.opsForValue().get(lockKey))) {System.out.println("释放自己的锁");redisTemplate.delete(lockKey);} else {System.out.println("不是我自己的锁,我不释放");}}
}
这回看似肯定没问题了,但是分布式服务有个最大的特点就是防止单点灾难
。
如果你在加锁期间你的服务挂了咋办,你的key一直不会被释放,这样对于公共资源,大家一块都不能使用了;这在开发中肯定不行,redis也有设置键的过期命令
set key value ex number nx
其中number
就是时间,nx
表示不存在才会执行。
但是这儿过期时间怎么去把握,如果设置的时间过长,可能造成资源的浪费,如果设置的时间过短,可能会在程序执行过程中,释放锁。那么这个问题应该如何解决呢?
没准定时任务其周期刷新是个好的方法。如果我们设置一个定时任务去周期性的帮我们续费key的时间。如果这个线程一直在,就一直续费,这个想法感觉还可以。
2.4简单实现v4[ttl时间续费](×)
大体思路如下:
在获取锁成功,启动一个定时任务去周期设置key的失效时间,当然在key不存在或者此线程已经被销毁(也就是执行完业务之后),应该停止此定时任务。
创建一个间隔10s的定时任务,进行线程存活检测,参考代码如下:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/16 16:29* @注释 最简易的分布式锁实现 版本4 定时器续费*/
@Component
public class LockDemoSimple4 {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private String value;private String lockKey = "lock:consumer";/**** 尝试加锁*/public boolean trySimpleLock1() {// 尝试获取锁String uuid = UUID.randomUUID().toString();// 原子性操作setNXif (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid)) {// 加锁this.value = uuid;renewKey(Thread.currentThread(), lockKey);return true;}return false;}/***** 释放锁*/public void releaseSimpleLock1() {if (value != null && value.equals(redisTemplate.opsForValue().get(lockKey))) {System.out.println("释放自己的锁");redisTemplate.delete(lockKey);} else {System.out.println("不是我自己的锁,我不释放");}}/*** 定时续费* @param thread 线程* @param key*/public void renewKey(Thread thread, String key) {ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);scheduledExecutorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {if (thread.isAlive() && redisTemplate.hasKey(key)) {System.out.println("线程还在,给key续30秒");redisTemplate.expire(key, 30, TimeUnit.SECONDS);} else {System.out.println("线程已经不存在,终止定时任务");throw new RuntimeException("终止定时任务");}}}, 10, 10, TimeUnit.SECONDS);}
}
编写测试类对上述代码进行测试,对于程序A,设置休眠时间为50s,那么在休眠期间会触发redis锁key的续费操作。
观察redis中key的存活时间,发现会被续费
如果程序A异常终止,根据redis中设置的key的过期时间,依然获释放🔒资源,程序A运行时手动停止来模拟程序A异常终止
至此,基于redis手动实现分布式锁基本实现,现在可以再将代码进行封装一下。
2.5简单实现v5[代码封装,优化接口](!)
改动代码让其更符合使用的逻辑,比如说key让用户传进来,让用户自己设置过期时间,阻塞获取锁,或者定时一段时间内去获取锁。
示例代码:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/16 16:29* @注释 最简易的分布式锁实现 版本5 继续分装简化逻辑*/
@Component
public class LockDemoSimple5 {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private String value;private ThreadLocal<String> keyMap = new ThreadLocal<String>();//保存线程内部的局部变量@Autowiredprivate ScheduledExecutorService scheduledExecutorService;/**** 尝试加锁* @param key* @return*/public boolean trySimpleLock(String key) {keyMap.set(key);// 尝试获取锁String uuid = UUID.randomUUID().toString();this.value = uuid;System.out.println(Thread.currentThread().getName() + "获取锁 " + key + " " + uuid + "方法被调用");// 原子性操作setNXif (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {// 加锁renewKey(Thread.currentThread(), key);return true;}return false;}/**** 给定时间内尝试加锁* @param key* @param timeout 超时时间* @return*/public boolean trySimpleLock(String key, int timeout) {keyMap.set(key);// 尝试获取锁String uuid = UUID.randomUUID().toString();//计算结束时间Instant endTime = Instant.now().plusSeconds(timeout);//时间比较while (Instant.now().getEpochSecond() < endTime.getEpochSecond()) {// 原子性操作setNXif (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {// 加锁this.value = uuid;renewKey(Thread.currentThread(), key);return true;}}return false;}/**** 尝试加锁(阻塞)* @param key* @param timeout* @return*/public void Lock(String key, int timeout) {keyMap.set(key);// 尝试获取锁String uuid = UUID.randomUUID().toString();while (true) {if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {// 加锁this.value = uuid;renewKey(Thread.currentThread(), key);break;}}}/***** 释放锁*/public void releaseSimpleLock() {System.out.println(Thread.currentThread().getName() + "释放锁方法被调用");String key = keyMap.get();System.out.println(Thread.currentThread().getName() + "释放锁 " + " VALUE保存的: " + this.value);System.out.println(Thread.currentThread().getName() + "释放锁 " + "value从redis获取的: " + redisTemplate.opsForValue().get(key));if (value != null && value.equals(redisTemplate.opsForValue().get(key))) {System.out.println( Thread.currentThread().getName() + "释放自己的锁");redisTemplate.delete(key);keyMap.remove();} else {System.out.println("不是我自己的锁,我不释放");}}/*** 定时续费* @param thread* @param key*/public void renewKey(Thread thread, String key) {scheduledExecutorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {if (thread.isAlive() && redisTemplate.hasKey(key)) {System.out.println("线程还在,给key续30秒");redisTemplate.expire(key, 30, TimeUnit.SECONDS);} else {System.out.println("线程已经不存在,终止定时任务");throw new RuntimeException("终止定时任务");}}}, 10, 10, TimeUnit.SECONDS);}
}
抽取后的配置文件:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/19 14:16* @注释*/
@Configuration
public class LockDemoSimple5Conf {@Beanpublic ConcurrentHashMap<Thread, String> map() {return new ConcurrentHashMap<>();}/*** 使用线程池优化新性能** @return*/@Beanpublic ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() {return new ScheduledThreadPoolExecutor(10);}
}
编写测试代码进行上述v5版本的测试
//模仿实际场景,测试自定义锁@Testpublic void testSimpleLock2() throws InterruptedException {System.out.println("程序A:开始");try {if (lockDemoSimple5.trySimpleLock("Lock:key")) {System.out.println("程序A: 获取锁成功,开始执行业务逻辑,睡50秒");//模拟业务逻辑Thread.sleep(50000);} elseSystem.out.println("程序A:获取锁失败,无法执行业务逻辑");} catch (Exception e) {e.printStackTrace();} finally {//释放锁System.out.println("程序A:释放锁");lockDemoSimple5.releaseSimpleLock();}}@Testpublic void testSimpleLock3() throws Exception {try {System.out.println("程序B:开始获取锁");lockDemoSimple5.Lock("Lock:key", 30);System.out.println("程序B:获取锁成功,开始执行业务逻辑,睡30秒");//模拟业务逻辑Thread.sleep(30000);} catch (Exception e) {e.printStackTrace();} finally {//释放锁System.out.println("程序B:释放锁");lockDemoSimple5.releaseSimpleLock();}}
程序A运行
程序 B先是阻塞,等到程序A执行结束释放后,程序B进行执行
再将程序B的测试方法修改一下,设置成获取不到锁直接返回,在程序A执行的过程中启动程序B
@Testpublic void testSimpleLock3() throws Exception {try {System.out.println("程序B:开始获取锁");boolean b = lockDemoSimple5.trySimpleLock("Lock:key", 30);if (b) {System.out.println("程序B:获取锁成功,开始执行业务逻辑,睡30秒");//模拟业务逻辑Thread.sleep(30000);} else {System.out.println("程序B获取锁失败,无法执行业务");}} catch (Exception e) {e.printStackTrace();} finally {//释放锁System.out.println("程序B:释放锁");lockDemoSimple5.releaseSimpleLock();}}
可以看到经过30s后程序B仍然获取不到锁,然后直接返回了结果
至此呢,我们基本实现了简单的分布式锁。
对于分布式锁的特性,我们在百度一下。
分布式锁的特性:
多进程可见:多进程可见,否则就无法实现分布式效果
互斥(必须的):同一时刻,只能有一个进程获得锁,执行任务后释放锁
可重入(可选):同一个任务再次获取改锁不会被死锁
阻塞锁(可选):获取失败时,具备重试机制,尝试再次获取锁
性能好(可选):效率高,应对高并发场景
高可用:避免锁服务宕机或处理好宕机的补救措施
redistempletelua_852">2.6简单实现v6[提供可重入,接口优化,通过redistemplete执行lua脚本](!)
可以使用redis基本数据类型hset哈希结构来存储锁的持有者信息。每个锁的持有者(可能是一个线程或者一个客户端)在哈希中以一个字段的形式存在,字段名为持有者的ID(threadId
),字段值为持有的锁数量(这里可能是一个计数器)。当锁被释放时,持有者的计数将减少。
那么加锁和解锁的逻辑如下:
获取锁的步骤:
1.先判断key是否存在
2.如果存在,判断是否是自己的锁,使用唯一的uuid表示,如果是,给count +1,如果不是表示锁已经被别人占有,加锁失败
3.如果不存在,表示锁还没有被持有,则添加hash,key为分布式锁的标识,field为uuid,唯一的锁身份标识,标识是谁的锁,value设置为1表示进入了一次
释放锁的步骤:
1.先判断key是否存在
2.如果存在,则判断是不是自己的锁,通过唯一的身份标识uuid,如果是,count进行-1操作,-1之后如果值为0,则删除这个hash。如果不是自己的锁,则不做任何操作
3.如果不存在,不做任何操作
这个时候值得注意的一点是:大家如果都读取到那个能获取锁的时间,同时加锁咋整?虽然redis是单线程的,但是如果两个人读取Key是否存在刚好同时操作,就会出问题,为此我们需要将获取锁和释放锁以数据库的事务一样要么全部完成,要么都失败,但是很不幸redis的事务并不是数据库的事务,不过也相应的提供了lua脚本功能,你可以在脚本中,将执行的redis命令一次性执行完,对于redis而言他就是一条命令,能够保证原子性。
需要专门去学这东西吗,我个人感觉用处不大,用的时候直接复制过来就行,而且看起来也不是很难懂。
接下来我们对代码在进行封装:
初始化🔒的接口:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/25 13:40* @注释 创建锁*/
public interface LockObtainInterface {/**** 创建🔒* @return*/public LockInterface obtainLock(String key);
}
初始化🔒的实现类:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/16 16:29* @注释 最简易的分布式锁实现 获取锁*/
public class LockObtain implements LockObtainInterface {//redis Templateprivate StringRedisTemplate redisTemplate;//prefixprivate String prefix;public LockObtain(StringRedisTemplate redisTemplate, String prefix) {this.prefix = prefix;this.redisTemplate = redisTemplate;}@Overridepublic LockInterface obtainLock(String key) {return new LockDemoSimple6(redisTemplate, prefix + ":" + key);}
}
🔒操作接口:
/*** @version 1.0* @Author jerryLau* @Date 2024/4/25 13:31* @注释 自定义锁接口 加锁等操作*/
public interface LockInterface {/*** 尝试获取🔒资源,如果获取不到,就阻塞*/public void lock();/*** 尝试获取🔒资源** @return 获取到返回true, 如获取不到返回false*/public boolean tryLock();/*** 尝试在指定时间内获取🔒资源** @param time* @return获取指定时间内没有获取到返回true,如获取不到返回false*/public boolean tryLock(long time);/*** 释放🔒*/public int unlock();}
🔒的配置类
/*** @version 1.0* @Author jerryLau* @Date 2024/4/25 13:35* @注释 适用于案例6的自定义配置类*/
@Configuration
public class LockDemoSimple6Conf {@Beanpublic LockObtainInterface lockRegistry(StringRedisTemplate redisTemplate) {return new LockObtain(redisTemplate, "lock");}}
🔒操作的实现类:
/**** 🔒操作的实现类*/
public class LockDemoSimple6 implements LockInterface {private StringRedisTemplate redisTemplate;private String lockKey;private String lockKeyValue;private long DEFAULT_RELEASE_TIME = 30;private static final DefaultRedisScript<Long> LOCK_SCRIPT;private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;private ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);static {// 加载释放锁的脚本LOCK_SCRIPT = new DefaultRedisScript<>();LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(newClassPathResource("lock.lua")));LOCK_SCRIPT.setResultType(Long.class);// 加载释放锁的脚本UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(newClassPathResource("unlock.lua")));UNLOCK_SCRIPT.setResultType(Long.class);}public LockDemoSimple6(StringRedisTemplate redisTemplate, String lockKey) {this.redisTemplate = redisTemplate;this.lockKey = lockKey;this.lockKeyValue = UUID.randomUUID().toString();}@Overridepublic boolean tryLock() {// 执行脚本Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));// 判断结果return result != null && result.intValue() == 1;}@Overridepublic boolean tryLock(long time) {Instant endTime = Instant.now().plusMillis(time);while(Instant.now().getEpochSecond() < endTime.getEpochSecond()) {Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));if (result != null && result.intValue() == 1) {renewKey(Thread.currentThread());return true;}}return false;}@Overridepublic void lock() {while (true) {Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));if (result != null && result.intValue() == 1) {renewKey(Thread.currentThread());break;}}}@Overridepublic int unlock() {// 执行脚本Long execute = redisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(lockKey),lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));System.out.println("execute:"+execute);return execute.intValue();}/*** 定时续费* @param thread*/public void renewKey(Thread thread) {scheduledExecutorService.scheduleAtFixedRate(() -> {if (thread.isAlive() && redisTemplate.hasKey(lockKey)) {redisTemplate.expire(lockKey, DEFAULT_RELEASE_TIME, TimeUnit.SECONDS);} else {throw new RuntimeException("终止定时任务");}}, 10, 10, TimeUnit.SECONDS);}
}
在其中用到了加锁以及解锁的lua脚本
加锁脚本:
local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = ARGV[2]if(redis.call('exists', key) == 0)
thenredis.call('hset', key, threadId, '1')redis.call('expire', key, releaseTime)return 1
endif(redis.call('hexists', key, threadId) == 1)
thenredis.call('hincrby', key, threadId, '1')redis.call('expire', key, releaseTime)return 1
end
return 0
解锁脚本:
local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = tonumber(ARGV[2])-- 检查锁的持有者身份
if (redis.call('HEXISTS', key, threadId) == 0) then-- 释放失败,因为调用者不是锁的持有者return 0
end-- 减少锁的持有者计数
local count = redis.call('HINCRBY', key, threadId, -1)-- 如果计数大于0,重新设置过期时间
if (count > 0) thenredis.call('EXPIRE', key, releaseTime)-- 释放成功,但锁仍然被其他持有者持有return 1
else-- 删除整个哈希键,因为没有任何持有者了redis.call('DEL', key)-- 释放成功,且锁已经完全释放return 1
end
构建简单的测试类继续此时,先测试阻塞获取锁,启动程序A,程序B,B先阻塞一直等到A执行完成后在进行获取执行:
运行截图就不贴了
在测试指定时间内获取锁
至此,关于手动实现redis的分布式锁基本完成,哈哈哈,还算是比较顺利的
相关代码请查看代码仓库:jerryLau-hua/spring-boot-redis
后面有时间,可以在研究使用redission 等第三方框架 实现redis分布式锁,喜欢该系列的同学们记得一键三连哈🎉🎉🎉🎉🎉