Redis 分布式锁的正确实现原理演化历程与 Redission的源码








当线程A,加锁并设置过期时间-->执行业务-->判断锁id完成后,但这时CPU线程调度其它工作了在这里卡住了,
而且也到了锁的过期时间了被动被删除,当线程B,加锁并设置过期时间-->执行业务,这时线程A从新获取CPU执行权,继续主动删除锁,
从而产生误删,线程A将线程B加的锁给删了,所以需要 GET + DEL保持原子性










一、Redisson源码分析及原理详解
1、获取锁
RLock rLock = redissonClient.getLock(lockKey);
2、加锁
关于加锁,提供了下面一系列的方法

3、释放锁
rLock.unlock();
4、RLock接口
RLock接口继承了Lock接口,以及RLockAsync接口;它是Redisson提供的用于分布式锁的核心接口,它定义了获取锁和释放锁等方法 ,并扩展了很多方法

方法解析

注:除了以上的及方法外,RLock接口还提供了其他方法来支持其他类型的锁:比如:可重入锁、公平锁、联锁、红锁、读写锁、闭锁等特性,以便满足更为复杂的分布式锁需求场景
二、源码分析
1、创建锁对象
RLock rLock = redissonClient.getLock(lock);


进入父类构造方法RedissonBaseLock:

name:锁的名称;
id :随机序列号;
pubsub:锁订阅;
entryName:随机序列号+锁名称;
commandExecutor: lua脚本的executor执行器
internalLockLeaseTime : 取自 Config#lockWatchdogTimeout,默认30秒,这个参数还有另外一个作用,锁续命的执行周期 internalLockLeaseTime/3 = 10秒
2、加锁,下面以lock方法为例进行分析;
rLock.lock();

到这里我们看到waitTime参数为-1、由第一步得知leaseTime参数也为-1;接着进入方法tryAcquire方法;

解析: tryAcquire方法:执行lua脚本并且根据返回的结果ttl判断获取锁是否成功;
接下来,如果获取锁成功并且leaseTime(锁释放时间)为-1则开启看门狗,刷新锁的过期时间防止锁过期失效
最后进入tryAcquireAsync方法:

调用tryLockInnerAsync方法,如果获取锁失败,返回的结果是这个key的剩余有效期,如果获取锁成功,返回null。

Redisson获取锁的实现是通过lua脚本来实现的!

通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了锁key的剩余生存时间),加锁失败
获取锁成功后,ttlRemaining==null成立,由上得知leaseTime = -1,执行scheduleExpirationRenewal(threadId)方法来启动看门狗机制

关于ExpirationEntry类,一个锁就对应自己的一个ExpirationEntry类,那么EXPIRATION_RENEWAL_MAP存放的是所有的锁信息;
根据锁的名称从EXPIRATION_RENEWAL_MAP里面获取锁,如果存在这把锁则存入;如果不存在,则将这个新锁放置进EXPIRATION_RENEWAL_MAP,并且开启看门狗机制。
第一次获取锁oldEntry==null,进入上面else逻辑,进入renewExpiration方法:


Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);
参数说明:
task:要执行的定时任务
delay:延迟时间
unit:时间单位
关于renewExpiration方法:
首先,从EXPIRATION_RENEWAL_MAP中获取这个锁,接下来定义一个延迟任务task,这个任务的步骤如下:
新创建了一个子线程去反复调用。
从EXPIRATION_RENEWAL_MAP中获取这把锁,如果这把锁不存在了,说明被删除了,不在需要续期了
从锁中获取获得这把锁的线程threadId
调用renewExpirationAsync方法刷新最长等待时间
如果刷新成功,则进来递归调用这个函数renewExpiration()
客户端A加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端A业务代码还没执行完,还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
3、回到lock方法逻辑
如果加锁成功ttl则返回null,直接返回,加锁流程结束;
如果加锁失败了,这里返回的ttl为过期时间,则会执行下面的逻辑。

源码解析:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 1、获取锁,加锁成功:ttl为null; 加锁失败:返回的ttl为过期时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 2、表示加锁成功
if (ttl == null) {
return;
}
// 3、此时,表示加锁失败了 异步订阅当前key, threadId只有公平锁时候才有用
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) { //是否支持中断 下面同步执行订阅(其实是有个默认的订阅时间, 超时就会报错, 防止异常或者太久卡死在这)
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
//到这里, 说明key被释放了 , 可以抢锁了
try {
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId); // 还是调用之前的方法, 抢锁
// lock acquired
if (ttl == null) { // 成功, 那就中断跳出去
break;
}
// waiting for message
if (ttl >= 0) { // 被别人抢走了
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else { // <0 //在Redis 2.6和之前版本,如果key不存在或者key存在且无过期时间将返回-1。
// 从 Redis 2.8开始,错误返回值发送了如下变化:
// 如果key不存在返回-2
// 如果key存在且无过期时间返回-1
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally { //没抢到锁 ,就一直在while true里面轮询
// 取消订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
加锁流程小结:
当前线程,调用tryAcquire方法执行LUA脚本进行加锁;若没有知道锁生效的时间,设置超时时间为30秒。
获取锁成功(返回的ttl == null), 直接返回;
获取锁失败(返回的ttl为过期时间) 则进行如下处理:
订阅当前key,并阻塞, 直到锁被释放。
while(true)循环, 再尝试获取锁, 如果获取成功, 跳出循环直接返回。
如果获取失败, 那么继续阻塞, 等待锁释放。并重复上一步操作
跳出循环后,取消订阅。
详细流程图如下:

三、解锁流程
unlock()方法
@Override
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
进入unlockAsync方法:
@Override
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<>();
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
result.trySuccess(null);
});
return result;
}
进入unlockInnerAsync方法执行LUA解锁脚本:
/**
* 解锁
* @param threadId 当前线程id
* @return
* null: 当前线程没有锁;0: 当前线程还持有重入锁; 1: 释放完毕
*/
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + // 如果当前线程没持有锁, 或者锁过期了,返回null
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //走到这,确定当前线程持有锁, 对锁减一, (可重入)
"if (counter > 0) then " + // 如果还持有锁, 没释放完
"redis.call('pexpire', KEYS[1], ARGV[2]); " + //续期, 延长锁的时间到internalLockLeaseTime
"return 0; " + //返回0
"else " +
"redis.call('del', KEYS[1]); " + //否则证明锁已经释放完毕, 删除锁
"redis.call('publish', KEYS[2], ARGV[1]); " + //推送消息 , 当前锁已经释放
"return 1; " + //释放成功,返回1
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), // KEYS[1] key channel name (redisson_lock__channel+key)
LockPubSub.UNLOCK_MESSAGE, //ARGV[1] 解锁消息 0
internalLockLeaseTime, //ARGV[2] 时间
getLockName(threadId)); // ARGV[3] connectionid+threadid , 对应的是field
}
该代码解析:
如果释放的锁线程和已存在锁线程不是同一个线程,返回 null
通过hincrby递减1,先释放一次锁,若剩余次数还大于0,则证明当前锁是重入锁,刷新过期时间
若剩余次数小于0,删除key并发布锁释放消息,解锁成功

