关于redis:Redis-分布式锁的正确实现原理演化历程与-Redission-实战总结

5次阅读

共计 11813 个字符,预计需要花费 30 分钟才能阅读完成。

Redis 分布式锁应用 SET 指令就能够实现了么?在分布式畛域 CAP 实践始终存在。

分布式锁的门道可没那么简略,咱们在网上看到的分布式锁计划可能是有问题的。

「码哥」一步步带你深刻分布式锁是如何一步步欠缺,在高并发生产环境中如何正确应用分布式锁。

在进入注释之前,咱们先带着问题去思考:

  • 什么时候须要分布式锁?
  • 加、解锁的代码地位有考究么?
  • 如何避免出现锁再也无奈删除?「」
  • 超时工夫设置多少适合呢?
  • 如何防止锁被其余线程开释
  • 如何实现重入锁?
  • 主从架构会带来什么平安问题?
  • 什么是 Redlock
  • Redisson 分布式锁最佳实战
  • 看门狗实现原理
  • ……

什么时候用分布式锁?

码哥,说个艰深的例子解说下什么时候须要分布式锁呢?

诊所只有一个医生,很多患者前来就诊。

医生在同一时刻只能给一个患者提供就诊服务。如果不是这样的话,就会呈现医生在就诊肾亏的「肖菜鸡」筹备开药时候患者切换成了脚臭的「谢霸哥」,这时候药就被谢霸哥取走了。

治肾亏的药被有脚臭的拿去了。

当并发去读写一个【共享资源】的时候,咱们为了保证数据的正确,须要管制同一时刻只有一个线程拜访。

分布式锁就是用来管制同一时刻,只有一个 JVM 过程中的一个线程能够拜访被爱护的资源。

分布式锁入门

65 哥:分布式锁应该满足哪些个性?

  1. 互斥:在任何给定时刻,只有一个客户端能够持有锁;
  2. 无死锁:任何时刻都有可能取得锁,即便获取锁的客户端解体;
  3. 容错:只有大多数 Redis的节点都曾经启动,客户端就能够获取和开释锁。

码哥,我能够应用 SETNX key value 命令是实现「互斥」个性。

这个命令来自于 SET if Not eXists 的缩写,意思是:如果 key 不存在,则设置 value 给这个key,否则啥都不做。Redis 官网地址说的:

命令的返回值:

  • 1:设置胜利;
  • 0:key 没有设置胜利。

如下场景:

敲代码一天累了,想去放松按摩下肩颈。

168 号技师最热门,大家喜爱点,所以并发量大,须要分布式锁管制。

同一时刻只容许一个「客户」预约 168 技师。

肖菜鸡申请 168 技师胜利:

> SETNX lock:168 1
(integer) 1 # 获取 168 技师胜利

谢霸哥前面到,申请失败:

> SETNX lock 2
(integer) 0 # 客户谢霸哥 2 获取失败

此刻,申请胜利的客户就能够享受 168 技师的肩颈放松服务「共享资源」。

享受完结后,要及时开释锁,给后来者享受 168 技师的服务机会。

肖菜鸡,码哥考考你如何开释锁呢?

很简略,应用 DEL 删除这个 key 就行。

> DEL lock:168
(integer) 1

码哥,你见过「龙」么?我见过,因为我被一条龙服务过。

肖菜鸡,事件可没这么简略。

这个计划存在一个存在造成锁无奈开释的问题,造成该问题的场景如下:

  1. 客户端所在节点解体,无奈正确开释锁;
  2. 业务逻辑异样,无奈执行 DEL指令。

这样,这个锁就会始终占用,锁在我手里,我挂了,这样其余客户端再也拿不到这个锁了。

超时设置

码哥,我能够在获取锁胜利的时候设置一个「超时工夫」

比方设定按摩服务一次 60 分钟,那么在给这个 key 加锁的时候设置 60 分钟过期即可:

> SETNX lock:168 1  // 获取锁
(integer) 1
> EXPIRE lock:168 60  // 60s 主动删除
(integer) 1

这样,到点后锁主动开释,其余客户就能够持续享受 168 技师按摩服务了。

谁要这么写,就糟透了。

「加锁」、「设置超时」是两个命令,他们不是原子操作。

如果呈现只执行了第一条,第二条没机会执行就会呈现「超时工夫」设置失败,仍然呈现锁无奈开释。

