分布式锁——Redisson源码篇-RLock(二)

一、复习

场景模拟

初始化

在获取锁的时候,会初始化一些参数,例如commandExecutor leaseTime uuid

  • commandExecutor 命令执行器
  • leaseTime 持有锁的最大时间,租期
  • uuid 客户端的唯一标识

第一次加锁

  1. 设置获取到锁的主体,使用线程id作为标识
  2. 当未设置leaseTime的时候,也就是代表的锁可以无限持有时间
  1. 判断redis中是否已经存在了这个锁
  2. 第一次加锁肯定不存在,递增一个lockName为key的hash数据结构里面的一个key为uuid:threadId的元素
{
  "lockName": {
    "uuid:threadId": 1
  }
}
复制代码
  1. 给设置这个redis key设置一个过期时间 leaseTime,返回一个null
  2. 此时,就表明了客户端已经加锁成功了
  1. 收到监听加锁结果后,判断条件是ttl == null就表示这个客户端加锁成功
  2. 如果锁是有最大持有时长的,就会直接结束整个流程
  1. 如果是没有最大持有时长,会启动一个leaseTime/3 后执行的任务,任务会将锁的过期时间重新给设置为leaseTime,这个机制也被称之为watchdog
  2. 同时,本地还会维护一个watchdog需要监听的map,也就是每一个加锁成功且需要watchdog去设置过期时间的锁都会被记录在map中
{
  "uuid:lockName": {
    "timeout": timeout object,
    "threadIds": {
      "threadId": 1,
      "counter": number
    }
  }
}
复制代码

第二个其他线程来加锁: 互斥锁,锁竞争

  1. 获取当前线程的标识 threadId
  2. 第二个线程来加锁的时候,会发现一些不同的地方
  1. 判断redis中是否已经存在了这个锁
  2. 判断redis中这个key是否存在一个key为 uuid:threadId的元素
  1. 因为是其他线程,所以也不存在
  2. 会获取这个key的有效时间再返回出去
  1. 此时i,内部会判断ttl不为空,就会走进一个死循环
  2. 在循环内部会去持续的去获取锁
  1. 为了提高分布式锁的效率,就是在这个循环的内部是引入了信号量的数据结构的
  2. 这个时候,线程就阻塞住了

第三个线程来加锁:可重入锁

  1. 获取当前线程的标识threadId
  2. 判断redis中是否已经存在了这个锁
  1. 判断redis中这个key是否存在一个key为 uuid:threadId的元素
  2. 发现第二个条件是满足的
  1. 此时,会递增一下redis key的里面uuid:threadId的元素
  2. 重置过期时间为leaseTime
  1. 返回null,表示加锁成功
  2. 维护本地的map的时候,发现已经存在了当前key的元素,就会将里面的conter递增

释放锁

  1. 释放锁,其实和加锁的核心流程是类似的
  2. 先获取当前线程的threadId
  1. 去本地维护的map里,找到这个uuid:locakName删除这个threadId对应的map,如果key中元素threadIds已经是空了,就删除key
  2. 判断redis中lockName的key中是否存在一个key为uuid:threadId的元素
  1. 正常释放锁的情况下,会将redis中key对应的hash中的元素的值-1
  2. 判断递减之后的这个值是否是大于0的
  1. 如果大于零,也就是存在重入锁,就会做一个重置过期时间
  2. 否则就会直接删除redis key,还发了一个广播消息,不知道干啥的
  1. 返回1
  2. 本地处理只要返回的不是null就相当于释放锁成功,否则释放锁失败

获取锁后客户端宕机

  1. 当获取锁以后,客户端宕机了
  2. 此时,在客户端辛勤劳作的watchdog就会死掉
  1. 然后,等待redis中key的超时,其他客户端就会获取到锁

二、互斥、可重入、释放锁、锁超时和自动释放

互斥

通过在redis中维护一个hash结构,控制加锁的线程和机器不会重复

  • redis性能足够高,能支撑高并发
  • hash结构足够灵活

阻塞

如果一个锁获取失败的话,会在本地无限循环来获取锁,锁竞争都处于一个循环执行的状态

  • 同时引入了信号量避免并发过高的问题

可重入

可重入锁是为了避免出现死锁的情况,需要的一个特点

通过在redis和本地都维护了一个当前机器threadId的加锁次数

释放锁

本质上,释放锁其实就是加锁的反操作,会递减加锁次数,去删除redis中的key,本地map中的key

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
  return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
	redis.call('pexpire', KEYS[1], ARGV[2]);
	return 0;
else
  redis.call('del', KEYS[1]);
  redis.call('publish', KEYS[2], ARGV[1]);
  return 1;
end;
return nil;
复制代码

锁超时

就是加锁超时:waitTime

  • 获取当前时刻current
  • 尝试加锁后,如果加锁成功则直接返回,如果加锁失败则会waitTime减掉这个加锁过程的时间
  • 判断是否已经超时了,超时则失败
  • 后续会进入一个循环中去持续加锁
  • 只要尝试加一次锁,就要去计算一下这个时间

超时锁自动释放

就设置了leaseTime参数

  • 不会再去触发watchdog,等待redis中的锁超时后就会自动释放

总结

大图

图比较乱奥,但是应该能大概看得明白,比较核心的组件都画出来了

隐患

本质还是在集群中挑选一个master来加锁,实现了高可用机制,如果master宕机,slave会自动切换为master

如果master刚被写入一个锁,然后就宕机了,还没有被异步写入到slave中,slave就切换成了新的master,这样的话,其他线程就也会就获取锁成功

猜你喜欢

转载自juejin.im/post/7036320318993989645