🍰 个人主页:不摆烂的小劉
🍞文章有不合理的地方请各位大佬指正。
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️
本文主要讨论Redission
的基本使用和特点:可重入、重试、超时释放
在分布式系统里,多个服务可能会同时对共享资源进行操作,这样就可能引发数据不一致的问题。Redisson
提供了分布式锁的实现,能够保证在分布式环境下同一时间只有一个服务可以访问共享资源。
文章目录
Redission的使用
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
- 配置
Redisson
客户端
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
- 实践应用
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
// 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// 尝试获取锁 获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
// 判断释放获取成功
if (isLock) {
try {
System.out.println("执行业务");
} finally {
//释放锁
lock.unlock();
}
}
}
Redission可重入
1.什么是可重入?
可重入,同一个线程能够多次获取同一把锁,不会因为之前已经持有这把锁而被阻塞。例如把锁想象成房间的门禁卡,同一个人拿着这张卡,不管进出这个房间多少次,都能顺利通过门禁,而不会被拦住
2.可重入有什么用?
- 避免死锁:线程在持有锁的过程中,需要再次获取该锁时,如果使用不可重入锁,线程会被阻塞,导致自己等待自己释放锁,从而造成死锁。而可重入锁允许线程在持有锁的情况下再次获取锁
- 简化代码逻辑:在同一个事务中需要多次操作同一受保护资源,需要相同锁的方法;如果使用不可重入锁,开发者需要在调用嵌套方法时手动处理锁的释放和重新获取,或者锁的颗粒度较大,影响性能
3.可重入锁 vs 不可重入锁
-
业务流程
用户点击“立即购买” → 检查库存 → 扣减库存 → 生成订单 → 支付回调。
这些步骤可能由不同方法调用,但需共享同一把锁(如lock:order:userId
)保证原子性。 -
不可重入锁
假设方法A(检查库存)获取锁后,调用方法B(扣减库存)。
若锁不可重入,方法B会因线程A已持锁而被阻塞,导致订单流程中断,甚至触发超时错误 -
可重入锁
线程A首次获取锁时,count=1
,正常执行方法A。
进入方法B时,因owner仍是线程A,直接增加count=2,无需重新加锁。
最终释放锁时,count
从2递减到0,锁被删除
3. Redission
可重入的实现原理
原理:Hash
结构+计数器
Redission
的lua
脚本位置org.redisson.RedissonLock#tryLockInnerAsync
-- 检查锁的键是否存在
if redis.call('exists', KEYS[1]) == 0 then
-- 首次获取锁
-- 哈希表 KEYS[1] 中字段 ARGV[2] 的值增加 1
redis.call('hincrby', KEYS[1], ARGV[2], 1)
-- 锁的键KEYS[1]设置过期时间ARGV[1]
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
-- 检查哈希表中指定字段是否存在
elseif redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
-- 哈希表 KEYS[1] 中字段 ARGV[2] 的值增加 1
redis.call('hincrby', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
-- 其他情况返回锁剩余时间
return redis.call('pttl', KEYS[1])
4.代码举例应用
@Resource
private RedissonClient redissonClient;
private RLock lock;
@BeforeEach
void setUp() {
lock = redissonClient.getLock("order");
}
@Test
void method1() throws InterruptedException {
// 尝试获取锁
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
if (!isLock) {
log.error("获取锁失败 .... 1");
return;
}
try {
log.info("获取锁成功 .... 1");
method2();
log.info("开始执行业务 ... 1");
} finally {
log.warn("准备释放锁 .... 1");
lock.unlock();
}
}
void method2() {
// 尝试获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败 .... 2");
return;
}
try {
log.info("获取锁成功 .... 2");
log.info("开始执行业务 ... 2");
} finally {
log.warn("准备释放锁 .... 2");
lock.unlock();
}
}
执行结果
Redission重试
1.什么是重试?
指定的等待时间内持续尝试获取锁,直到超时或成功获取锁
2.重试有什么用?
- 重试可以增加锁获取的机会,提高业务操作的成功率;如果一个用户请求因为暂时无法获取锁而立即返回失败,影响用户体验
- 当锁被释放时,等待的线程会被通知,减少了无效的
CPU
消耗,得益于Redis
的发布-订阅功能和Netty
的非阻塞IO
模型[Netty具体细节后续再讨论]
3.Redission重试的实现原理
- 订阅锁释放通知频道
RFuture<RedissonLockEntry> subscribeFuture = subscribe(lockName);
- 等待通知或超时
// 简化版等待逻辑
RedissonLockEntry entry = subscribeFuture.getNow();
RLatch latch = entry.getLatch(); // 分布式信号量
// 尝试获取信号量,最多等待剩余时间
boolean acquired = latch.tryAcquire(remainingTime, TimeUnit.MILLISECONDS);
if (acquired) {
// 收到锁释放通知,再次尝试获取锁
retryLock();
} else {
// 超时,返回获取失败
return false;
}
-
拿到锁执行业务
-
释放锁
释放锁会广播释放锁的通知
-- KEYS[1]: 锁名称
-- KEYS[2]: 锁释放通知频道
-- ARGV[1]: 释放消息(固定值0)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
-- 发布释放通知
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
Redission超时释放、看门狗
1.什么是超时释放、看门狗?
超时释放:锁被获取时,Redisson
会为锁设置一个默认超时时间(如30秒)
看门狗:自动续期机制,用于解决长耗时业务的锁超时问题
2.超时释放VS看门狗
机制 | 锁的行为 | 举例 |
---|---|---|
超时释放 | 锁在固定时间后自动释放(如30秒 ) | 最大等待时间4s,锁自动释放的时间5s redissonClient.getLock(“order”).tryLock(4L,5L ,TimeUnit.SECONDS); |
看门狗 | 未指定leaseTime或leaseTime=-1 业务执行时间可能超过超时时间 锁的有效期被不断续期,直到业务完成或线程异常 | 最大等待时间1s, redissonClient.getLock(“order”).tryLock(1L, TimeUnit.SECONDS); |
3. Redission
重试的实现原理
超时释放是为Hash
设置了固定的过期时间
看门狗,异步定时任务续期锁的过期时间
看门狗原理:
参数 | 含义 | 示例值 |
---|---|---|
KEYS[1] | 锁的Redis键 | “lock:123” |
ARGV[1] | 新过期时间(毫秒) | 5000 |
ARGV[2] | 锁的唯一标识(UUID) | “unique-id-123” |
--检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 在哈希表 KEYS[1] 中创建字段 ARGV[2]值初始化为1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置键的过期时间为 ARGV[1] 毫秒
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 哈希表中是否存在字段 ARGV[2]
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 递增字段 ARGV[2] 的值
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 重置键的过期时间为 ARGV[1] 毫秒
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 返回锁的剩余时间(毫秒)
return redis.call('pttl', KEYS[1]);
Redission注意事项
1.释放锁的顺序与计数器一致性
Redisson
通过计数器(Hash
结构中的字段值)实现可重入,每次获取锁时计数器递增,释放时递减。
必须成对释放锁:确保每个 lock()/tryLock()
对应一个 unlock()
,且在finally
块中释放,避免因异常导致计数器未归零,引发锁提前释放或死锁
RLock lock = redissonClient.getLock("lock:order");
try {
lock.lock(); // 可重入获取锁(计数器+1)
// 嵌套调用其他需同一锁的方法
nestedMethod();
} finally {
// 可重入锁的计数器与线程绑定,需通过 isHeldByCurrentThread() 校验,防止跨线程释放锁
if (lock.isHeldByCurrentThread()) {
// 释放锁(计数器-1,若为0则删除锁)
lock.unlock();
}
}
2. 同步与异步重试
同步重试(lock()
):线程会阻塞直到获取锁
异步重试(tryLockAsync()
):结合 RFuture
实现非阻塞调用,需处理 whenComplete
回调中的异常,避免内存泄漏。
lock.tryLockAsync(5, 10, TimeUnit.SECONDS)
.whenComplete((acquired, e) -> {
if (acquired) {
// 异步执行业务逻辑
} else {
log.error("锁获取失败:", e);
}
});
3.避免 “假死锁” 陷阱
若线程持有锁但进入死循环,看门狗会持续续期,导致其他线程无法获取锁(非真正死锁,但表现类似)
避免使用看门狗,除非特殊业务场景,必须设置锁持有时间
参考:
[1]https://redisson.org
[2]https://springdoc.cn/spring-boot-redisson/
[3]redisson使用—redisson官方文档+注释
[4]https://www.cnblogs.com/jackson0714/p/redisson.html
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️