码哥,那咋办,我想被一条龙服务,要解决这个问题

Redis 2.6.X 之后,官网拓展了 SET 命令的参数,满足了当 key 不存在则设置 value,同时设置超时工夫的语义,并且满足原子性。

SET resource_name random_value NX PX 30000
  • NX:示意只有 resource_name 不存在的时候能力 SET 胜利,从而保障只有一个客户端能够取得锁;
  • PX 30000:示意这个锁有一个 30 秒主动过期工夫。

这样写还不够,咱们还要避免不能开释不是本人加的锁。咱们能够在 value 上做文章。

持续往下看……

开释了不是本人加的锁

这样我能稳当的享受一条龙服务了么?

No,还有一种场景会导致 开释他人的锁

  1. 客户 1 获取锁胜利并设置设置 30 秒超时;
  2. 客户 1 因为一些起因导致执行很慢(网络问题、产生 FullGC……),过了 30 秒仍然没执行完,然而锁过期「主动开释了」;
  3. 客户 2 申请加锁胜利;
  4. 客户 1 执行实现,执行 DEL 开释锁指令,这个时候就把客户 2 的锁给开释了。

有个关键问题须要解决:本人的锁只能本人来开释。

我要如何删除是本人加的锁呢?

在执行 DEL 指令的时候,咱们要想方法查看下这个锁是不是本人加的锁再执行删除指令。

解铃还须系铃人

码哥,我在加锁的时候设置一个「惟一标识」作为 value 代表加锁的客户端。SET resource_name random_value NX PX 30000

在开释锁的时候,客户端将本人的「惟一标识」与锁上的「标识」比拟是否相等,匹配上则删除,否则没有权力开释锁。

伪代码如下:

// 比对 value 与 惟一标识
if (redis.get("lock:168").equals(random_value)){redis.del("lock:168"); // 比对胜利则删除
 }

有没有想过,这是 GET + DEL 指令组合而成的,这里又会波及到原子性问题。

咱们能够通过 Lua 脚本来实现,这样判断和删除的过程就是原子操作了。

// 获取锁的 value 与 ARGV[1] 是否匹配,匹配则执行 del
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这样通过惟一值设置成 value 标识加锁的客户端很重要,仅应用 DEL 是不平安的,因为一个客户端可能会删除另一个客户端的锁。

应用下面的脚本,每个锁都用一个随机字符串“签名”,只有当删除锁的客户端的“签名”与锁的 value 匹配的时候,才会删除它。

官网文档也是这么说的:https://redis.io/topics/distlock

这个计划曾经绝对完满,咱们用的最多的可能就是这个计划了。

正确设置锁超时

锁的超时工夫怎么计算适合呢?

这个工夫不能瞎写,个别要依据在测试环境屡次测试,而后压测多轮之后,比方计算出均匀执行工夫 200 ms。

那么锁的 超时工夫就放大为均匀执行工夫的 3~5 倍。

为啥要放放大呢?

因为如果锁的操作逻辑中有网络 IO 操作、JVM FullGC 等,线上的网络不会总一帆风顺,咱们要给网络抖动留有缓冲工夫。

那我设置更大一点,比方设置 1 小时不是更平安?

不要钻牛角,多大算大?

设置工夫过长,一旦产生宕机重启,就意味着 1 小时内,分布式锁的服务全副节点不可用。

你要让运维手动删除这个锁么?

只有运维真的不会打你。

有没有完满的计划呢?不论工夫怎么设置都不大适合。

咱们能够让取得锁的线程开启一个 守护线程,用来给快要过期的锁「续航」。

加锁的时候设置一个过期工夫,同时客户端开启一个「守护线程」,定时去检测这个锁的生效工夫。

如果快要过期,然而业务逻辑还没执行实现,主动对这个锁进行续期,从新设置过期工夫。

这个情理行得通,可我写不出。

别慌,曾经有一个库把这些工作都封装好了他叫 Redisson

在应用分布式锁时,它就采纳了「主动续期」的计划来防止锁过期,这个守护线程咱们个别也把它叫做「看门狗」线程。

一路优化下来,计划仿佛比拟「谨严」了,形象出对应的模型如下。

  1. 通过 SET lock_resource_name random_value NX PX expire_time,同时启动守护线程为快要过期但还没执行完的客户端的锁续命;
  2. 客户端执行业务逻辑操作共享资源;
  3. 通过 Lua 脚本开释锁,先 get 判断锁是否是本人加的,再执行 DEL

