Redis分布式锁手动实现

server/2024/10/17 20:49:25/

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分布式数据库的分布式等。

基于分布式锁现在已经有很多开源的实现,我们可以直接引用就行,基于redisredission,基于zookeeper的 Curator框架,Spring框架也为此为我们提供了统一的分布式锁的定义接口。

基于上述框架的分布式锁机制,我们有机会再细聊。

接下来,我们一起来手动实现基于redis分布式

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();}

请添加图片描述

我们模拟一下正式的运行环境,testSimpleLock1AtestSimpleLock1B两个测试方法分别代表分布式系统中的两个程序,优先运行程序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由以下几部分构成:

  1. 时间戳:通常使用当前时间或时钟序列作为UUID的第一个组成部分,以确保每个UUID的唯一性。这个时间戳是自1582年10月15日午夜(即格林威治标准时间0点)以来的纳秒数。
  2. 时钟序列号:表示当前计数器的值,当时间戳发生变化时,时钟序列号会重新开始计数。
  3. 全局唯一标识:通常为一个计算机名、网络地址或MAC地址等固定值,用于标识生成UUID的计算机或网络环境。
  4. 变量节点号:一般是当前计算机的MAC地址或其他唯一标识符,用于增加UUID的随机性和唯一性。
  5. 版本号:表明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分布式锁,喜欢该系列的同学们记得一键三连哈🎉🎉🎉🎉🎉


http://www.ppmy.cn/server/20290.html

相关文章

基于ssm + 小程序的党建考试系统实现与设计(源码+数据库+文档)

基于ssm 小程序的“党建考试系统”实现与设计&#xff08;源码数据库文档) 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringmvcSpringMybatis、vue、小程序工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 管理系统-登录界面展示 管理系统…

【嵌入式AI开发】轻量级卷积神经网络MobileNetV2详解

前言:MobileNetV2网络先升维后降维,在降维时使用线性激活函数,带残差的Inverted bottleck模块,防止ReLU信息丢失。在图像分类、目标检测、语义分割等任务上实现了网络轻量化、速度和准确度的权衡。 回顾MobileNetV1的理论和MobileNetV2项目实战可查阅如下链接: 【嵌入式AI…

Docker在Windows与CentOS上的安装

这个季节有着无数的热烈&#xff0c;就像是飞鸟对天空的迫切。大家好&#xff0c;今天给大家分享一下关于Docker的安装&#xff0c;那么作为一名软件测试工程师&#xff0c;为什么需要了解Docker并且使用Docker呢&#xff1f;Docker会给我们带来怎样的好处呢&#xff1f; 原因…

esp32s3中使用双通道通信解决TCP粘包问题

在使用esp32 idf例程中的tcp_server和tcp_client通信测试时发现&#xff0c; 在tcp_server端&#xff0c;接收到一帧数据之后必须马上回复至少一个字节&#xff0c;才能保证每帧数据不粘包&#xff0c; 如果不回复操作&#xff0c;300ms以内的通信时延会导致tcp严重粘包&…

OceanBase 分布式数据库【信创/国产化】- OceanBase Demo 环境搭建

本心、输入输出、结果 文章目录 OceanBase 分布式数据库【信创/国产化】- OceanBase Demo 环境搭建前言OceanBase 数据更新架构部署背景信息组件介绍部署前提条件下载并安装 all-in-one 安装包单机部署 OceanBase 数据库执行输出中的连接命令连接数据库配置 OceanBase 密码Ocea…

Linux - tar (tape archive)

tar 的全称是 Tape Archive。它最初是在 Unix 系统中用于将数据写入磁带的工具&#xff0c;但现在它通常用于创建、维护、修改和提取文件的归档文件。尽管 tar 可以用于压缩和解压缩文件&#xff0c;但它本身并不进行压缩&#xff0c;而是通常与 gzip 或 bzip2 等压缩工具一起使…

腾讯云邮件推送如何设置?群发邮件的技巧?

腾讯云邮件推送功能有哪些&#xff1f;怎么有效使用邮件推送&#xff1f; 腾讯云邮件推送以其稳定、高效的特点&#xff0c;受到了众多企业的青睐。那么&#xff0c;腾讯云邮件推送如何设置呢&#xff1f;又有哪些群发邮件的技巧呢&#xff1f;下面AokSend就来详细探讨一下。 …

grpc笔记

教程地址 【狂神说】gRPC最新超详细版教程通俗易懂 | Go语言全栈教程_哔哩哔哩_bilibili rpc 定义&#xff1a;Remote Procedure Call——远程过程调用&#xff0c;通俗的含义是&#xff1a;远程定义好方法名、参数和返回值&#xff0c;RPC可以像调用本地方法那样调用远端方…