Redisson 框架中的分布式锁
实现分布式锁通常有三种方式:数据库、Redis 和 Zookeeper。我们比较常用的是通过 Redis 和 Zookeeper 实现分布式锁。Redisson 框架中封装了通过 Redis 实现的分布式锁,下面我们分析一下它的具体实现。
by emanjusaka from https://www.emanjusaka.top/2024/03/redisson-distributed-lock 彼岸花开可奈何
本文欢迎分享与聚合,全文转载请留下原文地址。
关键点
原子性
要么都成功,要么都失败
过期时间
如果锁还没来得及释放就遇到了服务宕机,就会出现死锁的问题。给 Redis 的 key 设置过期时间,即使服务宕机了超过设置的过期时间锁会自动进行释放。
锁续期
因为给锁设置了过期时间而我们的业务逻辑具体要执行多长时间可能是变化和不确定的,如果设定了一个固定的过期时间,可能会导致业务逻辑还没有执行完,锁被释放了的问题。锁续期能保证锁是在业务逻辑执行完才被释放。
正确释放锁
保证释放自己持有的锁,不能出现 A 释放了 B 持有锁的情况。
Redis 实现分布式锁的几种部署方式
单机
在这种部署方式中,Redis 的所有实例都部署在同一台服务器上。这种部署方式简单易行,但存在单点故障的风险。如果 Redis 实例宕机,则所有分布式锁都将失效。
哨兵
在这种部署方式中,Redis 的多个实例被配置为哨兵。哨兵负责监控 Redis 实例的状态,并在主实例宕机时自动选举一个新的主实例。这种部署方式可以提供更高的可用性和容错性。
集群
在这种部署方式中,Redis 的多个实例被配置为一个集群。集群中的每个实例都是平等的,并且可以处理读写操作。这种部署方式可以提供最高的可用性和容错性。
红锁
搞几个独立的 Master,比如 5 个,然后挨个加锁,只要超过一半以上(这里是 5/2+1=3 个)就代表加锁成功,然后释放锁的时候也逐台释放。
使用方式
引入依赖
<!-- pom.xml文件-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
版本依赖:
redisson-spring-data module name Spring Boot version redisson-spring-data-16 1.3.y redisson-spring-data-17 1.4.y redisson-spring-data-18 1.5.y redisson-spring-data-2x 2.x.y redisson-spring-data-3x 3.x.y yml配置
spring:
redis:
redisson:
config:
singleServerConfig:
address: redis://127.0.0.1:6379
database: 0
password: null
timeout: 3000
直接注入使用
package top.emanjusaka; import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service; import javax.annotation.Resource;
import java.util.concurrent.TimeUnit; /**
* @Author emanjusaka www.emanjusaka.top
* @Date 2024/2/28 16:41
* @Version 1.0
*/
@Service
public class Lock {
@Resource
private RedissonClient redissonClient; public void lock() {
// 写入redis的key值
String lockKey = "lock-test";
// 获取一个Rlock锁对象
RLock lock = redissonClient.getLock(lockKey);
// 获取锁,并为其设置过期时间为10s
lock.lock(10, TimeUnit.SECONDS);
try {
// 执行业务逻辑....
System.out.println("获取锁成功!");
} finally {
// 释放锁
lock.unlock();
System.out.println("释放锁成功!");
}
} }
底层剖析
lock()
关键代码
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
RFuture<T>:表示返回一个异步结果对象,其中泛型参数 T 表示结果的类型。tryLockInnerAsync方法接受一下参数:waitTime:等待时间,用于指定在获取锁时的最大等待时间。leaseTime:租约时间,用于指定锁的持有时间unit:时间单位,用于将 leaseTime 转换为毫秒threadId:线程 ID,用于标识当前线程command:Redis 命令对象,用于执行 Redis 操作
方法体中的代码使用 Lua 脚本来实现分布式锁的逻辑。
- if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)): 如果键不存在或者哈希表中已经存在对应的线程ID,则执行以下操作:
- redis.call('hincrby', KEYS[1], ARGV[2], 1): 将哈希表中对应线程ID的值加1。
- redis.call('pexpire', KEYS[1], ARGV[1]): 设置键的过期时间为租约时间。
- return nil: 返回nil表示成功获取锁。
- else: 如果键存在且哈希表中不存在对应的线程ID,则执行以下操作:
- return redis.call('pttl', KEYS[1]): 返回键的剩余生存时间。
- if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)): 如果键不存在或者哈希表中已经存在对应的线程ID,则执行以下操作:
commandExecutor.syncedEval:表示同步执行 Redis 命令LongCodec.INSTANCE:用于编码和解码长整型数据Collections.singletonList(getRawName()):创建一个只包含一个元素的列表,元素为锁的名称unit.toMillis(leaseTime):将租约时间转换为毫秒getLockName(threadId):根据线程 ID 生成锁的名称
// 省去了那些无关重要的代码
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// tryAcquire就是上面分析的lua完整脚本
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 返回null就代表上锁成功。
if (ttl == null) {
return;
}
// 如果没成功,也就是锁的剩余时间不是null的话,那么就执行下面的逻辑
// 其实就是说 如果有锁(锁剩余时间不是null),那就死循环等待重新抢锁。
try {
while (true) {
// 重新抢锁
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 抢锁成功就break退出循环
if (ttl == null) {
break;
}
// 省略一些代码
}
} finally {}
}
上面代码实现了一个分布式锁的功能。它使用了Lua脚本来尝试获取锁,并在成功获取锁后返回锁的剩余时间(ttl)。如果获取锁失败,则进入一个死循环,不断尝试重新获取锁,直到成功为止。
unlock()
关键代码
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"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(ARGV[4], KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()),
LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), getSubscribeService().getPublishCommand());
}
RFuture<Boolean>: 表示返回一个异步结果对象,其中泛型参数Boolean表示结果的类型。unlockInnerAsync方法接受以下参数:threadId: 线程ID,用于标识当前线程。
- 方法体中的代码使用Lua脚本来实现分布式锁的解锁逻辑。以下是对Lua脚本的解释:
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0): 如果哈希表中不存在对应的线程ID,则返回nil表示无法解锁。local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1): 将哈希表中对应线程ID的值减1,并将结果赋值给变量counter。if (counter > 0): 如果counter大于0,表示还有其他线程持有锁,执行以下操作:redis.call('pexpire', KEYS[1], ARGV[2]): 设置键的过期时间为租约时间。return 0: 返回0表示锁仍然被其他线程持有。
else: 如果counter等于0,表示当前线程是最后一个持有锁的线程,执行以下操作:redis.call('del', KEYS[1]): 删除键,释放锁。redis.call(ARGV[4], KEYS[2], ARGV[1]): 调用发布命令,通知其他线程锁已经释放。return 1: 返回1表示成功释放锁。
return nil: 如果前面的条件都不满足,返回nil表示无法解锁。
evalWriteAsync方法用于执行Lua脚本并返回异步结果对象。getRawName(): 获取锁的名称。LongCodec.INSTANCE: 用于编码和解码长整型数据。RedisCommands.EVAL_BOOLEAN: 指定Lua脚本的返回类型为布尔值。Arrays.asList(getRawName(), getChannelName()): 创建一个包含两个元素的列表,元素分别为锁的名称和频道名称。LockPubSub.UNLOCK_MESSAGE: 发布消息的内容。internalLockLeaseTime: 锁的租约时间。getLockName(threadId): 根据线程ID生成锁的名称。getSubscribeService().getPublishCommand(): 获取发布命令。
锁续期
watchDog
核心工作流程是定时监测业务是否执行结束,没结束的话在看你这个锁是不是快到期了(超过锁的三分之一时间),那就重新续期。这样防止如果业务代码没执行完,锁却过期了所带来的线程不安全问题。
Redisson 的 watchDog 机制底层不是调度线程池,而是直接用的 netty 事件轮。
Redisson的WatchDog机制是用于自动续期分布式锁和监控对象生命周期的一种机制,确保了分布式环境下锁的正确性和资源的及时释放。
- 自动续期:当Redisson客户端获取了一个分布式锁后,会启动一个WatchDog线程。这个线程负责在锁即将到期时自动续期,保证持有锁的线程可以继续执行任务。默认情况下,锁的初始超时时间是30秒,每10秒钟WatchDog会检查一次锁的状态,如果锁依然被持有,它会将锁的过期时间重新设置为30秒。
- 参数配置:可以通过设置lockWatchdogTimeout参数来调整WatchDog检查锁状态的频率和续期的超时时间。这个参数默认值是30000毫秒(即30秒),适用于那些没有明确指定leaseTimeout参数的加锁请求。
- 重连机制:除了锁自动续期外,WatchDog机制还用作Redisson客户端的自动重连功能。当客户端与Redis服务器失去连接时,WatchDog会自动尝试重新连接,从而恢复服务的正常运作。
- 资源管理:WatchDog也负责监控Redisson对象的生命周期,例如分布式锁。当对象的生命周期到期时,WatchDog会将其从Redis中删除,避免过期数据占用过多内存空间。
- 异步加锁:在加锁的过程中,WatchDog会在RedissonLock#tryAcquireAsync方法中发挥作用,该方法是进行异步加锁的逻辑所在。通过这种方式,加锁操作不会阻塞当前线程,提高了系统的性能。
本文原创,才疏学浅,如有纰漏,欢迎指正。如果本文对您有所帮助,欢迎点赞,并期待您的反馈交流,共同成长。
原文地址: https://www.emanjusaka.top/2024/03/redisson-distributed-lock
微信公众号:emanjusaka的编程栈
Redisson 框架中的分布式锁的更多相关文章
- spring boot 利用redisson实现redis的分布式锁
原文:http://liaoke0123.iteye.com/blog/2375469 利用redis实现分布式锁,网上搜索的大部分是使用java jedis实现的. redis官方推荐的分布式锁实现 ...
- 基于Redisson+SpringBoot的Redission分布式锁
原文:https://blog.csdn.net/sunct/article/details/80178197 定义分布式锁接口 package com.redis.lock.redisson_spr ...
- Curator框架实现ZooKeeper分布式锁
排他锁(X) 这里主要讲讲分布式锁中的排他锁.排他锁(Exclusive Locks,简称X锁),又称为写锁或独占锁,是一种基本的锁类型.如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只 ...
- Redisson源码解读-分布式锁
前言 Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid).Redisson有一样功能是可重入的分布式锁.本文来讨论一下这个功能的特点以及源 ...
- redis中的分布式锁
分布式锁的实现场景 在平时的开发中,对于高并发的开发场景,我们不可避免要加锁进行处理,当然redis中也是不可避免的,下面是我总结出来的几种锁的场景 Redis分布式锁方案一 使用Redis实现分布式 ...
- 如何在springcloud分布式系统中实现分布式锁?
一.简介 一般来说,对数据进行加锁时,程序先通过acquire获取锁来对数据进行排他访问,然后对数据进行一些列的操作,最后需要释放锁.Redis 本身用 watch命令进行了加锁,这个锁是乐观锁.使用 ...
- 分布式锁中的王者方案-Redisson
上篇讲解了如何用 Redis 实现分布式锁的五种方案,但我们还是有更优的王者方案,就是用 Redisson. 缓存系列文章: 缓存实战(一):20 图 |6 千字|缓存实战(上篇) 缓存实战(二):R ...
- 使用Redisson实现分布式锁,Spring AOP简化之
源码 Redisson概述 Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid).它不仅提供了一系列的分布式的Java常用对象,还提供了许多 ...
- redis分布式锁Redisson扩展
如果大家项目中Redis是多机部署的可以来好好看看这篇实现,讲的非常好. 使用Redisson实现分布式锁,Spring AOP简化之 源码 Redisson概述 Redisson是一个在Redi ...
- Redisson实现Redis分布式锁的底层原理
一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起,一路聊到分布式事务.分布式锁.ZooKeeper等知识.所以咱们这篇文章 ...
随机推荐
- STM32CubeMX教程24 WDG - 独立窗口看门狗
1.准备材料 开发板(正点原子stm32f407探索者开发板V2.4) STM32CubeMX软件(Version 6.10.0) 野火DAP仿真器 keil µVision5 IDE(MDK-Arm ...
- (数据科学学习手札157)pandas新增case_when方法
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 大家好我是费老师,pandas在前不久更新的2. ...
- Gin 应用多实例部署session问题、session参数与刷新
目录 一.Gin Session 存储的实现方案 二.memstore:基于内存的实现 2.1 基本使用 2.2 关键参数 三.使用redis:多实例部署 3.1 使用redis优势 3.2 基本使用 ...
- 【Mysql】复合主键的索引
复合主键在where中使用查询的时候到底走不走索引呢?例如下表: create table index_test ( a int not null, b int not null, c int not ...
- 4.9 C++ Boost 命令行解析库
命令行解析库是一种用于简化处理命令行参数的工具,它可以帮助开发者更方便地解析命令行参数并提供适当的帮助信息.C++语言中,常用的命令行解析库有许多,通过本文的学习,读者可以了解不同的命令行解析库和它们 ...
- Python 实现SynFlood洪水攻击
Syn-Flood攻击属于TCP攻击,Flood类攻击中最常见,危害最大的是Syn-Flood攻击,也是历史最悠久的攻击之一,该攻击属于半开放攻击,攻击实现原理就是通过发送大量半连接状态的数据包,从而 ...
- React框架运行机制
React框架运行主流程 1.JSX是JS语言的扩展,被babel编译后,会转换成React.creatElement(),这个方法返回的是一个虚拟DOM. 2.将虚拟DOM渲染到真实DOM的方法是R ...
- ElasticSearch7.3学习(七)----Mapping映射入门
1.mapping映射 概念:自动或手动为index中的_doc建立的一种数据结构和相关配置,简称为mapping映射.插入几条数据,让es自动为我们建立一个索引 PUT /website/_doc/ ...
- 力扣只写函数好怪WWWW,转战洛谷!
转战!目标刷完官方推荐题集
- 转载洛谷:23.08.19 普及模拟1 T1
Past 题目描述 所有人,都有一段支离破碎的过去. 你有\(n\)段过去的经历,有时顺利,有时不顺,于是你用一个评价值\(a_i\)来描述你的第\(i\)段经历,它们构成了长度为\(n\)的序列\( ...