网站没做好能不能备案,wordpress 结构解析,义乌推广公司,wordpress 为什么流行文章目录 Redisson 是什么Redisson 使用客户端模式单节点模式哨兵模式主从模式集群模式Spring Boot 整合 Redisson 中的锁Redisson 可重入锁Redisson 公平锁Redisson 联锁Redisson 读写锁Redisson Redlock Redisson 的看门狗机制RedLock 解决单体故障问题如何使用 RedLockMarti… 文章目录 Redisson 是什么Redisson 使用客户端模式单节点模式哨兵模式主从模式集群模式Spring Boot 整合 Redisson 中的锁Redisson 可重入锁Redisson 公平锁Redisson 联锁Redisson 读写锁Redisson Redlock Redisson 的看门狗机制RedLock 解决单体故障问题如何使用 RedLockMartin 对于 Relock 的质疑使用分布式锁的目的锁在分布式系统中遇到的问题时钟不正确导致的问题fecing token 方案 Antirez 的反驳时钟问题线程暂停问题fecing token 方案 RedLock 被弃用了 相信大部分同学都使用过 Redisson 来操作 Redis尤其是用它来实现分布式锁但是有些小伙伴可能对 Redisson 实现分布式锁的原理不是很清楚只知道怎么用如何用但是不清楚为什么要这么用这篇文章就 Redisson 实现分布式锁讲透一篇文章让你彻彻底底了解其核心原理。
Redisson 是什么
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格In-Memory Data Grid及基于Redis 实现的分布式工具集合。它不仅提供了一系列的分布式的Java常用对象还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)等还提供了许多分布式服务。
Redisson 的宗旨是促进使用者对 Redis 的关注分离Separation of Concern从而让使用者能够将精力更集中地放在处理业务逻辑上为每个试图再造分布式轮子的程序员带来了大部分分布式问题的解决办法。
功能特性
支持 Redis 单节点(single)模式、哨兵(sentinel)模式、主从(Master/Slave)模式以及集群(Redis Cluster)模式程序接口调用方式采用异步执行和异步流执行两种方式。数据序列化Redisson 的对象编码类是用于将对象进行序列化和反序列化以实现对该对象在 Redis 里的读取和存储。单个集合数据分片在集群模式下Redisson 为单个 Redis 集合类型提供了自动分片的功能。提供多种分布式对象如Object BucketBitsetAtomicLongBloom Filter 和 HyperLogLog 等。提供丰富的分布式集合如MapMultimapSetSortedSetListDequeQueue等。分布式锁和同步器的实现可重入锁(Reentrant Lock)公平锁(Fair Lock)联锁(MultiLock)红锁(Red Lock)信号量(Semaphore)可过期性信号锁(PermitExpirableSemaphore)等。提供先进的分布式服务如分布式远程服务(Remote Service)分布式实时对象(Live Object)服务分布式执行服务(Executor Service)分布式调度任务服务(Schedule Service)和分布式映射归纳服务(MapReduce)。
Redisson 使用
客户端模式
引入依赖
dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.27.2/version
/dependency获取 RedissonClient。RedissonClient有多种模式主要的模式有 单节点模式哨兵模式主从模式集群模式
单节点模式
程序化配置方法
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson Redisson.create();Config config new Config();
config.useSingleServer().setAddress(myredisserver:6379);
RedissonClient redisson Redisson.create(config);配置参数
SingleServerConfig singleConfig config.useSingleServer();具体的参数配置github.com/redisson/re…
哨兵模式
程序化配置哨兵模式的方法如下
Config config new Config();
config.useSentinelServers().setMasterName(mymaster)//可以用rediss://来启用SSL连接.addSentinelAddress(127.0.0.1:26389, 127.0.0.1:26379).addSentinelAddress(127.0.0.1:26319);RedissonClient redisson Redisson.create(config);具体的参数配置见github.com/redisson/re…
主从模式
程序化配置主从模式的用法
Config config new Config();
config.useMasterSlaveServers()//可以用rediss://来启用SSL连接.setMasterAddress(redis://127.0.0.1:6379).addSlaveAddress(redis://127.0.0.1:6389, redis://127.0.0.1:6332, redis://127.0.0.1:6419).addSlaveAddress(redis://127.0.0.1:6399);RedissonClient redisson Redisson.create(config);具体的参数配置见github.com/redisson/re…
集群模式
程序化配置主从模式的用法
Config config new Config();
config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间单位是毫秒// 可以用rediss://来启用SSL连接.addNodeAddress(redis://127.0.0.1:7000, redis://127.0.0.1:7001).addNodeAddress(redis://127.0.0.1:7002);
RedissonClient redisson Redisson.create(config);
集群模式除了适用于 Redis 集群环境也适用于任何云计算服务商提供的集群模式例如 AWS ElastiCache 集群版、Azure Redis Cache 和阿里云(Aliyun)的云数据库 Redis 版。
Spring Boot 整合
添加 redisson-spring-boot-starter 依赖
dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.23.5/version
/dependency属性配置
spring:data:redis:# 数据库database: 0# 主机host: localhost# 端口port: 6379# 密码password:123456# 读超时timeout: 5s# 连接超时connect-timeout: 5s添加配置类
Configuration
public class RedissonConfig {Autowiredprivate RedisProperties redisProperties;Beanpublic RedissonClient redissonClient() {Config config new Config();String redisUrl String.format(redis://%s:%s, redisProperties.getHost() ,redisProperties.getPort() );config.useSingleServer().setAddress(redisUrl).setPassword(redisProperties.getPassword());config.useSingleServer().setDatabase(3);return Redisson.create(config);}
}Redisson 中的锁
Redisson 可重入锁
基于 Redis 的 Redisson 分布式可重入锁 RLock它实现了 java.util.concurrent.locks.Lock。同时还支持自动过期解锁。使用最多的是下面三类方法
lock.lock()lock.lock(10, TimeUnit.SECONDS)10 秒后自动释放锁无需手动调用 unlock() 解锁。lock.tryLock(5, 10, TimeUnit.SECONDS)尝试加锁最多等待 5 秒加锁成功后10 秒后自动释放锁。
下面用示例验证它的可重入逻辑
public class RedissonLockTest {RedissonClient redisson Redisson.create();RLock lock redisson.getLock(reentrantLockTest);Testpublic void reentrantLock01Test() throws InterruptedException {boolean isLock lock.tryLock();if (isLock) {System.out.println(Thread.currentThread().getName() -- 获取锁成功...);// 整理等待 30 秒是为了查看数据TimeUnit.SECONDS.sleep(30);// 调用 reentrantLock02Test 第二次获取锁reentrantLock02Test();}}public void reentrantLock02Test() {boolean isLock lock.tryLock();if (isLock) {System.out.println(Thread.currentThread().getName() -- 获取锁成功...);}}
}执行程序当控制台第一次打印 “获取锁成功” 后查看 Redis 数据 第二次打印 “获取锁成功” Redisson 分布式锁采用了 Redis 的 hash 数据结构存储key 为我们指定的值field 属性为线程标识value 为锁次数。当线程第一次获取时此时 Redis 中没有这个 key获取锁成功创建锁数据并设置锁次数为 1。接下来如果线程再次获取锁则先对比线程标识是否为同一个线程如果是则重入锁次数 1。
释放锁也需要同样对比线程标识然后将所次数 -1 当锁的次数为 0 时表示锁已完全释放。
Redisson 公平锁
Redisson 支持公平锁和非公平锁上面的重入锁就是非公平锁。公平锁与 JUC 中的公平锁一致遵循先到先得的原则。
Redisson 提供了 getFairLock() 来创建公平锁
RLock fairLock redisson.getFairLock(myFairLock);获取公平锁后调用 lock() 即可获取锁
fairLock.lock();公平锁一般适用于对锁的公平性要求较高的场景例如任务调度、消息处理等。
Redisson 联锁
联锁RedissonMultiLock是指同时对多个资源进行加锁操作只有所有资源都加锁成功的时候联锁才会成功。
Redisson 中的联锁是将多个 RLock 对象关联为一个联锁对象实现加锁和解锁功能。每个 RLock 对象实例可以来自于不同的 Redisson 实例。
RLock lock1 redissonClient.getFairLock(testLock1);
RLock lock2 redissonClient.getFairLock(testLock2);
RLock lock3 redissonClient.getFairLock(testLock3);RedissonMultiLock multiLock new RedissonMultiLock(lock1, lock2, lock3);
try {// 同时加锁testLock1 testLock2 testLock3// 所有的锁都上锁成功才算成功。boolean tryLock multiLock.tryLock(1, TimeUnit.SECONDS);if (tryLock) {// do something()}
} catch (InterruptedException e) {throw new RuntimeException(e);
}Redisson 读写锁
与 Java 一样Redisson 也提供了读写锁。读写锁是 Redisson 中的高级分布式锁它分为读锁和写锁两种锁
读锁允许多个线程同时获取锁并进行读操作。写锁要求独占。
使用 Redisson 的 getReadWriteLock() 创建读写锁对象
RReadWriteLock readWriteLock redisson.getReadWriteLock(readWriteLock);调用 readLock() 或者 writeLock() 获取读写锁
// 获取读锁
RLock readLock readWriteLock.readLock();// 获取写锁
RLock writeLock readWriteLock.writeLock();Redisson Redlock
Redlock 是 Redis 作者对分布式锁提出的一种加锁算法其核心是假设 Redis 集群中有 N 个 Redis 节点只有当客户端成功在 N/21 个实例中成功加锁成功才算成功持有分布式锁。
RLock lock1 redissonClient.getLock(testLock1);
RLock lock2 redissonClient.getLock(testLock2);
RLock lock3 redissonClient.getLock(testLock3);RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();Redisson 的看门狗机制
如果任务的执行时间比锁的超时时间还长这种情况会导致锁过早被释放了从而会让其他线程在当前线程的任务完成之前获取到锁这就会引发线程安全问题。为了解决这个问题一般有如下几种解决方案
续租机制【推荐方案】
最常见有效的方案是实现一个锁续租机制。也就是在任务执行期间会定期更新锁的过期时间。确保锁在整个任务执行期间保持有效。Redisson 提供了 watch dog 机制看门狗该机制具备锁自动续期功能用于避免分布式锁在业务处理过程中因执行时间过长而被提前释放。watch dog会自动检测用户线程是否还活着如果活着它会在锁快要自动释放之前自动续期直到用户线程完成工作。
使用更长的锁超时时间
预估一个任务的最长执行时间然后将所的超时时间设置更长一点已覆盖这个时间范围。但是这种方案有几个缺陷绝大部分任务的执行时间都会比预估的最长超时时间短如果某个线程中途崩溃了导致锁无法正常释放这就会降低系统的并发性。
检查任务状态
再获取锁后检查任务的执行状态如果仍然有任务在运行则在那里等待。
任务拆分
我们可以将一个长时间执行的任务拆分为多个独立的较短的小任务每个步骤都有自己独立的分布式锁这样就可以减少锁定资源的时间同时确保每个阶段都能在适当的时间内完成。
这里详细介绍 Redisson 的看门狗机制。
Redisson 的 watch dog 的核心思想是在 Redisson 客户端获取到锁后会自动启动一个监控任务该任务会定期检查锁的状态并在需要时自动延长锁的过期时间。其核心机制有如下几点
自动续期当 Redisson 客户端获取锁后默认情况下watch dog 会每隔一段时间默认是锁有效期的 1/3即 10 秒自动将锁的有效期重新设置为最初的有效期默认 30 秒直到锁被释放。这个操作是通过一个后台线程完成的它确保了即使客户端处理逻辑较长也不会因为锁自动过期而导致锁被提前释放。停止续期由于某种原因导致客户端崩溃watch dog 会停止续期锁会在最后一次续期后的有效期内自动释放掉。续期时长默认情况下watch dog 每 10 秒续期一次每次续期 30 秒。
下面看看 Redisson 的 watch dog 源码。
源码路径如下lock() — tryAcquire() — tryAcquireAsync() private RFutureLong tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFutureLong ttlRemainingFuture;// leaseTime 0表示指定了锁定时间则直接加锁if (leaseTime 0) {ttlRemainingFuture tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 没有指定锁定时间默认加锁时间为 internalLockLeaseTimettlRemainingFuture tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStageLong s handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture new CompletableFutureWrapper(s);CompletionStageLong f ttlRemainingFuture.thenApply(ttlRemaining - {// lock acquiredif (ttlRemaining null) {if (leaseTime 0) {// leaseTime 0 不使用自动续期internalLockLeaseTime unit.toMillis(leaseTime);} else {// 自动续期scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper(f);}leaseTime 0说明我们调用加锁方法时指定的锁过期时间这个时候是不会开启 watch dog 机制直接设置过期时间即可。
如果没有指定过期时间则使用 internalLockLeaseTime 为过期时间该值通过 getServiceManager().getCfg().getLockWatchdogTimeout() 获取 lockWatchdogTimeout 的值默认为 30 秒
private long lockWatchdogTimeout 30 * 1000;当然也可以调用 setLockWatchdogTimeout() 设置 watch dog 默认时间。
只有当 leaseTime -1 时才会调用 scheduleExpirationRenewal() 开启自动续期进程 protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry new ExpirationEntry();ExpirationEntry oldEntry EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry ! null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);try {renewExpiration();} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId, null);}}}}scheduleExpirationRenewal() 首先会将该续期任务添加到 EXPIRATION_RENEWAL_MAP 集合中EXPIRATION_RENEWAL_MAP 是 Redisson 用来管理锁续期任务的集合其作用是跟踪当前正在被自动续期的锁。
在 scheduleExpirationRenewal() 中调用 renewExpiration()开启自动续期定时任务 private void renewExpiration() {ExpirationEntry ee EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee null) {return;}Timeout task getServiceManager().newTimeout(new TimerTask() {Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent null) {return;}Long threadId ent.getFirstThreadId();if (threadId null) {return;}CompletionStageBoolean future renewExpirationAsync(threadId);future.whenComplete((res, e) - {if (e ! null) {log.error(Cant update lock {} expiration, getRawName(), e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// reschedule itselfrenewExpiration();} else {cancelExpirationRenewal(null, null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);}
从 renewExpiration() 可以看出Redisson 是使用了一个 TimerTask 定时任务去执行续期任务的delay 为 internalLockLeaseTime / 3。在该定时任务中调用 renewExpirationAsync() 完成续期 protected CompletionStageBoolean renewExpirationAsync(long threadId) {return evalWriteSyncedAsync(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));}这里是使用 lua 脚本调用 pexpire 命令来进行续期。
然而在 TimerTask 里面它并不是无脑地调用 renewExpirationAsync() 来续期的这里会有两个判断
ExpirationEntry ent EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent null) {return;
}
Long threadId ent.getFirstThreadId();
if (threadId null) {return;
}ent null 表示该自动续期任务已经被释放了当调用 unlock() 时Redisson 会 remove 掉这个任务 protected void cancelExpirationRenewal(Long threadId, Boolean unlockResult) {ExpirationEntry task EXPIRATION_RENEWAL_MAP.get(getEntryName());if (task null) {return;}if (threadId ! null) {task.removeThreadId(threadId);}if (threadId null || task.hasNoThreads()) {Timeout timeout task.getTimeout();if (timeout ! null) {timeout.cancel();}EXPIRATION_RENEWAL_MAP.remove(getEntryName());}}虽然 Redisson 的看门狗机制能够解决锁自动续期的问题但是它是单机的单机就存在两个问题
单点故障如果 Redis 节点因为故障等原因导致 Redis 实例挂掉那么所有这个 Redis 实例的节点都将无法获取到锁会严重阻碍业务。主从同步问题当使用集群部署 Redis如果一个客户端在 Master 节点上获取到了锁然后没有来得及将数据同步到 Slave 节点上它就挂了。就算此时选举出来了一个新的 Master 节点它里面也没有对应的锁信息这个时候其他客户端就会获取锁成功会导致并发问题。
Redis 官网也提到了这些问题 那怎么解决呢Redis 作者提出 RedLock 解决方案。
RedLock 解决单体故障问题
RedLock 是 Redis 作者提出的一个多节点分布式锁算法它主要是解决单节点 Redis 分布式锁可能存在的单点故障问题。其核心思想是不在单个 Redis 实例上进行加锁而是在多个互相独立的 Redis 节点加锁只有在大多数节点上解锁成功锁才算获取成功。其核心原理如下
多个独立节点RedLock 不再是在单个 Redis 节点加锁而是在多个互相独立的 Redis 节点加锁通常是基数个避免脑裂这些节点彼此直接不是主从关系也不是集群。尝试加锁在获取锁时客户端会向所有 Redis 节点发送加锁请求每个请都有着相同的锁 ID 和相同的过期时间注意该过期时间是毫秒级要远远小于锁的有效时间。大多数节点获取锁成功客户端需要判断获取锁成功的节点数如果获得锁的节点数大于约定节点数N/21则认为获取锁成功。
如下 释放锁当客户端不需要锁后就会释放锁释放锁时客户端会向所有的 Redis 节点发送释放锁的请求不管这些节点是否成功获取了锁。
RedLock 获取锁过程如下假如有 5 个 Redis 节点
客户端先获取当前时间戳 T1。客户端依次向 5 个 Redis 实例发送获取锁的请求且每个请求都会设置超时时间该超时时间是毫秒级它要远远小于锁的有效期如果某一个 Redis 实例加锁失败则立刻向下一个 Redis 实例发起获取锁请求。当有 ≥ 3 个 Redis 节点获取锁成功客户端再次获取当前时间戳 T2如果 T2 - T1 锁的过期时间则获取 RedLock 成功。
如何使用 RedLock
Redisson 提供了 RedLock 的实现直接用 RedissonRedLock 即可 Testpublic void redissonRedLockTest() {Config config1 new Config();config1.useSingleServer().setAddress(redis://127.0.0.1:6379);RedissonClient redissonClient1 Redisson.create(config1);Config config2 new Config();config2.useSingleServer().setAddress(redis://127.0.0.2:6380);RedissonClient redissonClient2 Redisson.create(config2);Config config3 new Config();config3.useSingleServer().setAddress(redis://127.0.0.3:6381);RedissonClient redissonClient3 Redisson.create(config3);RLock rLock1 redissonClient1.getLock(lock1);RLock rLock2 redissonClient2.getLock(lock2);RLock rLock3 redissonClient3.getLock(lock3);RedissonRedLock redLock new RedissonRedLock(rLock1, rLock2, rLock3);boolean lockResult redLock.tryLock();if (lockResult) {try{//....} finally {redLock.unlock();}}}到这里了是不是小伙伴们认为 RedLock 就万无一失了其实不然。Redis 作者 Antirez 提出 RedLock 方案后立刻就遭到英国剑桥大学、业界著名的分布式系统专家 Martin 的质疑他认为 Antirez 提出的 RedLock 算法模型有问题写了一篇文章列出 RedLock 的算法问题并提出了自己的看法。而 Antirez 也不甘示弱也写了一篇文章来反驳。
两位大神的原文
Martinnews.ycombinator.com/item?id110…Antireznews.ycombinator.com/item?id110…
下面的内容是对这两篇文章的解读。
Martin 对于 Relock 的质疑
在 Martin 大神的文章中主要是阐述了 4 点
使用分布式锁的目的锁在分布式系统中遇到的问题时钟不正确导致的问题fecing token 方案
使用分布式锁的目的
Martin 表示我们使用 Redis 来实现分布式锁的主要目的是两点。
效率使用分布式锁的互斥能力避免多次做重复的工作。这种情况即使锁失效也不会带来「恶性」的后果。例如多发了 1 次邮件、多计算一次都是无伤大雅的场景。但是 Martin 认为如果是为了效率单机版的 Redis 效率更高即使发生偶尔的宕机也不会产生很严重的问题。使用 RedLock 太重了没有必要。正确性使用锁是为了防止多个线程互相竞争保证线程安全如果锁失效则会发生线程不安全导致数据不一致影响比较恶劣。然而Martin 认为 RedLock 根本无法达到安全的效果会存在锁失效的情况。
所以无论是效率还是正确性Martin 认为 RedLock 都达不到。
锁在分布式系统中遇到的问题
Martin 表示一个分布式系统存在着各种异常情况这些异常场景主要包括三大块这也是分布式系统会遇到的三座大山NPC。
NNetwork Delay网络延迟PProcess Pause进程暂停CClock Drift时钟漂移
Martin 使用了一个进程暂停的例子来说明具体过程如下
客户端 1 请求获取锁节点 A、B、C、D、E客户端 1 获取锁成功这是系统暂停比如 STW这个暂停时间会比较长。客户端 1 获取的锁全部过期客户端 2 请求获取锁节点 A、B、C、D、E客户端 2 获取锁成功执行业务逻辑此时客户端 1 GC 结束因为客户端 1 在开始的时候已经获取锁成功了所以它就不会再次请求获取锁了而是直接执行执业务逻辑这就导致客户端 1 和 客户端 2 并行执行同业务逻辑则会发生冲突。
如下图 需要注意的是不仅仅只是 GC 导致的暂停任何可以造成系统停顿的因素都会导致这种情况产生比如 I/O 、网络阻塞等等。
时钟不正确导致的问题
Martin 指出一个优秀的分布式系统应该基于异步模型简单概括就是不对时间做任何假设不能使用时间来作为安全保障。因为在分布式系统中会有程序暂停、数据包延迟、系统时间错误。而一个好的分布式系统不会因为这些因素影响锁的安全性只可能影响到它的活性liveness property。也就是说在极端情况下优秀的分布式锁顶多是不能在有限的时间内给出结果但不能给出一个错误的结果这样的算法是真实存在的如Raft、Zab 和 Paxos等等。
但是RedLock 严重依赖依赖系统时钟因为在 RedLock 的实现中它是依赖锁的过期时间的如果多个 Redis 实例的时钟不一致则会导致如下这种情况
有 5 个 Redis 节点 A、B、C、D、E客户端 1 成功获取节点 A、B、C 三个节点的锁获得分布式锁节点 A 时钟向前跳跃导致 A 节点的锁提前释放客户端 2 成功获取节点 A、D、E获得分布式锁这是客户端 1 和客户端 2 同时持有分布式锁导致冲突
而机器发生时钟漂移的概率还是有的比如
运维手动修改机器时钟在同步 NTP 时间时发生了大的跳跃
fecing token 方案
针对 RedLock 的缺陷Martin 提出了自己的解决方案fecing token。
Martin 的解决方案是为锁资源增加一个递增的 token 用来保证分布式锁的安全性 客户端在获取锁时锁服务提供一个递增的 token。如在上图 Client1 除了获取锁外还获得了一个值为 33 的 token 。客户端拿着这个 token 去操作共享资源。共享资源可以根据 token 拒绝后来者的请求。例如上图中Client1 因为 STW 暂停导致锁被释放了Client2 获取锁后使用 token 34 去操作共享资源
Martin 认为 fecing token 方案无论是碰到分布式中 NPC 的那种情况都能够保证分布锁的安全性因为它是建立在异步模型的。
Antirez 的反驳
针对 Martin 的质疑Antirez 做出来以下几点反驳。
时钟问题
针对 Martin 提出的时钟错误问题Antirez 反驳道
人为手动修改不要这么做就可以了。如果可以认为破坏的话无论采用哪种手段都是不安全的。时钟跳跃NTP受到一个阶跃时钟更新对于这个问题需要通过运维来保证。需要将阶跃的时间更新到服务器的时候应当采取小步快跑的方式。多次修改每次更新时间尽量小。
严格上来说RedLock 是建立在可信的时钟模型上的在现实情况下确实是会存在一些时钟错误的情况但是我们可以通过一些运维手段或者工程机制最大限度保证时钟可信。
线程暂停问题
针对线程暂停的问题我们再次回顾 RedLock 获取锁的过程
客户端先获取当前时间戳 T1。客户端依次向 5 个 Redis 实例发送获取锁的请求且每个请求都会设置超时时间该超时时间是毫秒级它要远远小于锁的有效期如果某一个 Redis 实例加锁失败则立刻向下一个 Redis 实例发起获取锁请求。当有 ≥ 3 个 Redis 节点获取锁成功客户端再次获取当前时间戳 T2如果 T2 - T1 锁的过期时间则获取 RedLock 成功。
在这个步骤中RedLock 会两次获取时间戳。如果线程暂停是发生在获取 T 时间戳前那么是可以通过 T2 - T1 锁的过期时间 检测出来的。如果超出了锁的过期时间则会被认为获取锁失败所以这种情况是可以避免的。
如果线程暂停是发生客户端 1 获取分布锁成功后导致其他线程能够获取分布式锁产生锁冲突。那这就不是 RedLock 所负责的范畴了RedLock 只提供的正确的分布式锁而且这种情况其他的分布式锁服务如Zookeeper也是无法避免的。
fecing token 方案
Martin 提供的fecting token 方案需要共享资源具备拒绝旧 token 的能力试想下如果共享资源就具备这种互斥能力那还需要分布式锁干嘛
RedLock 被弃用了
由于 RedLock 存在争议Redis 官方已经标记 RedLock 算法为 “discouraged” 更新记录如下 所以在实际生产环境下还是尽量不要使用 RedLock 。对于大多数的场景而言使用 Redisson 的普通锁就可以了如果项目对分布式锁的安全性要求很高推荐使用基于 Raft 或 Paxos 算法的 etcd 或 ZooKeeper他们在设计时充分考虑了分布式环境下的一致性和可靠性问题提供了比 RedLock 更为健壮的解决方案。