Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析
原文:Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析
一、RedissonLock#lock 源码分析
1、根据锁key计算出 slot,一个slot对应的是redis集群的一个节点
redisson 支持分布式锁的功能,基本都是基于 lua 脚本来完成的,因为分布式锁肯定是具有比较复杂的判断逻辑,而lua脚本可以保证复杂判断和复杂操作的原子性。
redisson 的 RedissonLock 执行lua脚本,需要先找到当前锁key需要存放到哪个slot,即在集群中哪个节点进行操作,后续不同客户端或不同线程再使用这个锁key进行上锁,也需要到对应的节点的slot中进行加锁操作。
执行lua脚本的源码:
org.redisson.command.CommandAsyncService#evalWriteAsync(java.lang.String, org.redisson.client.codec.Codec, org.redisson.client.protocol.RedisCommand<T>, java.lang.String, java.util.List<java.lang.Object>, java.lang.Object...)
@Override
public <T, R> RFuture<R> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
// 根据锁key找到对应的redis节点
NodeSource source = getNodeSource(key);
return evalAsync(source, false, codec, evalCommandType, script, keys, params);
}
private NodeSource getNodeSource(String key) {
// 计算锁key对应的slot
int slot = connectionManager.calcSlot(key);
return new NodeSource(slot);
}
计算 slot 分主从模式和集群模式,我们一般生产环境都是使用集群模式。
public static final int MAX_SLOT = 16384;
@Override
public int calcSlot(String key) {
if (key == null) {
return 0;
}
int start = key.indexOf('{');
if (start != -1) {
int end = key.indexOf('}');
key = key.substring(start+1, end);
}
// 使用 CRC16 算法来计算 slot,其中 MAX_SLOT 就是 16384,redis集群规定最多有 16384 个slot。
int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;
log.debug("slot {} for {}", result, key);
return result;
}
2、RedissonLock 之 lua 脚本加锁
RedissonLock#tryLockInnerAsync
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (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(getName()), internalLockLeaseTime, getLockName(threadId));
}
2.1、KEYS
Collections.singletonList(getName())
KEYS:["myLock"]
2.2、ARGVS
internalLockLeaseTime,getLockName(threadId)
internalLockLeaseTime:其实就是 watchdog 的超时时间,默认是30000毫秒 Config#lockWatchdogTimeout。
private long lockWatchdogTimeout = 30 * 1000;
getLockName(threadId):客户端ID(UUID):线程ID(threadId)
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
ARGVS:[30000,"UUID:threadId"]
2.3、lua 脚本分析
1、分支一:不存在加锁记录,获取锁成功
lua脚本:
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
分析:
利用 exists 命令判断 myLock 这个 key 是否存在
exists myLock
如果不存在,则执行下面两个操作
执行一个map的操作,给指定key的值增加1
hincrby myLock UUID:threadId
执行后多了一个map数据结构:
myLock:{
"UUID:threadId":1
}
给 myLock 设置过期时间为30000毫秒
expire myLock 30000
最后返回nil,即null
2、分支二:锁记录已存在,重复加锁
lua脚本:
"if (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; " +
分析:
判断之前加锁的是否为当前客户端当前线程
hexists myLock UUID:threadId
如果存在,则将加锁次数增加1
hincrby myLock UUID:threadId 1
增加1后,map集合内容为:
myLock:{
"UUID:threadId":2
}
利用map这个数据结构,存放加锁的客户端线程信息,从而支持可重入锁。
重新刷新 myLock 的过期时间为30000毫秒
expire myLock 30000
3、分支三:获取锁失败,直接返回锁剩余过期时间
lua脚本:
"return redis.call('pttl', KEYS[1]);"
分析:
- 利用 pttl 命令获取锁剩余毫秒数
pttl myLock
- 返回步骤1获取的毫秒数
3、watchdog 不断为锁续命
因为我们是利用 lock() 方法获取锁的,没有指定多久后释放,但是 redisson 不可能真的不设置锁key的过期时间。
因为要考虑到一个场景:一个客户端成功获取锁,但是没有设置多久释放,如果redisson 在redis实例中设置锁的时候也没有设置过期时间,如果这个时候客户端所在的服务器挂掉了,那么他就不会执行到unlock() 方法去释放锁了,那么这个时候就会导致死锁,其他任何的客户端都获取不到锁。
所以 redisson 会有一个 watchdog 的角色,每隔10_000毫秒就会为锁续命,详细可看看下面截图:
再看看定时任务详细的设计:
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
// 一开始就是null,直接放入 EXPIRATION_RENEWAL_MAP 中
entry.addThreadId(threadId);
// 调用定时任务
renewExpiration();
}
}
private void renewExpiration() {
// 上面已经传入,不为空
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 开启定时任务,时间是 internalLockLeaseTime / 3 毫秒后执行
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 判断是否存在 ExpirationEntry,只要加锁了,肯定存在
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// reschedule itself
// 循环调用
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断 myLock map 中是否存在当前客户端当前线程
myLock:{
"UUID:threadId":1
}
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 存在,刷新过期时间,30_000毫秒
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
4、死循环获取锁
关于死循环获取锁,这里是抓大放小,没有深入研究里面比较细的点,只有自己大概的猜测。
代码看下图:
如果获取锁失败,在进入死循环前,会订阅指定渠道:redisson_lock__channel:{myLock}
,然后进入死循环。
在死循环里面,首先会先尝试再获取一遍锁,因为可能之前获取锁的客户端刚好释放锁了。如果获取失败,那么就进入等待状态,等待时间是获取锁失败时返回的锁key的ttl。
订阅指定channel猜测:因为在客户端释放锁的时候,会往这个channel发送消息;因此可以利用此消息来提前让等待的线程被唤醒去尝试获取锁,因为此时锁已经被释放了。
5、其他的加锁方式
如果我们需要指定获取锁成功后持有锁的时长,可以执行下面方法,指定 leaseTime
lock.lock(10, TimeUnit.SECONDS);
如果指定了 leaseTime,watchdog就不会再启用了。
如果不但需要指定持有锁的时长,还想避免锁获取失败时的死循环,可以同时指定 leaseTime 和 waitTime
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
如果指定了 waitTime,只会在 waitTime 时间内循环尝试获取锁,超过 waitTime 如果还是获取失败,直接返回false。
Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析的更多相关文章
- Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析
这是关于RecyclerView的第二篇,说的是如何自定义Item动画,但是请注意,本文不包含动画的具体实现方法,只是告诉大家如何去自定义动画,如何去参考源代码. 我们知道,RecyclerView默 ...
- Rplidar学习(四)—— ROS下进行rplidar雷达数据采集源码分析
一.子函数分析 1.发布数据子函数 (1)雷达数据数据类型 Header header # timestamp in the header is the acquisition time of # t ...
- 重学c#系列——盛派自定义异常源码分析(八)
前言 接着异常七后,因为以前看过盛派这块代码,正好重新整理一下. 正文 BaseException 首先看下BaseException 类: 继承:public class BaseException ...
- ReentrantLock(重入锁)简单源码分析
1.ReentrantLock是基于AQS实现的一种重入锁. 2.先介绍下公平锁/非公平锁 公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁. 非公平锁 非公平锁是指多个线程获取锁的顺序并不是按照申 ...
- Java锁及AbstractQueuedSynchronizer源码分析
一,Lock 二,关于锁的几个概念 三,ReentrantLock类图 四,几个重要的类 五,公平锁获取 5.1 lock 5.2 acquire 5.3 tryAcquire 5.3.1 hasQu ...
- Springboot基于Redisson实现Redis分布式可重入锁【案例到源码分析】
一.前言 我们在实现使用Redis实现分布式锁,最开始一般使用SET resource-name anystring NX EX max-lock-time进行加锁,使用Lua脚本保证原子性进行实现释 ...
- 【分布式锁】06-Zookeeper实现分布式锁:可重入锁源码分析
前言 前面已经讲解了Redis的客户端Redission是怎么实现分布式锁的,大多都深入到源码级别. 在分布式系统中,常见的分布式锁实现方案还有Zookeeper,接下来会深入研究Zookeeper是 ...
- 时间轮机制在Redisson分布式锁中的实际应用以及时间轮源码分析
本篇文章主要基于Redisson中实现的分布式锁机制继续进行展开,分析Redisson中的时间轮机制. 在前面分析的Redisson的分布式锁实现中,有一个Watch Dog机制来对锁键进行续约,代码 ...
- Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁
Java锁 锁一般来说用作资源控制,限制资源访问,防止在并发环境下造成数据错误 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 Reentr ...
随机推荐
- 利用python爬取城市公交站点
利用python爬取城市公交站点 页面分析 https://guiyang.8684.cn/line1 爬虫 我们利用requests请求,利用BeautifulSoup来解析,获取我们的站点数据.得 ...
- 调试器gdb
1.启动和退出gdb gdb调试的对象是可执行文件,而不是程序源代码.如果要使一个可执行文件可以被gdb调试,那么在使用编译器gcc编译程序时加入-g选项.-g选项告诉gcc在编译程序时加入调试信息, ...
- Android 百度地图用法
一.展示百度地图,并将一个指定的点(根据经纬度确定)展示在手机屏幕中心 1.下载百度地图移动版API(Android)开发包 要在Android应用中使用百度地图API,就要在工程中引入百度地图API ...
- Can references refer to invalid location in C++?
在C++中,引用比指针更加的安全,一方面是因为引用咋定义时必须进行初始化,另一方面是引用一旦被初始化就无法使其与其他对象相关联. 但是,在使用引用的地方仍然会有一些例外. (1)Reference t ...
- 【Spring Framework】Spring入门教程(七)Spring 事件
内置事件 Spring中的事件是一个ApplicationEvent类的子类,由实现ApplicationEventPublisherAware接口的类发送,实现ApplicationListener ...
- TCP协议三步挥手与四步挥手
关于TCP协议 TCP(Transmission Control Protocol, 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议.与之对应的是UDP(User Datagram ...
- eclipse.ini配置 vmargs 说明
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M 1. 各个参数的含义什么? 参数中-vmargs的意思是设置JVM参数, ...
- RocketMQ架构原理解析(三):消息索引
一.概述 "索引"一种数据结构,帮助我们快速定位.查询数据 前文我们梳理了消息在Commit Log文件的存储过程,讨论了消息的落盘策略,然而仅仅通过Commit Log存储消息是 ...
- 文件系统系列学习笔记 - inode/dentry/file/super(2)
此篇文章主要介绍下linux 文件系统下的主要对象及他们之间的关系. 1 inode inode结构中主要包含对文件或者目录原信息的描述,原信息包括但不限于文件大小.文件在磁盘块中的位置信息.权限位. ...
- 再识requests
高级用法 本篇文档涵盖了 Requests 的一些高级特性. 会话对象 会话对象让你能够跨请求保持某些参数.它也会在同一个 Session 实例发出的所有请求之间保持 cookie, 期间使用 url ...