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的分布 ...
随机推荐
- ctfshow--web1
第一题很简单 就是一个base64编码 我们打开开发者模式看源代码 Y3Rmc2hvd3s1MGMyZDdkYS1lOWZjLTQ5YzItYTRjZC1iZmJmZjIyYmI4NWV9将这段话ba ...
- dart变量声明和变量类型
ps==>所有的代码必须放在main方法中 main方法有两种写法 1==> main() { print("你好,dart我们相遇了"); } 2==> voi ...
- 在阿里云ECS上一键部署DeepSeek-R1
DeepSeek-R1 是一款开源模型,也提供了 API(接口)调用方式.据 DeepSeek介绍,DeepSeek-R1 后训练阶段大规模使用了强化学习技术,在只有极少标注数据的情况下提升了模型推理 ...
- Luogu P3041 USACO12JAN Video Game G 题解 [ 紫 ] [ AC 自动机 ] [ 动态规划 ]
Video Games G:弱智紫题,30min 切了,dp 思路非常板. 多模式串一看肯定就是要建出 AC 自动机,然后在 fail 树里下传标记,预处理每个节点到达后的得分. 然后设计 \(dp_ ...
- Scrapy css选择器提取数据
原文学习链接:http://www.scrapyd.cn/doc/185.html 一. 标签属性值的提取 href的值URL的提取:这是最常见的,我们要进入下一页.或是打开内容页--都少不了URL值 ...
- autohue.js:让你的图片和背景融为一体,绝了!
需求 先来看这样一个场景,拿一个网站举例 这里有一个常见的网站 banner 图容器,大小为为 1910*560 ,看起来背景图完美的充满了宽度,但是图片原始大小时,却是: 它的宽度只有 1440 , ...
- 动态代理到AOP
动态代理 代理(proxy)是一种设计模式,通过了目标对象的另外访问方法,即通过代理对象访问目标对象.动态代理是再程序运行时动态地生成一个代理类代替原本的类.该类会拦截对目标对象的方法调用 为什么使用 ...
- PowerShell开发游戏 · 打蜜蜂
可以看到,虽然非常抽象简单,但是基础游戏框架已经搭建,游戏机制完善,就缺美工了,哈哈~~~~ [首先] Powershell不是用来开发游戏的,但是没人规定不能开发.因为它可以调取windo ...
- 什么是CPU?
当你用手机刷短视频.用电脑玩游戏,或是使用智能手表查看健康数据时,这些设备的核心"大脑"--CPU(中央处理器)正在默默工作.它是现代计算设备的核心,但很多人对它一知半解.今天我们 ...
- 【数值计算方法】线性方程组迭代算法的Python实现
线性方程组迭代算法的Python实现 jacobi,GS,SOR迭代法 def JacobiIter(A:np.ndarray, b:np.ndarray, tol:float=1e-5, maxIt ...