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的分布式锁
但是在微服务多个不同的进程之间这些标志位是不共享的,因此需要一个为分布式服务,存储共享锁标志。常见的分布式锁:redis分布式锁
,zookeeper分布式锁
,数据库的分布式锁
等。
基于分布式锁现在已经有很多开源的实现,我们可以直接引用就行,基于redis的redission,基于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 host
port: 6379 #default port
password: XXXXX #your password
timeout: 60000 #redis client timeOut
database: 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 {
@Autowired
private 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);
}
编写测试类进行测试:
//测试自定义锁
@Test
public void testSimpleLock1() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("线程:" + i + "开始执行,尝试获取锁,获取结果为:" + lockDemoSimple1.trySimpleLock1());
}
}
运行结果:
可以看到,redis中0号数据库中看到了存入的数据
接下来我们执行一下释放锁的测试方法,会发现redis0号数据库中数据被删除了
@Test
public void testSimpleLock1Release() throws Exception {
lockDemoSimple1.releaseSimpleLock1();
}
我们模拟一下正式的运行环境,testSimpleLock1A
和testSimpleLock1B
两个测试方法分别代表分布式系统中的两个程序,优先运行程序A,然后运行程序B。
//测试自定义锁v1
@Test
public 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();
}
}
@Test
public 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 {
@Autowired
private 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
@Test
public 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();
}
}
@Test
public 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 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String value;
private String lockKey = "lock:consumer";
/***
* 尝试加锁
*/
public boolean trySimpleLock1() {
// 尝试获取锁
String uuid = UUID.randomUUID().toString();
// 原子性操作setNX
if (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 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String value;
private String lockKey = "lock:consumer";
/***
* 尝试加锁
*/
public boolean trySimpleLock1() {
// 尝试获取锁
String uuid = UUID.randomUUID().toString();
// 原子性操作setNX
if (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() {
@Override
public 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 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String value;
private ThreadLocal<String> keyMap = new ThreadLocal<String>();//保存线程内部的局部变量
@Autowired
private 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 + "方法被调用");
// 原子性操作setNX
if (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()) {
// 原子性操作setNX
if (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() {
@Override
public 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 {
@Bean
public ConcurrentHashMap<Thread, String> map() {
return new ConcurrentHashMap<>();
}
/**
* 使用线程池优化新性能
*
* @return
*/
@Bean
public ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() {
return new ScheduledThreadPoolExecutor(10);
}
}
编写测试代码进行上述v5版本的测试
//模仿实际场景,测试自定义锁
@Test
public void testSimpleLock2() throws InterruptedException {
System.out.println("程序A:开始");
try {
if (lockDemoSimple5.trySimpleLock("Lock:key")) {
System.out.println("程序A: 获取锁成功,开始执行业务逻辑,睡50秒");
//模拟业务逻辑
Thread.sleep(50000);
} else
System.out.println("程序A:获取锁失败,无法执行业务逻辑");
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
System.out.println("程序A:释放锁");
lockDemoSimple5.releaseSimpleLock();
}
}
@Test
public 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
@Test
public 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仍然获取不到锁,然后直接返回了结果
至此呢,我们基本实现了简单的分布式锁。
对于分布式锁的特性,我们在百度一下。
分布式锁的特性:
多进程可见:多进程可见,否则就无法实现分布式效果
互斥(必须的):同一时刻,只能有一个进程获得锁,执行任务后释放锁
可重入(可选):同一个任务再次获取改锁不会被死锁
阻塞锁(可选):获取失败时,具备重试机制,尝试再次获取锁
性能好(可选):效率高,应对高并发场景
高可用:避免锁服务宕机或处理好宕机的补救措施
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 Template
private StringRedisTemplate redisTemplate;
//prefix
private String prefix;
public LockObtain(StringRedisTemplate redisTemplate, String prefix) {
this.prefix = prefix;
this.redisTemplate = redisTemplate;
}
@Override
public 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 {
@Bean
public 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(new
ClassPathResource("lock.lua")));
LOCK_SCRIPT.setResultType(Long.class);
// 加载释放锁的脚本
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
ClassPathResource("unlock.lua")));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public LockDemoSimple6(StringRedisTemplate redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockKeyValue = UUID.randomUUID().toString();
}
@Override
public boolean tryLock() {
// 执行脚本
Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
// 判断结果
return result != null && result.intValue() == 1;
}
@Override
public 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;
}
@Override
public 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;
}
}
}
@Override
public 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)
then
redis.call('hset', key, threadId, '1')
redis.call('expire', key, releaseTime)
return 1
end
if(redis.call('hexists', key, threadId) == 1)
then
redis.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) then
redis.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分布式锁,喜欢该系列的同学们记得一键三连哈🎉🎉🎉🎉🎉