Redisson就是一个使用Redis解决分布式问题的方案的集合,是一个十分成熟的Redis框架,功能也很多,比如:分布式锁和同步器、分布式对象、分布式集合、分布式服务。。。
什么是可重入锁?为什么要用可重入锁?
可重入锁是一种线程同步机制,它允许一个线程在释放锁之前可以再次获取同一锁。这种特性在多线程编程中非常有用,尤其是在需要递归调用的场景下。
可重入锁的特点:
-
重入性:当一个线程持有锁并再次尝试获取该锁时,它不会被阻塞。这与普通的互斥锁不同,后者在同一时间只允许一个线程持有锁。
-
递归性:可重入锁允许同一个线程在其执行过程中多次获得同一锁,只要每次获取锁时的嵌套深度不超过锁的最大重入深度。
-
灵活性:现代可重入锁通常提供了更多的控制功能,比如公平性和非公平性、超时等待等,这些特性使得锁的选择更加灵活,可以根据不同的应用场景进行调整。
使用可重入锁的原因:
-
减少锁竞争:在多级嵌套的代码结构中,使用可重入锁可以避免频繁地上下文切换和锁的争夺,从而提高程序的执行效率。
-
简化并发控制:对于需要在多个方法内部或不同方法之间共享资源的情况,可重入锁简化了同步控制逻辑,减少了复杂的锁管理问题。
-
增强代码可读性:在设计上考虑重入性可以使代码逻辑更加清晰,因为程序员不需要担心在递归调用时如何正确释放锁。
-
支持高级并发模型:可重入锁为实现更复杂的并发控制策略提供了基础,如工作窃取模式、线程池中的任务调度等。
Lua脚本介绍
在高并发的情况下,为了防止出现超卖问题,需要保证一个线程获取锁和释放锁的操作具有原子性,解决方案之一就是使用Lua脚本。
lua环境安装
Windows的安装可以参考菜鸟教程:学习站点:Lua 环境安装 | 菜鸟教程 (runoob.com)
注:在IDEA中编写Lua脚本,需要先下载一个Lua脚本插件**Tarantool-EmmyLua**
Lua脚本是如何确保原子性的?
Redis使用(支持)相同的Lua解释器,来运行所有的命令。Redis还保证脚本以原子方式执行:在执行脚本时,不会执行其他脚本或Redis命令。这个语义类似于MULTI(开启事务)/EXEC(触发事务,一并执行事务中的所有命令)。从所有其他客户端的角度来看,脚本的效果要么仍然不可见,要么已经完成。
注意:虽然Redis在单个Lua脚本的执行期间会暂停其他脚本和Redis命令,以确保脚本的执行是原子的,但如果Lua脚本本身出错,那么无法完全保证原子性。也就是说Lua脚本中的Redis指令出错,会发生回滚以确保原子性,但Lua脚本本身出错就无法保障原子性。
Redissson可重入锁原理
Redisson内部释放锁,并不是直接执行del命令将锁给删除,而是将锁以hash数据结构的形式存储在Redis中,每次获取锁,都将value的值+1,每次释放锁,都将value的值-1,只有锁的value值归0时才会真正的释放锁,从而确保锁的可重入性。
1)编写获取锁的Lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by ghp.
--- DateTime: 2023/2/14 16:11
---
-- 获取锁的key,即: KEY_PREFIX + name
local key = KEYS[1];
-- 获取当前线程的标识, 即: ID_PREFIX + Thread.currentThread().getId()
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; -- 返回1表示锁获取成功
end
-- 缓存中已存在锁,判断threadId是否说自己的
if (redis.call('HEXISTS', key, threadId) == 1) then
-- 是自己的锁,获取锁然后重入次数+1
redis.call('HINCRBY', key, threadId, '1');
-- 设置有效期
redis.call('EXPIRE', key, releaseTime);
return 1; -- 返回1表示锁获取成功
end
-- 锁不是自己的,直接返回0,表示锁获取失败
return 0;
2)编写释放锁的Lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by ghp.
--- DateTime: 2023/2/14 16:11
---
-- 获取锁的key,即: KEY_PREFIX + name
local key = KEYS[1];
-- 获取当前线程的标识, 即: ID_PREFIX + Thread.currentThread().getId()
local threadId = ARGV[1];
-- 锁的有效期
local releaseTime = ARGV[2];
-- 判断当前线程的锁是否还在缓存中
if (redis.call('HEXISTS', key, threadId) == 0) then
-- 缓存中没找到自己的锁,说明锁已过期,则直接返回空
return nil; -- 返回nil,表示啥也不干
end
-- 缓存中找到了自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 进一步判断是否需要释放锁
if (count > 0) then
-- 重入次数大于0,说明不能释放锁,且刷新锁的有效期
redis.call('EXPIRE', key, releaseTime);
return nil;
else
-- 重入次数等于0,说明可以释放锁
redis.call('DEL', key);
return nil;
end
3)编写可重入锁:
public class ReentrantLock implements Lock {
/**
* RedisTemplate
*/
private StringRedisTemplate stringRedisTemplate;
/**
* 锁的名称
*/
private String name;
/**
* key前缀
*/
private static final String KEY_PREFIX = "lock:";
/**
* ID前缀
*/
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
/**
* 锁的有效期
*/
public long timeoutSec;
public ReentrantLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
/**
* 加载获取锁的Lua脚本
*/
private static final DefaultRedisScript<Long> TRYLOCK_SCRIPT;
static {
TRYLOCK_SCRIPT = new DefaultRedisScript<>();
TRYLOCK_SCRIPT.setLocation(new ClassPathResource("lua/re-trylock.lua"));
TRYLOCK_SCRIPT.setResultType(Long.class);
}
/**
* 获取锁
*
* @param timeoutSec 超时时间
* @return
*/
@Override
public boolean tryLock(long timeoutSec) {
this.timeoutSec = timeoutSec;
// 执行lua脚本
Long result = stringRedisTemplate.execute(
TRYLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId(),
Long.toString(timeoutSec)
);
return result != null && result.equals(1L);
}
/**
* 加载释放锁的Lua脚本
*/
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("lua/re-unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
/**
* 释放锁
*/
@Override
public void unlock() {
// 执行lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId(),
Long.toString(this.timeoutSec)
);
}
}
4)编写测试类
@SpringBootTest
@Slf4j
public class ReentrantLockTest {
@Resource
private StringRedisTemplate stringRedisTemplate;
private ReentrantLock lock;
/**
* 方法1获取一次锁
*/
@Test
void method1() {
boolean isLock = false;
// 创建锁对象
lock = new ReentrantLock(stringRedisTemplate, "order:" + 1);
try {
isLock = lock.tryLock(1200);
if (!isLock) {
log.error("获取锁失败,1");
return;
}
log.info("获取锁成功,1");
method2();
} finally {
if (isLock) {
log.info("释放锁,1");
lock.unlock();
}
}
}
/**
* 方法二再获取一次锁
*/
void method2() {
boolean isLock = false;
try {
isLock = lock.tryLock(1200);
if (!isLock) {
log.error("获取锁失败, 2");
return;
}
log.info("获取锁成功,2");
} finally {
if (isLock) {
log.info("释放锁,2");
lock.unlock();
}
}
}
}