这个计划实际上曾经比拟完满,能写到这一步曾经战胜 90% 的程序猿了。

然而对于谋求极致的程序员来说还远远不够:

  1. 可重入锁如何实现?
  2. 主从架构解体复原导致锁失落如何解决?
  3. 客户端加锁的地位有门道么?

加解锁代码地位有考究

依据后面的剖析,咱们曾经有了一个「绝对谨严」的分布式锁了。

于是「谢霸哥」就写了如下代码将分布式锁使用到我的项目中,以下是伪代码逻辑:

public void doSomething() {redisLock.lock(); // 上锁
    try {
        // 解决业务
        .....
        redisLock.unlock(); // 开释锁} catch (Exception e) {e.printStackTrace();
    }
}

有没有想过:一旦执行业务逻辑过程中抛出异样,程序就无奈执行开释锁的流程。

所以开释锁的代码肯定要放在 finally{} 块中。

加锁的地位也有问题,放在 try 里面的话,如果执行 redisLock.lock() 加锁异样,然而理论指令曾经发送到服务端并执行,只是客户端读取响应超时,就会导致没有机会执行解锁的代码。

所以 redisLock.lock() 应该写在 try 代码块,这样保障肯定会执行解锁逻辑。

综上所述,正确代码地位如下:

public void doSomething() {
    try {
        // 上锁
        redisLock.lock();
        // 解决业务
        ...
    } catch (Exception e) {e.printStackTrace();
    } finally {
      // 开释锁
      redisLock.unlock();}
}

实现可重入锁

65 哥:可重入锁要如何实现呢?

当一个线程执行一段代码胜利获取锁之后,继续执行时,又遇到加锁的代码,可重入性就就保障线程能继续执行,而不可重入就是须要期待锁开释之后,再次获取锁胜利,能力持续往下执行。

用一段代码解释可重入:

