Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端
开心一刻
一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解
劝解员:兄弟,别跳
跳楼人:我不想活了
劝解员:你想想你媳妇
跳楼人:媳妇跟人跑了
劝解员:你还有兄弟
跳楼人:就是跟我兄弟跑的
劝解员:你想想你家孩子
跳楼人:孩子是他俩的
劝解员:死吧,妈的你活着也没啥价值了
前言
关于锁,相信大家都不陌生,一般我们用其在多线程环境中控制对共享资源的并发访问
单服务下,用 JDK 中的 synchronized 或 Lock 的实现类可实现对共享资源的并发访问
分布式服务下,JDK 中的锁就显得力不从心了,分布式锁也就应运而生了
分布式锁的实现方式有很多,常见的有如下几种
基于 MySQL,利用行级悲观锁(select ... for update)
基于 Redis,利用其 (setnx + expire) 或 set
基于 Zookeeper,利用其临时目录和事件回调机制
具体的实现细节就不展开了,网上资料很多
看下文之前最好先看下:Redisson 分布式锁实现之前置篇 → Redis 的发布/订阅 与 Lua,方便更好的理解下文
分布式锁的特点
可以类比 JDK 中的锁
互斥
不仅要保证同个服务中不同线程的互斥,还需要保证不同服务间、不同线程的互斥
如何处理互斥,是自旋、还是阻塞 ,还是其他 ?
超时
锁超时设置,防止程序异常奔溃而导致锁一直存在,后续同把锁一直加不上
续期
程序具体执行的时长无法确定,所以过期时间只能是个估值,那么就不能保证程序在过期时间内百分百能运行完
所以需要进行锁续期,保证业务能够正常执行完
可重入
可重入锁又名递归锁,是指同一个线程在外层方法已经获得锁,再进入该线程的中层或内层方法会自动获取锁
简单点来说,就是同个线程可以反复获取同一把锁
专一释放
通俗点来讲:谁加的锁就只有它能释放这把锁
为什么会出现这种错乱释放的问题了,举个例子就理解了
线程 T1 对资源 lock_zhangsan 加了锁,由于某些原因,加锁业务还未执行完,锁过期自动释放了,此时线程 T2 对资源 lock_zhangsan 加锁成功
T2 执行业务的时候,T1 业务执行完后释放资源 lock_zhangsan 的锁,结果把 T2 加的锁给释放了
公平与非公平
公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁
非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁
JDK 中的 ReentrantLock 就有公平和非公平两种实现,有兴趣的可以去看看它的源码
多数情况下用的是非公平锁,但有些特殊情况下需要用公平锁
很多小伙伴觉得:引入一个简单的分布式锁,有必要考虑这么多吗?
虽然绝大部分情况下,我们的程序都是在跑正常流程,但不能保证异常情况 100% 跑不到,出于健壮性考虑,异常情况都需要考虑到
下面我们就来看看 Redisson 是如何实现这些特点的
Redisson 实现分布式锁
关于 Redisson,更多详细信息可查看官方文档
Redisson 是 Redis 官方推荐的 Java 版的 Redis 客户端,它提供了非常丰富的功能,其中就包括本文关注的分布式锁
环境准备
简单示例开始之前,我们先看下环境;版本不同,会有一些差别
JDK:1.8
Redis:3.2.8
Redisson:3.13.6
简单示例
先将 Redis 信息配置给 Redisson,创建出 RedissonClient
Redis 的部署方式不同,Redisson 配置模式也会不同,详细信息可查看:Configuration
我们就配置最简单的 Single instance mode
RedissonClient 创建出来后,就可以通过它来获取锁
完整示例代码:redisson-demo
接下来我们从源码层面一起看看 Redisson 具体是如何实现分布式锁的特点的
客户端创建
客服端的创建过程中,会生成一个 id 作为唯一标识,用以区分分布式下不同节点中的客户端
id 值就是一个 UUID,客户端启动时生成
那么这个 id 有什么用,大家暂且在脑中留下这个疑问,我们接着往下看
锁的获取
我们从 lock 开始跟源码
最终会来到有三个参数的 lock 方法
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId(); // 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
} // 锁被其他线程占用而获取失败,使用redis的发布订阅功能来等待锁的释放通知,而非自旋监测锁的释放
RFuture<RedissonLockEntry> future = subscribe(threadId); // 当前线程会阻塞,直到锁被释放时当前线程被唤醒(有超时等待,默认 7.5s,而不会一直等待)
// 持有锁的线程释放锁之后,redis会发布消息,所有等待该锁的线程都会被唤醒,包括当前线程
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
} try {
while (true) {
// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
} // waiting for message
if (ttl >= 0) {
try {
// future.getNow().getLatch() 返回的是 Semaphore 对象,其初始许可证为 0,以此来控制线程获取锁的顺序
// 通过 Semaphore 控制当前服务节点竞争锁的线程数量
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 退出锁竞争(锁获取成功或者放弃获取锁),则取消锁的释放订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
主要是三个点:尝试获取锁、订阅、取消订阅;我们一个一个来看
尝试获取锁
尝试获取锁主要做了两件事:1、尝试获取锁,2、锁续期
尝试获取锁主要涉及到一段 lua 代码
结合我的上篇文章来看,这个 lua 脚本还是很好理解的
1、用 exists 判断 key 不存在,则用 hash 结构来存放锁,key = 资源名,field = uuid + : + threadId,value 自增 1
设置锁的过期时间(默认是 lockWatchdogTimeout = 30 * 1000 毫秒),并返回 nil
2、用 hexists 判断 field = uuid + : + threadId 存在
则该 field 的 value 自增 1,并重置过期时间,最后返回 nil
这里相当于实现了锁的重入
3、上面两种情况都不满足,则说明锁被其他线程占用了,直接返回锁的过期时间
这里有个疑问:为什么 field = uuid + : + threadId,而不是 field = threadId
友情提示下:从多个服务(也就是多个 Redisson 客户端)来考虑
这个问题想清楚了,那么前面提到的:在 Redisson 客户端创建的过程中生成的 id(一个随机的 uuid 值),它的作用也就清楚了
在获取锁成功之后,会启一个定时任务实现锁续期,也涉及到一段 lua 脚本
这段脚本很简单,相信大家都能看懂
默认情况下,锁的过期时间是 30s,锁获取成功之后每隔 10s 进行一次锁续期,重置过期时间成 30s
若锁已经被释放了,则定时任务也会停止,不会再续期
订阅
获取锁的过程中,尝试获取锁失败(锁被其他线程锁占有),则会完成对该锁频道的订阅,订阅过程中线程会阻塞
持有锁的线程释放锁时会向锁频道发布消息,订阅了该锁频道的线程会被唤醒,继续去获取锁
这里有个疑问:假设持有锁的线程意外停止了,未向锁频道发布消息,那订阅了锁频道的线程该如何唤醒
Redisson 其实已经考虑到了
有超时机制,默认超时时长 = 3000 + 1500 * 3 = 7500 毫秒
再提个问题:为什么要用 Redis 的发布订阅
假设我们不用 Redis 的发布订阅,我们该如何实现,自旋?
自旋有什么缺点? 自旋频率难以掌控,太高会增大 CPU 的负担,太低会不及时(锁都释放半天了才检测到)
可以类比 生产者与消费者 来考虑这个问题
取消订阅
有订阅,肯定就有取消订阅;当阻塞的线程被唤醒并获取到锁时需要取消对锁频道的订阅
当然,取消获取锁的线程也需要取消对锁频道的订阅
比较好理解,就是取消当前线程对锁频道的订阅
锁的释放
我们从 unlock 开始
代码比较简单,我们继续往下跟
主要有两点:1、锁释放,2、取消续期定时任务
锁释放
重点在于一个 lua 脚本
我们把参数具象化,脚本就好理解了
KEYS[1] = 锁资源,KEYS[2] = 锁频道
ARGV[1] = 锁频道消息类型,ARGV[2] = 过期时间,ARGV[3] = uuid + : + threadId
1、如果当前线程未持有锁,直接返回 nil
2、hash 结构的 field 的 value 自减 1,counter = 自减后的 value 值
如果 counter > 0,表示线程重入了,重置锁的过期时间,返回 0
如果 counter <= 0,删除锁,并对锁频道发布锁释放消息(频道订阅者则可收到消息,然后唤醒线程去获取锁),返回 1
3、上面 1、2 都不满足,则直接返回 nil
两个细节:1、重入锁的释放,2、锁彻底释放后的消息发布
取消续期定时任务
比较简单,没什么好说的
总结
我们从分布式锁的特点出发,来总结下 Redisson 是如何实现这些特点的
互斥
Redisson 采用 hash 结构来存锁资源,通过 lua 脚本对锁资源进行操作,保证线程之间的互斥
互斥之后,未获取到锁的线程会订阅锁频道,然后进入一定时长的阻塞
超时
有超时设置,给 hash 结构的 key 加上过期时间,默认是 30s
续期
线程获取到锁之后会开启一个定时任务(watchdog),每隔一定时间(默认 10s)重置 key 的过期时间
可重入
通过 hash 结构解决,key 是锁资源,field 是持有锁的线程,value 表示重入次数
专一释放
通过 hash 结构解决,field 中存放了线程信息,释放的时候就能够知道是不是线程加上的锁,是才能够进行锁释放
公平与非公平
留给大家补充
参考
拜托,面试请不要再问我Redis分布式锁的实现原理!【石杉的架构笔记】
Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端的更多相关文章
- Redisson 分布式锁源码 09:RedLock 红锁的故事
前言 RedLock 红锁,是分布式锁中必须要了解的一个概念. 所以本文会先介绍什么是 RedLock,当大家对 RedLock 有一个基本的了解.然后再看 Redisson 中是如何实现 RedLo ...
- 又长又细,万字长文带你解读Redisson分布式锁的源码
前言 上一篇文章写了Redis分布式锁的原理和缺陷,觉得有些不过瘾,只是简单的介绍了下Redisson这个框架,具体的原理什么的还没说过呢.趁年前项目忙的差不多了,反正闲着也是闲着,不如把Rediss ...
- Redisson 分布式锁源码 02:看门狗
前言 说起 Redisson,比较耳熟能详的就是这个看门狗(Watchdog)机制. 本文就一起看看加锁成功之后的看门狗(Watchdog)是如何实现的? 加锁成功 在前一篇文章中介绍了可重入锁加锁的 ...
- Redisson 分布式锁源码 11:Semaphore 和 CountDownLatch
前言 Redisson 除了提供了分布式锁之外,还额外提供了同步组件,Semaphore 和 CountDownLatch. Semaphore 意思就是在分布式场景下,只有 3 个凭证,也就意味着同 ...
- Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析
原文:Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析 一.RedissonLock#lock 源码分析 1.根据锁key计算出 slot,一个slot对 ...
- Redisson 分布式锁实现之前置篇 → Redis 的发布/订阅 与 Lua
开心一刻 我找了个女朋友,挺丑的那一种,她也知道自己丑,平常都不好意思和我一块出门 昨晚,我带她逛超市,听到有两个人在我们背后小声嘀咕:"看咱前面,想不到这么丑都有人要." 女朋友 ...
- Redisson分布式锁的简单使用
一:前言 我在实际环境中遇到了这样一种问题,分布式生成id的问题!因为业务逻辑的问题,我有个生成id的方法,是根据业务标识+id当做唯一的值! 而uuid是递增生成的,从1开始一直递增,那么在同一台机 ...
- Redisson 分布式锁实战与 watch dog 机制解读
Redisson 分布式锁实战与 watch dog 机制解读 目录 Redisson 分布式锁实战与 watch dog 机制解读 背景 普通的 Redis 分布式锁的缺陷 Redisson 提供的 ...
- Redisson分布式锁实现
转: Redisson分布式锁实现 2018年09月07日 15:30:32 校长我错了 阅读数:3303 转:分布式锁和Redisson实现 概述 分布式系统有一个著名的理论CAP,指在一个分布 ...
随机推荐
- 24.Collection集合
1.Collection集合 1.1数组和集合的区别[理解] 相同点 都是容器,可以存储多个数据 不同点 数组的长度是不可变的,集合的长度是可变的 数组可以存基本数据类型和引用数据类型 集合只能存引用 ...
- Linux ll查看文件属性详解-软硬链接详解
Linux文件属性及类型 [root@localhost ~]# ll anaconda-ks.cfg 文件类型 权限 硬连接数 文件的大小 文件的创建,修改时间 - rw-------. 1 roo ...
- Redis 为什么使用跳跃表
引言 跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的. 什么是跳跃表 对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个 ...
- make clean 和 make distclean区别-(转自秋水Leo)
make clean仅仅是清除之前编译的可执行文件及配置文件. 而make distclean要清除所有生成的文件. Makefile 在符合GNU Makefiel惯例的Makefile中,包含了一 ...
- Centos6.9以下查看端口占用情况和开启端口命令
Centos查看端口占用情况命令,比如查看80端口占用情况使用如下命令: lsof -i tcp:80 列出所有端口 netstat -ntlp 1.开启端口(以80端口为例) ...
- 继承(extends), 多态 , 抽象(abstract)接口() 易混难点解析
特性 java是单继承的,一个类直接继承的父类只能有唯一的一个 java中父类可以有多个子类 Object是所有类的父类,一个类没有父类则默认继承Object; 继承中的重写 子类重写方法访问权限不能 ...
- git&nodejs安装教程
git https://www.cnblogs.com/ximiaomiao/p/7140456.html nodejs https://jingyan.baidu.com/article/e7505 ...
- 在ssm框架测试中解决javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException
在单元测试发现causeBy:javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException 经发现是db.p ...
- openresty 学习笔记六:使用session库
openresty 学习笔记六:使用session库 lua-resty-session 是一个面向 OpenResty 的安全和灵活的 session 库,它实现了 Secure Cookie Pr ...
- 4D毫米波雷达Radar
4D毫米波雷达Radar 围绕雷达.激光雷达.高精定位等新一代传感器技术将会进入量产周期. 自动驾驶公司的竞争,在传感器配置上坦白说并没有太多差异化.除了车载激光雷达属于近几年的产物,类似摄像头.毫米 ...