源码解析:注:如果当前线程没有持有锁,调用RLock.unlock()方法不会抛出异常,也不会影响到其他线程。
Redis 分布式锁的正确实现原理演化历程与 Redission的源码的更多相关文章
- Redis全方位详解--数据类型使用场景和redis分布式锁的正确姿势
一.Redis数据类型 1.string string是Redis的最基本数据类型,一个key对应一个value,每个value最大可存储512M.string一半用来存图片或者序列化的数据. 2.h ...
- 七种方案!探讨Redis分布式锁的正确使用姿势
前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...
- 论Redis分布式锁的正确使用姿势
前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...
- Redis分布式锁的正确使用与实现原理
模拟一个电商里面下单减库存的场景. 1.首先在redis里加入商品库存数量. 2.新建一个Spring Boot项目,在pom里面引入相关的依赖. <dependency> <gro ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- 【分布式缓存系列】集群环境下Redis分布式锁的正确姿势
一.前言 在上一篇文章中,已经介绍了基于Redis实现分布式锁的正确姿势,但是上篇文章存在一定的缺陷——它加锁只作用在一个Redis节点上,如果通过sentinel保证高可用,如果master节点由于 ...
- Redis(十三):Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁的正确实现方式(Java版)
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis 分布式锁的正确实现方式(转)
_ 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各 ...
- 【转】Redis 分布式锁的正确实现方式( Java 版 )
链接:wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/ 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布 ...
随机推荐
- 【Docker】---Docker入门篇(1)
Docker入门篇 简单一句话: Docker 是一个便携的应用容器. 一.Docker的作用 网上铺天盖地的是这么说的: (1) Docker 容器的启动可以在秒级实现,这相比传统的虚拟机方式要快得 ...
- linux:配置NTP
介绍 网络时间协议(Network Time Protocol,NTP),用于同步网络中各个计算机的时间的协议.其用途是将计算机的时钟同步到世界协调时 UTC. ntpd(Network Time P ...
- Vim编辑器退出的多种方法
当文本编辑结束之后,通常需要退出编辑器.退出编辑器又分为4种情况:保存退出.正常退出.不保存退出及强制退出.下面简单说下吧! 1.先介绍一下保存退出.当我们编辑或修改好了文件内容,如图. 我们 ...
- C#中使用正则将字符串中某字符不区分大小写并按全字匹配替换为空
具体代码如下所示: //将字符串中desc不区分大小写并按全字匹配替换为空 var strText = "CreatDeSce DeSc,UserName AsC"; string ...
- 【1】JobManager启动
一.Flink底层通信技术 Akka + Netty Akka:它是基于协程的,基于scala的偏函数 Netty:相比更加基础一点,可以为不同的应用层通信协议(RPC,FTP,HTTP等)提供支持 ...
- 记一次腾讯云轻量级服务器安装mysql配置完成后,外网无法访问问题
一.配置信息正常 1.防火墙配置通过 2.mysql端口正常启动netstat -antlp | grep 3306 3.配置都正常,但是telnet访问不通超时Operation timed out ...
- Linux驱动---设备驱动模型
目录 一.简介 二.驱动模型 2.1.总线 2.2.设备 2.3.驱动 三.设备树 3.1.设备树简介 3.2.设备树格式 3.3.节点格式 3.4.节点属性 四.设备树API函数 4.1获取设备节点 ...
- 浅说 c++20 cppcoro (三)
浅说 c++20 cppcoro (三),https://www.cnblogs.com/bbqzsl/p/18679860 接着上一篇浅说 c++20 coroutine (二) ,继续没说完的事. ...
- 燕千云ITSM已支持DeepSeek对接!AI能力持续升级
春节期间,DeepSeek火爆全网,引发热议,作为国产AI大模型的黑马,DeepSeek凭借独特的训练方法.先进的模型架构和强大的联网推理能力,正不断拓展AI技术的应用边界.其"快思考&qu ...
- Python中requests库的安装
这篇博客指导读者如何通过管理员权限的CMD命令行,进入Python主目录并使用pip安装requests库.在PyCharm中,如果IDE未检测到requests,用户需要在此安装,或者考虑更新Pyt ...