[转]分布式锁-RedisLockRegistry源码分析
前言
官网的英文介绍大概如下:
Starting with version 4.0, the RedisLockRegistry is available. Certain components (for example aggregator and resequencer) use a lock obtained from a LockRegistry instance to ensure that only one thread is manipulating a group at a time. The DefaultLockRegistry performs this function within a single component; you can now configure an external lock registry on these components. When used with a shared MessageGroupStore, the RedisLockRegistry can be use to provide this functionality across multiple application instances, such that only one instance can manipulate the group at a time.
When a lock is released by a local thread, another local thread will generally be able to acquire the lock immediately. If a lock is released by a thread using a different registry instance, it can take up to 100ms to acquire the lock.
To avoid "hung" locks (when a server fails), the locks in this registry are expired after a default 60 seconds, but this can be configured on the registry. Locks are normally held for a much smaller time.
上述大概意思是RedisLockRegistry可以确保在分布式环境中,只有一个thread在执行,也就是实现了分布式锁,当一个本地线程释放了锁,其他本地现场会立即去抢占锁,如果锁被占用了,那么会进行重试机制,100毫秒进行重试一次。同时也避免了"hung" locks 当服务器fails的时候。同时也给锁设置了默认60秒的过期时间
如何获取锁
详细流程如上图所示,这里主要核心业务是这样,首先Lock是java.util.concurrent.locks中的锁,也就是本地锁。然后自己用RedisLock实现了Lock接口而已,但是实际上RedisLock也使用了本地锁。主要是通过redis锁+本地锁双重锁的方式实现的一个比较好的锁。针对redis锁来说只要能获取到锁,那么就算是成功的。如果获取不到锁就等待100毫秒继续重试,如果获取到锁那么就采用本地锁锁住本地的线程。通过两种方式很好的去实现了一个完善的分布式锁机制。
下面代码主要是获取锁的一个流程,先从本地锁里面获取,如果获取到了那么和redis里面存放的RedisLock锁做对比,判断是否是同一个对象,如果不是那么就删除本地锁然后重新创建一个锁返回
@Override
public Lock obtain(Object lockKey) {
Assert.isInstanceOf(String.class, lockKey);
//try to find the lock within hard references
//从本地强引用里面获取锁,
RedisLock lock = findLock(this.hardThreadLocks.get(), lockKey);
/*
* If the lock is locked, check that it matches what's in the store.
* If it doesn't, the lock must have expired.
*/
//这里主要判断了这个锁是否是锁住的,如果不是的那么该锁已经过期了
//如果强引用里面有这个锁,并且lock.thread!=null,说明这个锁没有被占用
if (lock != null && lock.thread != null) {
//从redis获取锁,若如果redis锁为空或者跟当前强引用的锁不一致,可以确定两个问题
//1.redis里面的锁和本地的锁不是一个了
//2.redis里面没有锁
RedisLock lockInStore = this.redisTemplate.boundValueOps(this.registryKey + ":" + lockKey).get();
if (lockInStore == null || !lock.equals(lockInStore)) {
//删除强引用里面锁
getHardThreadLocks().remove(lock);
lock = null;
}
}
//如果锁==null
if (lock == null) {
//try to find the lock within weak references
//尝试线从弱引用里面去找锁
lock = findLock(this.weakThreadLocks.get(), lockKey);
//如果弱引用锁==null 那么新建一个锁
if (lock == null) {
lock = new RedisLock((String) lockKey);
//判断是否用弱引用,如果用那么就加入到弱引用里面
if (this.useWeakReferences) {
getWeakThreadLocks().add(lock);
}
}
}
return lock;
}
上面获取到的是RedisLock,RedisLock是实现java原生Lock接口,并重写了lock()方法。首先从localRegistry中获取到锁,这里的锁是java开发包里面的ReentrantLock。首先把本地先锁住,然后再去远程obtainLock。每次sleep() 100毫秒直到获取到远程锁为止,代码如下所示:
@Override
public void lock() {
//这里采用java开发包里面的ReentrantLock 进行多线程的加锁,单机多线程的情况下解决并发的问题
Lock localLock = RedisLockRegistry.this.localRegistry.obtain(this.lockKey);
localLock.lock();
while (true) {
try {
while (!this.obtainLock()) {
Thread.sleep(100); //NOSONAR
}
break;
}
catch (InterruptedException e) {
/*
* This method must be uninterruptible so catch and ignore
* interrupts and only break out of the while loop when
* we get the lock.
*/
}
catch (Exception e) {
localLock.unlock();
rethrowAsLockException(e);
}
}
}
核心远程锁还是在RedisLock中,这里采用了redis事务+watch的方式,watch和事务都是redis里面自带的。使用watch时候如果key的值发生了任何变化。那么exec()将不会执行,那么如下代码返回的success就是false。从而来实现redis锁的功能
private boolean obtainLock() {
//判断创建这个类的线程和当前是否是一个,如果是就直接获取锁
Thread currentThread = Thread.currentThread();
if (currentThread.equals(this.thread)) {
this.reLock++;
return true;
}
//把当前锁存到集合种
toHardThreadStorage(this);
/*
* Set these now so they will be persisted if successful.
*/
this.lockedAt = System.currentTimeMillis();
this.threadName = currentThread.getName();
Boolean success = false;
try {
success = RedisLockRegistry.this.redisTemplate.execute(new SessionCallback<Boolean>() {
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Boolean execute(RedisOperations ops) throws DataAccessException {
String key = constructLockKey();
//监控key如果该key被改变了 那么该事务是不能被实现的会进行回滚
ops.watch(key); //monitor key
//如果key存在了就停止监控,如果key已经存在了 那么肯定是被别人占用了
if (ops.opsForValue().get(key) != null) {
ops.unwatch(); //key already exists, stop monitoring
return false;
}
ops.multi(); //transaction start
//设置一个值并加上过期时间 m默认是一分钟左右的时间
//set the value and expire
//把锁放入到redis中
ops.opsForValue()
.set(key, RedisLock.this, RedisLockRegistry.this.expireAfter, TimeUnit.MILLISECONDS);
//exec will contain all operations result or null - if execution has been aborted due to 'watch'
return ops.exec() != null;
}
});
}
finally {
//如果不成功那么把当前过期时间和锁的名字设置成null
if (!success) {
this.lockedAt = 0;
this.threadName = null;
toWeakThreadStorage(this);
}
else {
//如果成功把当前锁的thread名称设置成currentThread
this.thread = currentThread;
if (logger.isDebugEnabled()) {
logger.debug("New lock; " + this.toString());
}
}
}
return success;
}
上面是整个加锁的流程,基本流程比较简单,看完加锁应该自己都能解锁,无非就是去除redis锁和本地的锁而已。
@Override
public void unlock() {
//判断当前运行的线程和锁的线程做对比,如果两个线程不一样那么抛出异常
if (!Thread.currentThread().equals(this.thread)) {
if (this.thread == null) {
throw new IllegalStateException("Lock is not locked; " + this.toString());
}
throw new IllegalStateException("Lock is owned by " + this.thread.getName() + "; " + this.toString());
}
try {
//如果reLock--小于=0的话就删除redis里面的锁
if (this.reLock-- <= 0) {
try {
this.assertLockInRedisIsUnchanged();
RedisLockRegistry.this.redisTemplate.delete(constructLockKey());
if (logger.isDebugEnabled()) {
logger.debug("Released lock; " + this.toString());
}
}
finally {
this.thread = null;
this.reLock = 0;
toWeakThreadStorage(this);
}
}
}
finally {
//拿到本地锁,进行解锁
Lock localLock = RedisLockRegistry.this.localRegistry.obtain(this.lockKey);
localLock.unlock();
}
}
tryLock在原有的加锁上面增加了一个超时机制,主要是先通过本地的超时机制
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
//拿到本地锁
Lock localLock = RedisLockRegistry.this.localRegistry.obtain(this.lockKey);
//先本地锁进行tryLock
if (!localLock.tryLock(time, unit)) {
return false;
}
try {
long expire = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(time, unit);
boolean acquired;
//这里添加了超时机制,跟之前的无限等待做了一个区分
while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR
Thread.sleep(100); //NOSONAR
}
//超时后没有获取到锁,那么就把本地锁进行解锁
if (!acquired) {
localLock.unlock();
}
return acquired;
}
catch (Exception e) {
localLock.unlock();
rethrowAsLockException(e);
}
return false;
}
[转]分布式锁-RedisLockRegistry源码分析的更多相关文章
- Laravel Redis分布式锁实现源码分析
首先是锁的抽象类,定义了继承的类必须实现加锁.释放锁.返回锁拥有者的方法. namespace Illuminate\Cache; abstract class Lock implements Loc ...
- ReentrantLock 锁释放源码分析
ReentrantLock 锁释放源码分析: 调用的是unlock 的方法: public void unlock() { sync.release(1); } 接下来分析release() 方法: ...
- ZooKeeper 分布式锁 Curator 源码 02:可重入锁重复加锁和锁释放
ZooKeeper 分布式锁 Curator 源码 02:可重入锁重复加锁和锁释放 前言 加锁逻辑已经介绍完毕,那当一个线程重复加锁是如何处理的呢? 锁重入 在上一小节中,可以看到加锁的过程,再回头看 ...
- ZooKeeper 分布式锁 Curator 源码 03:可重入锁并发加锁
前言 在了解了加锁和锁重入之后,最需要了解的还是在分布式场景下或者多线程并发加锁是如何处理的? 并发加锁 先来看结果,在多线程对 /locks/lock_01 加锁时,是在后面又创建了新的临时节点. ...
- ZooKeeper 分布式锁 Curator 源码 04:分布式信号量和互斥锁
前言 分布式信号量,之前在 Redisson 中也介绍过,Redisson 的信号量是将计数维护在 Redis 中的,那现在来看一下 Curator 是如何基于 ZooKeeper 实现信号量的. 使 ...
- ZooKeeper 分布式锁 Curator 源码 01:可重入锁
前言 一般工作中常用的分布式锁,就是基于 Redis 和 ZooKeeper,前面已经介绍完了 Redisson 锁相关的源码,下面一起看看基于 ZooKeeper 的锁.也就是 Curator 这个 ...
- concurrent(三)互斥锁ReentrantLock & 源码分析
参考文档:Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock:http://www.cnblogs.com/skywang12345/p/3496101.html Reentr ...
- 又长又细,万字长文带你解读Redisson分布式锁的源码
前言 上一篇文章写了Redis分布式锁的原理和缺陷,觉得有些不过瘾,只是简单的介绍了下Redisson这个框架,具体的原理什么的还没说过呢.趁年前项目忙的差不多了,反正闲着也是闲着,不如把Rediss ...
- TX-LCN5.0.2分布式事务框架源码分析-关键线索罗列-txc部分
1.注解TxcTransaction2.在其注解接口附近查找aop配置:TransactionAspect3.runTransaction是在执行事务业务代码时的包装逻辑4.transactionSe ...
随机推荐
- http://www.rabbitmq.com/
什么是RabbitMQ 官网http://www.rabbitmq.com/ 1.应用程序间健壮的消息发送 2.简单易用 3.可在所有主流操作系统运行 4.支持巨量的开发者平台 5.开源和商用双重支持 ...
- linux每天一小步---xargs命令详解
1 命令功能 xargs用来从标准输入中执行命令行 xargs命令用来将一些不支持管道传递参数的命令而使之支持 2 命令语法 xargs [选项参数] commands 3 命令参数 -O 当标准输 ...
- Shell编程-04-Shell中变量数值计算
目录 算术运算符 算术运算命令 数值运算用法 算术运算符 在任何一门形式的语言中均会存在算术运算的情况,Shell常见的运算符如下所示: 运算符 含义 + - * / % 加 减 乘 除 求余 ...
- MSP430 G2553 比较器Comparator_A+、数据流程图DFD、状态转换图STD
一.CA+构造 MSP430G2553带有一个比较器Comparator_A+(CA+),其构造框图如下图所示. 二.输入 & 输出 如上图所示,比较器有一个同向输入端(V+)和一个反向输入端 ...
- TSQL--TOP选项
TOP选项需要依据ORDER来选取记录,可以依据行数和百分比来选取记录 按照行数来选取10行记录 SELECT TOP(10) * FROM T1 ORDER BY ID 按照行数来选取10%的记录 ...
- python实现注册登录小程序
用python 实现模拟注册和登录的程序:用户信息最终以字典的格式储存在一个txt文件里,具体实现如下: users.txt里用户字典格式如下: { '}, '}, '} } # 注册 f = ope ...
- 手动安装httpd服务器
首先安装apr(Apache Portable Runtime) apr-util apr-iconv 安装之前需要 前置知识: 自己手动编译安装的软件的安装位置: /usr/local bin, s ...
- 10-12Linux流编程的一些知识点
第五章 Linux 的流编程 Linux流操作基础 流和文件的关系:流相当于一个缓冲区,可以将文件描述符和流关联,获得相应的缓冲区,以此来提高系统对磁盘的存取速度. 流的结构和操作 ...
- Cesium开发实践汇总
一.简介.开发环境搭建 二.Viewer控件 三.地图图层介绍 四.地形介绍 五.坐标变换 六.CZML 七.3D模型
- WindowsPhone模拟简易Toast弹出框
Coding4Fun这个开源控件中有ToastPrompt这个弹出框组件,但是由于Coding4Fun太庞大,如果只用到ToastPrompt这个控件的话,整个引用不太值当的.于是自己写了一个差不多的 ...