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的分布 ...
随机推荐
- 分圆多项式(cyclotomic polynomial)
最近论文中经常遇到分圆多项式,现在系统的学习一下! 本原单位根 之前介绍n次单位根,现在详细学习一下n次本原单位根(n-th primitive unit root) 一个复数是n次单位根,当且仅当具 ...
- AGC018
AGC018 B 题目大意 举办一场运动会,有 \(N\) 人,\(M\) 个项目,每个人所有项目都有一个排名,会选择参加排名最高且开设的项目,现在要开设若干项目使得人数最多的项目人数尽可能小,求这个 ...
- dart安装教程详解
官网 https://dart.dev 关于发布通道和版本字符串 Dart SDK有三个发布通道: 1==>:稳定释放,大约每三个月更新一次: 稳定释放适合生产使用. 2==>:预览发布, ...
- Dev Express WPF GridControl 数据导出到Excel
Dev Express WPF 给控件提供了公共的导出方法: Export to PDF Export to HTML Export to MHT Export to Text Export to C ...
- ETL工程师
Python Flume DataX HDFS 数仓建模分层:ODS.DIM.DWD.DWS.APS Kettle.Informatica SQL(Oracle.MySQL)
- Ansible - [07] 定义变量的几种方式
题记部分 Ansible 支持十几种定义变量的方式 Inventory 变量 Host Facts 变量 Register 变量 Playbook 变量 Playbook 提示变量 变量文件 命令行变 ...
- docker下安装Harbor
安装docker-compose # 安装docker-compose curl -L https://github.com/docker/compose/releases/download/1.18 ...
- LangChain大模型框架& Dify低代码 AI 开发平台
目录 1. LangChain介绍 1.1 架构 1.2 概念 1.3 术语 1.4 LangChain实战 2. LLM 应用开发平台dify 2.1 dify安装 2.2 设置知识库 3. dif ...
- mysql 获取数据库名、表名、字段名、根据表结构创建新表
1.查询当前使用的数据库 select database(): 2.获取当前数据库表 select * from information_schema.TABLES where TABLE_SCHEM ...
- JVM运行参数
一.三种参数类型 1.标准参数:比较稳定,以后版本会保留 -help -version 2.-X参数(非标准参数) -Xint -Xcomp 3.-XX参数(非标准参数,使用率较高) -XX:newS ...