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的分布 ...
随机推荐
- Superset 用户集成方案
注意,一下内容来自外网浏览器翻译,本人使用了将superset集成进入第三方系统,superset采用自定义身份验证+第三系统iframe嵌入方式,但是这个方式存在一个问题,iframe与redire ...
- G1原理—10.如何优化G1中的FGC
大纲 1.G1的FGC可以优化的点 2.一个bug导致的FGC(Kafka发送重试 + subList导致List越来越大) 3.为什么G1的FGC比ParNew + CMS要更严重 4.FGC的一些 ...
- C#客户端Json转DataTable
本文转自 https://blog.csdn.net/pinebud55/article/details/52240287 感谢pinebud55分享 之前我们有讨论过c#是如何处理json的,在我的 ...
- 【忍者算法】从生活场景到回文链表:探索对称性检测|LeetCode 234 回文链表
从生活场景到回文链表:探索对称性检测 生活中的回文现象 在日常生活中,回文无处不在.比如"上海自来水来自海上"."12321"这样正着读和倒着读都一样的字符串或 ...
- [ARC148C] Lights Out on Tree 题解
在考场遇到了这道题,感觉很有意思. 当时直接想到的就是虚树,可惜打挂了. 后来改对了,写篇题解纪念一下. 首先看到 \(\sum M_i\le 2\times 10^5\),很容易想到虚树的数据范围. ...
- OSAL架构
OSAL操作系统最多可以支持16个任务,由任务功耗管理PwrMgr_task_state变量可知,而OSAL每个任务最多只能支持16个事件处理,理论上最大可以执行256个事件处理. 对于一些运算能力不 ...
- PHP实现随机小姐姐扭一扭、学英语短视频Api接口搭建详细教程
图片API的文章:图片API制作,教大家制作一个自己的 图片Api ,再记录一下PHP制作视频api的方法.原理上与图片api的制作方法类似. 1.准备工作 准备一个域名,一个服务器(虚拟主机也可以) ...
- 在ubuntu系统下,安装opencv各个版本
要在Linux系统上安装OpenCV库,你可以通过包管理器(如apt)来安装.以下是详细的步骤,包括如何在/usr/local/lib或/usr/lib/x86_64-linux-gnu目录下安装Op ...
- 当我老丈人都安装上DeepSeek的时候,我就知道AI元年真的来了!
关注公众号回复1 获取一线.总监.高管<管理秘籍> 春节期间DeepSeek引爆了朋友圈,甚至连我老丈人都安装了APP,这与两年前OpenAI横空出世很不一样,DeepSeek似乎真的实现 ...
- Ubuntu下如何管理多个ssh密钥
Ubuntu下如何管理多个ssh密钥 前言 我一直在逃避这个问题,误以为我能够单纯地用一个 ssh 走天下. 好吧,现实是我不得不管理多个 ssh 做,那就写个博客总结一下吧. 查阅后发现前人已经 ...