public synchronized void a() {b();
}
public synchronized void b() {// pass}

假如 X 线程在 a 办法获取锁之后,继续执行 b 办法,如果此时 不可重入,线程就必须期待锁开释,再次争抢锁。

锁明明是被 X 线程领有,却还须要期待本人开释锁,而后再去抢锁,这看起来就很奇怪,我开释我本人~

Redis Hash 可重入锁

Redisson 类库就是通过 Redis Hash 来实现可重入锁

当线程领有锁之后,往后再遇到加锁办法,间接将加锁次数加 1,而后再执行办法逻辑。

退出加锁办法之后,加锁次数再减 1,当加锁次数为 0 时,锁才被真正的开释。

能够看到可重入锁最大个性就是计数,计算加锁的次数。

所以当可重入锁须要在分布式环境实现时,咱们也就须要统计加锁次数。

加锁逻辑

咱们能够应用 Redis hash 构造实现,key 示意被锁的共享资源,hash 构造的 fieldKey 的 value 则保留加锁的次数。

通过 Lua 脚本实现原子性,假如 KEYS1 =「lock」, ARGV「1000,uuid」:

---- 1 代表 true
---- 0 代表 false
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1;
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 1;
end ;
return 0;

加锁代码首先应用 Redis exists 命令判断以后 lock 这个锁是否存在。

如果锁不存在的话,间接应用 hincrby创立一个键为 lock hash 表,并且为 Hash 表中键为 uuid 初始化为 0,而后再次加 1,最初再设置过期工夫。

如果以后锁存在,则应用 hexists判断以后 lock 对应的 hash 表中是否存在 uuid 这个键,如果存在,再次应用 hincrby 加 1,最初再次设置过期工夫。

最初如果上述两个逻辑都不合乎,间接返回。

解锁逻辑

-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 0 代表 该可重入 key 不存在
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
    return nil;
end ;
-- 计算以后可重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
-- 小于等于 0 代表能够解锁
if (counter > 0) then
    return 0;
else
    redis.call('del', KEYS[1]);
    return 1;
end ;
return nil;

首先应用 hexists 判断 Redis Hash 表是否存给定的域。

如果 lock 对应 Hash 表不存在,或者 Hash 表不存在 uuid 这个 key,间接返回 nil

若存在的状况下,代表以后锁被其持有,首先应用 hincrby使可重入次数减 1,而后判断计算之后可重入次数,若小于等于 0,则应用 del 删除这把锁。

解锁代码执行形式与加锁相似,只不过解锁的执行后果返回类型应用 Long。这里之所以没有跟加锁一样应用 Boolean , 这是因为解锁 lua 脚本中,三个返回值含意如下:

  • 1 代表解锁胜利,锁被开释
  • 0 代表可重入次数被减 1
  • null 代表其余线程尝试解锁,解锁失败.

主从架构带来的问题

码哥,到这里分布式锁「很完满了」吧,没想到分布式锁这么多门道。

路还很远,之前剖析的场景都是,锁在「单个」Redis 实例中可能产生的问题,并没有波及到 Redis 主从模式导致的问题。

咱们通常应用「Cluster 集群」或者「哨兵集群」的模式部署保障高可用。

这两个模式都是基于「主从架构数据同步复制」实现的数据同步,而 Redis 的主从复制默认是异步的。

以下内容来自于官网文档 https://redis.io/topics/distlock

咱们试想下如下场景会产生什么问题:

  1. 客户端 A 在 master 节点获取锁胜利。
  2. 还没有把获取锁的信息同步到 slave 的时候,master 宕机。
  3. slave 被选举为新 master,这时候没有客户端 A 获取锁的数据。
  4. 客户端 B 就能胜利的取得客户端 A 持有的锁,违反了分布式锁定义的互斥。

尽管这个概率极低,然而咱们必须得抵赖这个危险的存在。

Redis 的作者提出了一种解决方案,叫 Redlock(红锁)

Redis 的作者为了对立分布式锁的规范,搞了一个 Redlock,算是 Redis 官网对于实现分布式锁的领导标准,https://redis.io/topics/distlock,然而这个 Redlock 也被国外的一些分布式专家给喷了。

因为它也不完满,有“破绽”。

什么是 Redlock

红锁是不是这个?

泡面吃多了你,Redlock 红锁是为了解决主从架构中当呈现主从切换导致多个客户端持有同一个锁而提出的一种算法。

大家能够看官网文档(https://redis.io/topics/distlock),以下来自官网文档的翻译。

想用应用 Redlock,官网倡议在不同机器上部署 5 个 Redis 主节点,节点都是齐全独立,也不应用主从复制,应用多个节点是为容错。

一个客户端要获取锁有 5 个步骤

  1. 客户端获取以后工夫 T1(毫秒级别);
  2. 应用雷同的 key value 程序尝试从 N Redis 实例上获取锁。

    • 每个申请都设置一个超时工夫(毫秒级别),该超时工夫要远小于锁的无效工夫,这样便于疾速尝试与下一个实例发送申请。
    • 比方锁的主动开释工夫 10s,则申请的超时工夫能够设置 5~50 毫秒内,这样能够避免客户端长时间阻塞。
  3. 客户端获取以后工夫 T2 并减去步骤 1 的 T1 来计算出获取锁所用的工夫(T3 = T2 -T1)。当且仅当客户端在大多数实例(N/2 + 1)获取胜利,且获取锁所用的总工夫 T3 小于锁的无效工夫,才认为加锁胜利,否则加锁失败。
  4. 如果第 3 步加锁胜利,则执行业务逻辑操作共享资源,key 的真正无效工夫等于无效工夫减去获取锁所应用的工夫(步骤 3 计算的后果)。
  5. 如果因为某些起因,获取锁失败(没有在至多 N/2+1 个 Redis 实例取到锁或者取锁工夫曾经超过了无效工夫),客户端应该在所有的 Redis 实例上进行解锁(即使某些 Redis 实例基本就没有加锁胜利)。

另外部署实例的数量要求是奇数,为了能很好的满足过半准则,如果是 6 台则须要 4 台获取锁胜利能力认为胜利,所以奇数更正当

事件可没这么简略,Redis 作者把这个计划提出后,受到了业界驰名的分布式系统专家的 质疑

两人好比神仙打架,两人一来一回论据短缺的对一个问题提出很多论断……

  • Martin Kleppmann 提出质疑的博客:https://martin.kleppmann.com/…
  • Redlock 设计者的回复:http://antirez.com/news/101

Redlock 是与非

Martin Kleppmann 认为锁定的目标是为了爱护对共享资源的读写,而分布式锁应该「高效」和「正确」。

  • 高效性:分布式锁应该要满足高效的性能,Redlock 算法向 5 个节点执行获取锁的逻辑性能不高,成本增加,复杂度也高;
  • 正确性:分布式锁应该避免并发过程在同一时刻只能有一个线程能对共享数据读写。

出于这两点,咱们没必要承当 Redlock 的老本和简单,运行 5 个 Redis 实例并判断加锁是否满足大多数才算胜利。

主从架构解体复原极小可能产生,这没什么大不了的。应用单机版就够了,Redlock 太重了,没必要。

Martin 认为 Redlock 基本达不到安全性的要求,也仍旧存在锁生效的问题!

Martin 的论断

  1. Redlock 不三不四:对于偏好效率来讲,Redlock 比拟重,没必要这么做,而对于偏好正确性来说,Redlock 是不够平安的。
  2. 时钟假如不合理:该算法对系统时钟做出了危险的假如(假如多个节点机器时钟都是统一的),如果不满足这些假如,锁就会生效。
  3. 无奈保障正确性:Redlock 不能提供相似 fencing token 的计划,所以解决不了正确性的问题。为了正确性,请应用有「共识零碎」的软件,例如 Zookeeper

Redis 作者 Antirez 的反驳

Redis 作者的反驳文章中,有 3 个重点:

  • 时钟问题:Redlock 并不需要完全一致的时钟,只须要大体一致就能够了,容许有「误差」,只有误差不要超过锁的租期即可,这种对于时钟的精度要求并不是很高,而且这也合乎事实环境。
  • 网络提早、过程暂停问题:

    • 客户端在拿到锁之前,无论经验什么耗时长问题,Redlock 都可能在第 3 步检测进去
    • 客户端在拿到锁之后,产生 NPC,那 Redlock、Zookeeper 都无能为力
  • 质疑 fencing token 机制。

对于 Redlock 的争执咱们下期再见,当初进入 Redisson 实现分布式锁实战局部。

Redisson 分布式锁

基于 SpringBoot starter 形式,增加 starter。

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.16.4</version>
</dependency>

不过这里须要留神 springboot 与 redisson 的版本,因为官网举荐 redisson 版本与 springboot 版本配合应用。

将 Redisson 与 Spring Boot 库集成,还取决于 Spring Data Redis 模块。

「码哥」应用 SpringBoot 2.5.x 版本,所以须要增加 redisson-spring-data-25。

<dependency>
  <groupId>org.redisson</groupId>
  <!-- for Spring Data Redis v.2.5.x -->
  <artifactId>redisson-spring-data-25</artifactId>
  <version>3.16.4</version>
</dependency>

增加配置文件

spring:
  redis:
    database: 
    host:
    port:
    password:
    ssl: 
    timeout:
    # 依据理论状况配置 cluster 或者哨兵
    cluster:
      nodes:
    sentinel:
      master:
      nodes:

就这样在 Spring 容器中咱们领有以下几个 Bean 能够应用:

  • RedissonClient
  • RedissonRxClient
  • RedissonReactiveClient
  • RedisTemplate
  • ReactiveRedisTemplate

基于 Redis 的 Redisson 分布式可重入锁 RLock Java 对象实现了java.util.concurrent.locks.Lock 接口。

失败有限重试

RLock lock = redisson.getLock("码哥字节");
try {

  // 1. 最罕用的第一种写法
  lock.lock();
  
  // 执行业务逻辑
  .....
  
} finally {lock.unlock();
}

拿锁失败时会不停的重试,具备 Watch Dog 主动延期机制,默认续 30s 每隔 30/3=10 秒续到 30s。

失败超时重试,主动续命

// 尝试拿锁 10s 后进行重试, 获取失败返回 false,具备 Watch Dog 主动延期机制,默认续 30s
boolean flag = lock.tryLock(10, TimeUnit.SECONDS); 

超时主动开释锁

// 没有 Watch Dog,10s 后主动开释, 不须要调用 unlock 开释锁。lock.lock(10, TimeUnit.SECONDS);

超时重试,主动解锁

// 尝试加锁,最多期待 100 秒,上锁当前 10 秒主动解锁, 没有 Watch dog
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {...} finally {lock.unlock();
   }
}

Watch Dog 主动延时

如果获取分布式锁的节点宕机,且这个锁还出于锁定状态,就会呈现死锁。

为了防止这个状况,咱们都会给锁设置一个超时主动开释工夫。

然而,还是会存在一个问题。

假如线程获取锁胜利,并设置了 30 s 超时,然而在 30s 内工作还没执行完,锁超时开释了,就会导致其余线程获取不该获取的锁。

所以,Redisson 提供了 watch dog 主动延时机制,提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被敞开前,一直的缩短锁的有效期。

也就是说,如果一个拿到锁的线程始终没有实现逻辑,那么看门狗会帮忙线程一直的缩短锁超时工夫,锁不会因为超时而被开释。

默认状况下,看门狗的续期工夫是 30s,也能够通过批改 Config.lockWatchdogTimeout 来另行指定。

另外 Redisson 还提供了能够指定 leaseTime 参数的加锁办法来指定加锁的工夫。

超过这个工夫后锁便主动解开了,不会缩短锁的有效期。

原理如下图:

有两个点须要留神:

  • watchDog 只有在未显示指定加锁超时工夫(leaseTime)时才会失效。
  • lockWatchdogTimeout 设定的工夫不要太小,比方设置的是 100 毫秒,因为网络间接导致加锁完后,watchdog 去延期时,这个 key 在 redis 中曾经被删除了。

源码导读

在调用 lock 办法时,会最终调用到 tryAcquireAsync。调用链为:lock()->tryAcquire->tryAcquireAsync`,具体解释如下:

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        // 如果指定了加锁工夫,会间接去加锁
        if (leaseTime != -1) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            // 没有指定加锁工夫 会先进行加锁,并且默认工夫就是 LockWatchdogTimeout 的工夫
            // 这个是异步操作 返回 RFuture 相似 netty 中的 future
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }

        // 这里也是相似 netty Future 的 addListener,在 future 内容执行实现后执行
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}

            // lock acquired
            if (ttlRemaining == null) {
                // leaseTime 不为 - 1 时,不会主动延期
                if (leaseTime != -1) {internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    // 这里是定时执行 以后锁主动延期的动作,leaseTime 为 - 1 时,才会主动延期
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

scheduleExpirationRenewal 中会调用 renewExpiration 启用了一个 timeout 定时,去执行延期动作。

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {return;}

        Timeout task = commandExecutor.getConnectionManager()
          .newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                // 省略局部代码
                ....

                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    ....

                    if (res) {
                        // 如果 没有报错,就再次定时延期
                        // reschedule itself
                        renewExpiration();} else {cancelExpirationRenewal(null);
                    }
                });
            }
            // 这里咱们能够看到定时工作 是 lockWatchdogTimeout 的 1 / 3 工夫去执行 renewExpirationAsync
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        ee.setTimeout(task);
    }

scheduleExpirationRenewal 会调用到 renewExpirationAsync,执行上面这段 lua 脚本。

他次要判断就是 这个锁是否在 redis 中存在,如果存在就进行 pexpire 延期。

protected RFuture<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then" +
                        "redis.call('pexpire', KEYS[1], ARGV[1]);" +
                        "return 1;" +
                        "end;" +
                        "return 0;",
                Collections.singletonList(getRawName()),
                internalLockLeaseTime, getLockName(threadId));
    }
  • watch dog 在以后节点还存活且工作未实现则每 10 s 给锁续期 30s。
  • 程序开释锁操作时因为异样没有被执行,那么锁无奈被开释,所以开释锁操作肯定要放到 finally {} 中;
  • 要使 watchLog 机制失效,lock 时 不要设置 过期工夫。
  • watchlog 的延时工夫 能够由 lockWatchdogTimeout 指定默认延时工夫,然而不要设置太小。
  • watchdog 会每 lockWatchdogTimeout/ 3 工夫,去延时。
  • 通过 lua 脚本实现提早。

总结

竣工,我倡议你合上屏幕,本人在脑子里从新过一遍,每一步都在做什么,为什么要做,解决什么问题。

咱们一起从头到尾梳理了一遍 Redis 分布式锁中的各种门道,其实很多点是不论用什么做分布式锁都会存在的问题,重要的是思考的过程。

对于零碎的设计,每个人的出发点都不一样,没有完满的架构,没有普适的架构,然而在完满和普适能均衡的很好的架构,就是好的架构。

正文完
 0