前言
在了解了加锁和锁重入之后,最需要了解的还是在分布式场景下或者多线程并发加锁是如何处理的?
并发加锁
先来看结果,在多线程对 /locks/lock_01 加锁时,是在后面又创建了新的临时节点。
这块在加锁方法 CreateBuilderImpl#pathInForeground 中已经介绍过
这里判断 /locks/lock_01 路径已经存在,会直接创建新的临时顺序节点。
真正判断锁是否获取成功,其实是在 LockInternals#attemptLock 方法中的 internalLockLoop 方法中。
加锁结果及监听
internalLockLoop 方法的主要作用是判断加锁结果,以及获取锁失败时,对其他节点的监听。
获取父节点 /locks/lock_01 下的所有子节点,按照从小到大排序,判断自己是不是获取到锁,没有获取到就监听自己前一个节点;
支持设置超时时间,超时直接返回失败;
不支持设置超时时间或者还没有超时,则直接 wait 等待。
是否获取锁的代码在 StandardLockInternalsDriver#getsTheLock
这块就是判断 ...
前言
加锁逻辑已经介绍完毕,那当一个线程重复加锁是如何处理的呢?
锁重入
在上一小节中,可以看到加锁的过程,再回头看 internalLock 这个方法。
加锁成功之后,将当前线程放到 threadData 中,threadData 是 ConcurrentMap<Thread, LockData> 类型的,不用担心并发问题。
假如锁重入了,直接就会在上一部分 lockData != null 被拦下,然后执行 lockData.lockCount.incrementAndGet();。
对 lockCount 自增,代表了锁重入。
这里发现了吧!Curator 的锁重入是在 Java 代码中实现的。
锁释放
当锁需要释放的时候,只需要调用 lock.release() 进行释放即可,具体是如何释放的呢?
主要分为两部分:
递减 threadData 中当前线程的加锁次数;
加锁次数大于 0,说明还剩余重入次数,直接返回;
加锁次数等于 0,则 releaseLock 释放锁,并删除 threadData 中当前线程 key。
releaseLock 方法中就没有多 ...
前言
一般工作中常用的分布式锁,就是基于 Redis 和 ZooKeeper,前面已经介绍完了 Redisson 锁相关的源码,下面一起看看基于 ZooKeeper 的锁。也就是 Curator 这个框架。
Curator 的锁也分为很多种,本文分析共享可重入锁。
考虑到如果文章篇幅较长,不太适合阅读,所以对文章做了适当的拆分。
环境配置
本机三个节点
版本:3.7.0
系统:macOS
安装方式:brew install zookeeper
Curator Maven 依赖版本:5.1.0
12345<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.1.0</version></dependency>
加锁示例
详细信息可参考官方文档。
加锁前
在加锁之前,ZooKeeper 仅有一个节点 /zookeep ...
redisson
未读前言
Redisson 还支持可重入读写锁,允许在分布式场景下,同时有多个读锁和一个写锁处于加锁状态。
使用读写锁
Redisson 读写锁实现了 JUC 下的 ReadWriteLock,使用方式基本相同。
源码
加锁源码基本和之前的可重入锁加锁无区别,唯一的差异就是在 Lua 脚本这里。
所以下面着重分析 Lua 脚本。
读锁源码
源码地址:org.redisson.RedissonReadLock#tryLockInnerAsync
参数列表:
KEYS[1]:锁名字 anyRWLock
KEYS[2]:锁超时 key {锁名字}:UUID:ThreadId:rwlock_timeout 组成的字符串,{anyRWLock}:e70b1307-9ddd-43de-ac9d-9c42b5c99a0d:1:rwlock_timeout
ARGV[1]:锁时间,默认 30s
ARGV[2]:当前线程,UUID:ThreadId 组成的字符串,e70b1307-9ddd-43de-ac9d-9c42b5c99a0d:1
ARGV[3]:写锁名字,getWriteL ...
前言
Redisson 除了提供了分布式锁之外,还额外提供了同步组件,Semaphore 和 CountDownLatch。
Semaphore
意思就是在分布式场景下,只有 3 个凭证,也就意味着同时只会有三个线程执行业务。
设置凭证
参数列表:
KEYS[1]:指定的 key 这里叫 semaphore
KEYS[2]:redisson_sc:{semaphore}
ARGV[1]:凭证数 3
这块 lua 脚本相对简单,直接设置一个 key 的 value 为 3。
获取凭证
参数列表:
KEYS[1]:指定的 key 这里叫 semaphore
ARGV[1]:要获取的凭证数,默认 1
这段 lua 脚本就是:
获取 key semaphore 的值
如果值大于等于 1(要获取的凭证数),对值进行递减
成功返回 1,失败返回 0
如果成功获取凭证,直接返回,没有获取到凭证,则自旋等待。
释放凭证
释放凭证直接对 Redis key 的值进行自增即可。
CountDownLatch
输出内容如下:
源码分析
设置门闩数量
这块都大同小异。
减少门闩 ...
前言
RedLock 红锁,是分布式锁中必须要了解的一个概念。
所以本文会先介绍什么是 RedLock,当大家对 RedLock 有一个基本的了解。然后再看 Redisson 中是如何实现 RedLock 的。
在文章开头先说明 Redisson RedLock 建议不要使用!!!
在文章开头先说明 Redisson RedLock 建议不要使用!!!
在文章开头先说明 Redisson RedLock 建议不要使用!!!
重要的事情重复三遍!
什么是 RedLock?
RedLock,这块可以从网上搜到很多资料,本文也简单介绍下,当做扫盲。
单实例加锁
1SET resource_name my_random_value NX PX 30000
对于单实例 Redis 只需要使用这个命令即可。
NX:仅在不存在 key 的时候才能被执行成功;
PX:失效时间,传入 30000,就是 30s 后自动释放锁;
my_random_value:就是随机值,可以是线程号之类的。主要是为了更安全的释放锁,释放锁的时候使用脚本告诉 Redis: 只有 key 存在并且存储的值和我指定的值一样才 ...
前言
基于 Redis 的 Redisson 分布式联锁 RedissonMultiLock 对象可以将多个 RLock 对象关联为一个联锁,每个 RLock 对象实例可以来自于不同的 Redisson 实例。
当然,这是官网的介绍,具体是什么?一起看看联锁 MultiLock 使用以及源码吧!
MultiLock 使用
按照官方文档的说法,这里 Redisson 客户端可以不是同一个。当然,一般工作中也不会说不用一个客户端吧。
加锁
在阅读 MultiLock 加锁之前,小伙伴应该已经阅读过普通加锁的相关文章。
源码入口:org.redisson.RedissonMultiLock#lock()
默认超时时间 leaseTime 没有设置,所以为 -1。
这块方法太长,咱们拆分进行阅读。
基础等待时间 baseWaitTime = 锁数量 * 1500,在这里就是 4500 毫秒;
leaseTime == -1 所以 waitTime = baseWaitTime,也就是 4500;
while (true) 调用 tryLock 加锁,直到成功。
调用 tryLock 方 ...
redisson
未读前言
看门狗机制是在 RedissonBaseLock#scheduleExpirationRenewal 方法中,这块公平锁和非公平锁并无区别。
前文已经了解到,公平锁加锁失败之后,会将当前放到等待队列中,通过 Java 代码中的循环不断尝试获得锁。
锁释放
主动释放
源码:RedissonFairLock#unlockInnerAsync
KEYS[1]:加锁的名字,anyLock;
KEYS[2]:加锁等待队列,redisson_lock_queue:{anyLock};
KEYS[3]:等待队列中线程锁时间的 set 集合,redisson_lock_timeout:{anyLock},是按照锁的时间戳存放到集合中的;
KEYS[4]:redisson_lock__channel:{anyLock};
ARGV[1]:LockPubSub.UNLOCK_MESSAGE;
ARGV[2]:锁超时时间 30000;
ARGV[3]:UUID:ThreadId 组合 58f6c4a2-9908-4957-b229-283a ...
redisson
未读前言
在上一篇文章中已经分析过公平锁的加锁源码,并得出结论:
Redis Hash 数据结构:存放当前锁,Redis Key 就是锁,Hash 的 field 是加锁线程,Hash 的 value 是 重入次数;
Redis List 数据结构:充当线程等待队列,新的等待线程会使用 rpush 命令放在队列右边;
Redis sorted set 有序集合数据结构:存放等待线程的顺序,分数 score 用来是等待线程的超时时间戳。
现在看一下加锁失败被放到等待队列之后,线程是如何处理的?
排队等锁
源码入口:org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)。
线程进入排队之后,在 Java 代码中会 while (true) 一直循环调用 tryAcquire,尝试获取锁。
最终还是来到 RedissonFairLock#tryLockInnerAsync 方法中。
方便起见,重新贴一下 Lua 脚本,以及脚本的参数含义。
KEYS[1]:加锁的名字,anyLock;
KEYS ...
redisson
未读前言
默认的加锁逻辑是非公平的。
在加锁失败时,线程会进入 while 循环,一直尝试获得锁,这时候是多线程进行竞争。就是说谁抢到就是谁的。
Redisson 提供了 公平锁 机制,使用方式如下:
123RLock fairLock = redisson.getFairLock("anyLock");// 最常见的使用方法fairLock.lock();
下面一起看下公平锁是如何实现的?
公平锁
相信小伙伴们看过前面的文章,已经轻车熟路了,直接定位到源码方法:RedissonFairLock#tryLockInnerAsync。
好家伙,这一大块代码,我截图也截不完,咱们直接分析 lua 脚本。
PS:虽然咱不懂 lua,但是这一堆堆的 if else 咱们大概还是能看懂的。
因为 debug 发现 command == RedisCommands.EVAL_LONG,所以直接看下面一部分。
这么长,连呼好几声好家伙!
先来看看参数都有啥?
KEYS[1]:加锁的名字,anyLock;
KEYS[2]:加锁等待队列,redisson_lock_queue:& ...
redisson
未读前言
前面已经了解到了,可重入锁加锁,看门狗以及锁的互斥阻塞。
当锁加锁成功之后,锁是如何释放的?
主动释放
源码入口:RedissonLock#unlock
在解锁时会获取当前线程的id。
一路往里跟,直接来到 RedissonLock#unlockInnerAsync:
分析一下 lua 脚本的内容:
如果锁不存在,直接返回 null;
如果锁存在,则对锁的重入次数 -1;
剩余重入次数大于 0,重新设置过期时间,返回 0;
剩余重入次数不大于 0,删除 redis key 并发布消息,返回 1;
主动释放锁这块考虑的不仅仅是对 key 进行处理,因为可能存在重入锁,所以会先对 redis key 对应的 hash value 进行递减,相当于减去重入次数。
自动释放
相比较主动释放,自动释放就比较容易理解了。
当服务宕机时,看门狗不再看门,那么最多 30s 之后锁被自动释放;
当设置锁的时间时,锁到了时间,自动释放。
总结
Redisson 锁的释放分为两种:
主动释放:自己调用 API unlock 即可;
宕机/到期自动释放:Redis key 指定时 ...
redisson
未读前言
看过可重入锁的 Lua 脚本,已经可以知道当锁存在时,是会加锁失败的。
下面看一下,加锁失败之后是如何处理的呢?
加锁 Lua 脚本
在 lua 脚本中,前两段 if 分别排除了两种情况:
锁不存在;
锁存在且是自己线程(可重入);
剩下的情况就是锁存在,但是不是自己,也就意味着加锁失败。
执行 pttl 命令,返回锁的剩余时间。
加锁失败后的处理
源码定位:org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)
先来看开头一部分:
加锁成功后,会返回 ttl,此处会判断为 null,直接返回。
所以,下面的部分就是当获取锁失败之后的逻辑。
忽略掉不需要很关注的逻辑,重点则是 while (true) 里面这一小块。
一直循环调用 tryAcquire 方法,直到加锁成功!
总结
可重入锁的互斥是依靠 Redis Lua 脚本来保证的;
加锁失败会返回当前锁的剩余时间;
加锁失败后,会在 Java 代码中使用 while 循环一直尝试加锁。
大概的流程,如下图:
相关 ...