Redisson 实现分布式锁

文章目录

    • Redisson 是什么
    • Redisson 使用
      • 客户端模式
      • 单节点模式
      • 哨兵模式
      • 主从模式
      • 集群模式
      • Spring Boot 整合
    • Redisson 中的锁
      • Redisson 可重入锁
      • Redisson 公平锁
      • Redisson 联锁
      • Redisson 读写锁
      • Redisson Redlock
    • Redisson 的看门狗机制
    • RedLock 解决单体故障问题
      • 如何使用 RedLock
      • Martin 对于 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 FilterHyperLogLog 等。
  • 提供丰富的分布式集合,如:Map,Multimap,Set,SortedSet,List,Deque,Queue等。
  • 分布式锁和同步器的实现,可重入锁(Reentrant Lock),公平锁(Fair Lock),联锁(MultiLock),红锁(Red Lock),信号量(Semaphore),可过期性信号锁(PermitExpirableSemaphore)等。
  • 提供先进的分布式服务,如分布式远程服务(Remote Service),分布式实时对象(Live Object)服务,分布式执行服务(Executor Service),分布式调度任务服务(Schedule Service)和分布式映射归纳服务(MapReduce)。

Redisson 使用

客户端模式

  • 引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.27.2</version>
</dependency>
  • 获取 RedissonClientRedissonClient有多种模式,主要的模式有:
    • 单节点模式
    • 哨兵模式
    • 主从模式
    • 集群模式

单节点模式

程序化配置方法:

// 默认连接地址 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 依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.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/2+1 个实例中成功加锁成功,才算成功持有分布式锁。

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 RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> 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);}CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> 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;}CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) {log.error("Can't 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 CompletionStage<Boolean> 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/2+1),则认为获取锁成功。

如下:

  • 释放锁:当客户端不需要锁后,就会释放锁,释放锁时,客户端会向所有的 Redis 节点发送释放锁的请求,不管这些节点是否成功获取了锁。

RedLock 获取锁过程如下(假如有 5 个 Redis 节点):

  1. 客户端先获取当前时间戳 T1。
  2. 客户端依次向 5 个 Redis 实例发送获取锁的请求,且每个请求都会设置超时时间(该超时时间是毫秒级,它要远远小于锁的有效期),如果某一个 Redis 实例加锁失败,则立刻向下一个 Redis 实例发起获取锁请求。
  3. 当有 ≥ 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 也不甘示弱,也写了一篇文章来反驳。

两位大神的原文:

  • Martin:news.ycombinator.com/item?id=110…
  • Antirez:news.ycombinator.com/item?id=110…

下面的内容是对这两篇文章的解读。

Martin 对于 Relock 的质疑

Martin 大神的文章中主要是阐述了 4 点:

  • 使用分布式锁的目的
  • 锁在分布式系统中遇到的问题
  • 时钟不正确导致的问题
  • fecing token 方案
使用分布式锁的目的

Martin 表示我们使用 Redis 来实现分布式锁的主要目的是两点。

  • 效率:使用分布式锁的互斥能力,避免多次做重复的工作。这种情况即使锁失效,也不会带来「恶性」的后果。例如多发了 1 次邮件、多计算一次都是无伤大雅的场景。但是 Martin 认为,如果是为了效率,单机版的 Redis 效率更高,即使发生偶尔的宕机也不会产生很严重的问题。使用 RedLock 太重了,没有必要。
  • 正确性:使用锁是为了防止多个线程互相竞争,保证线程安全,如果锁失效,则会发生线程不安全,导致数据不一致,影响比较恶劣。然而,Martin 认为 RedLock 根本无法达到安全的效果,会存在锁失效的情况。

所以,无论是效率还是正确性,Martin 认为 RedLock 都达不到。

锁在分布式系统中遇到的问题

Martin 表示,一个分布式系统,存在着各种异常情况,这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山:NPC

  • N:Network Delay,网络延迟
  • P:Process Pause,进程暂停
  • C:Clock Drift,时钟漂移

Martin 使用了一个进程暂停的例子来说明,具体过程如下:

  1. 客户端 1 请求获取锁节点 ABCDE
  2. 客户端 1 获取锁成功,这是系统暂停(比如 STW),这个暂停时间会比较长。
  3. 客户端 1 获取的锁全部过期
  4. 客户端 2 请求获取锁节点 ABCDE
  5. 客户端 2 获取锁成功,执行业务逻辑
  6. 此时,客户端 1 GC 结束,因为客户端 1 在开始的时候已经获取锁成功了,所以它就不会再次请求获取锁了,而是直接执行执业务逻辑,这就导致客户端 1 和 客户端 2 并行执行同业务逻辑,则会发生冲突。

如下图:

需要注意的是,不仅仅只是 GC 导致的暂停,任何可以造成系统停顿的因素都会导致这种情况产生,比如 I/O 、网络阻塞等等。

时钟不正确导致的问题

Martin 指出一个优秀的分布式系统应该基于异步模型,简单概括就是不对时间做任何假设,不能使用时间来作为安全保障。因为在分布式系统中会有程序暂停、数据包延迟、系统时间错误。而一个好的分布式系统不会因为这些因素影响锁的安全性,只可能影响到它的活性(liveness property)。也就是说在极端情况下优秀的分布式锁顶多是不能在有限的时间内给出结果,但不能给出一个错误的结果,这样的算法是真实存在的如RaftZabPaxos等等。

但是,RedLock 严重依赖依赖系统时钟,因为在 RedLock 的实现中,它是依赖锁的过期时间的,如果多个 Redis 实例的时钟不一致,则会导致如下这种情况:

  1. 有 5 个 Redis 节点 ABCDE
  2. 客户端 1 成功获取节点 ABC 三个节点的锁,获得分布式锁
  3. 节点 A 时钟向前跳跃,导致 A 节点的锁提前释放
  4. 客户端 2 成功获取节点 ADE,获得分布式锁
  5. 这是客户端 1 和客户端 2 同时持有分布式锁,导致冲突

而机器发生时钟漂移的概率还是有的,比如:

  • 运维手动修改
  • 机器时钟在同步 NTP 时间时,发生了大的跳跃
fecing token 方案

针对 RedLock 的缺陷,Martin 提出了自己的解决方案:fecing token

Martin 的解决方案是为锁资源增加一个递增的 token 用来保证分布式锁的安全性:

  1. 客户端在获取锁时,锁服务提供一个递增的 token。如在上图 Client1 除了获取锁外,还获得了一个值为 33 的 token
  2. 客户端拿着这个 token 去操作共享资源。
  3. 共享资源可以根据 token 拒绝后来者的请求。例如上图中,Client1 因为 STW 暂停导致锁被释放了,Client2 获取锁后使用 token = 34 去操作共享资源

Martin 认为 fecing token 方案无论是碰到分布式中 NPC 的那种情况,都能够保证分布锁的安全性,因为它是建立在异步模型的。

Antirez 的反驳

针对 Martin 的质疑,Antirez 做出来以下几点反驳。

时钟问题

针对 Martin 提出的时钟错误问题,Antirez 反驳道:

  1. 人为手动修改:不要这么做就可以了。如果可以认为破坏的话,无论采用哪种手段都是不安全的。
  2. 时钟跳跃:NTP受到一个阶跃时钟更新,对于这个问题,需要通过运维来保证。需要将阶跃的时间更新到服务器的时候,应当采取小步快跑的方式。多次修改,每次更新时间尽量小。

严格上来说,RedLock 是建立在可信的时钟模型上的,在现实情况下确实是会存在一些时钟错误的情况,但是我们可以通过一些运维手段或者工程机制最大限度保证时钟可信。

线程暂停问题

针对线程暂停的问题,我们再次回顾 RedLock 获取锁的过程:

  1. 客户端先获取当前时间戳 T1。
  2. 客户端依次向 5 个 Redis 实例发送获取锁的请求,且每个请求都会设置超时时间(该超时时间是毫秒级,它要远远小于锁的有效期),如果某一个 Redis 实例加锁失败,则立刻向下一个 Redis 实例发起获取锁请求。
  3. 当有 ≥ 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 更为健壮的解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/393925.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C语言篇】操作符详解(上篇)

文章目录 操作符详解&#xff08;上篇&#xff09;前言sizeof强制类型转换算术操作符赋值操作符逻辑操作符逻辑取反运算符逻辑与运算符逻辑或运算符 关系操作符自增自减操作符和-逗号表达式 操作符详解&#xff08;上篇&#xff09; 前言 操作符又被叫做运算符&#xff0c;是不…

进程状态(三)----- linux 中具体的进程状态(下)

目录 前言1. T && t 状态2. X 与 Z 状态3. 孤儿进程 前言 继上一篇文章 进程状态&#xff08;二&#xff09;----- linux 中具体的进程状态&#xff08;上&#xff09; 介绍了 linux 系统中具体的 R、S、D 状态&#xff0c;而这篇文章继续介绍 linux 系统中剩下的三种…

SpringBoot简单项目(二维码扫描)

pom.xml中导入依赖 <!-- zxing --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.0</version></dependency><dependency><groupId>com.google.zxing</gro…

探索七款前沿UI设计软件:创新与实践

之前我们分享了制作原型的有用工具。制作完原型后&#xff0c;我们需要优化界面&#xff0c;这就是 UI 设计师的任务了。UI 设计软件对设计师来说非常重要。UI 设计工具的使用是否直接影响到最终结果的质量&#xff0c;所以有人会问&#xff1a;UI 界面设计使用什么软件&#x…

Java批量查询CSDN质量分

文章目录 前言代码实现pom.xml实体类工具类质量分查询 效果开源仓库 前言 在CSDN平台申请“专家博主”、“优质创作者”等称号的时候&#xff0c;往往会对博客的质量分有一定的要求&#xff0c;这时候我们需要审视以往所发表的博客&#xff0c;找出质量分较低的博客&#xff0…

nordic 蓝牙ble 配对绑定的流程 原理

目录 配对和绑定的基本概念 配对和绑定的流程 1. 配对请求和响应 2. 配对方法选择 3. 密钥生成和交换 4. 配对完成和绑定 配对和绑定的代码实现 初始化Peer Manager 处理Peer Manager事件 处理BLE事件 启动广播 在Nordic芯片上实现蓝牙低功耗(BLE)设备的配对和绑定…

Python 为Excel单元格设置填充\背景色 (纯色、渐变、图案)

在使用Excel进行数据处理和分析时&#xff0c;对特定单元格进行背景颜色填充不仅能够提升工作表的视觉吸引力&#xff0c;还能帮助用户快速识别和区分不同类别的数据&#xff0c;增强数据的可读性和理解性。 本文将通过以下三个示例详细介绍如何使用Python在Excel中设置不同的单…

sql注入——sqlilabs1-15

目录 sql注入靶场练习--sqlilabs 1.less-1​编辑 1.测试发现单引号为逃逸符号 2.确定查询列数为三列 3.查询到数据库名 4.查询数据库中的表名 5.查询用户表的列名字 6.查询用户信息 2.less-2​编辑 2.确定查询列数为三列 3.查询到数据库名 4.查询数据库中的表名 5.…

机械学习—零基础学习日志(高数23——无穷小运算)

零基础为了学人工智能&#xff0c;真的开始复习高数 这段时间&#xff0c;把张宇老师讲解考研的第一部分基本全部学习完毕了。 这里把第一部分的内容最后汇总一下。 无穷小运算——吸收律 这里展示一些无穷小的具体计算思路 无穷小运算——计算方法 泰勒展开的原则 夹逼准则…

SQL报错注入之floor

目录 1.简述 2.关键函数说明 1.rand函数 2.floor&#xff08;rand&#xff08;0&#xff09;*2&#xff09;函数 3.group by 函数 4.count&#xff08;*&#xff09;函数 3.报错分析 4.报错流程 4.1寻找注入点 4.2爆数据库名 4.3爆表名 4.4爆字段名 4.5查询数据 1.…

PySide入门实战之五 | 信号与槽函数之鼠标、键盘等事件

&#x1f680;&#x1f680;&#x1f680; Pyside6实战教程专栏目录入口&#xff1a;点击跳转 目录 一、前期准备二、鼠标触发事件鼠标拖动窗口 一、前期准备 我们采用Pyside入门实战之四中通过QTDesigner创建的界面&#xff0c;具体由两个Label和一个Button组件构成&#xff…

【c++】基础知识——快速入门c++

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C 目录 前言 一、手搓一个Hello World 二、命名空间namespace 1.命名空间的定义 2.命名空间的使用 3.命名空间补充知识 三、c中的输入和输出 四、缺省参…

图书馆座位再利用小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;座位信息管理&#xff0c;座位预订管理&#xff0c;互勉信息管理&#xff0c;意见反馈管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;我的 开发…

Unity补完计划 之Tilemap

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 1.Tilemap 是什么 Q&#xff1a;和 SpriteShape有什么区别&#xff1f; A&#xff1a;tilemap强项在于做重的复背景&…

VsCode无法远程调试

一、问题描述 按照《VsCode gdb gdbserver远程调试C程序》中介绍的方法&#xff0c;配置好VsCode后&#xff0c;按下F5快捷键&#xff0c;或点击“Start Debugging”按钮&#xff0c;没有反应&#xff0c;无法启动调试&#xff1a; 二、解决方法 针对该问题&#xff0c;我尝…

常用设计模式总结

代码的评判角度 常见的评判代码好坏的词汇&#xff1a; 灵活性&#xff08;flexibility&#xff09;、可扩展性&#xff08;extensibility&#xff09;、可维护性&#xff08;maintainability&#xff09;、可 读性&#xff08;readability&#xff09;、可理解性&#xff08;…

电子元器件—三极管(一篇文章搞懂电路中的三极管)(笔记)(面试考试必备知识点)

三极管的定义及工作原理 1. 定义 三极管&#xff08;Transistor&#xff09;是一种具有三层半导体材料&#xff08;P-N-P 或 N-P-N&#xff09;构成的半导体器件&#xff0c;用于信号放大、开关控制和信号调制等应用。三极管有三个引脚&#xff1a;发射极&#xff08;Emitter…

Javascript——NaN有什么用法

简介 在 JavaScript 中&#xff0c;NaN&#xff08;Not a Number&#xff09;是一个特殊的值&#xff0c;用来表示非数字的结果&#xff0c;例如一个不合法的数学运算的结果。根据 IEEE 754 浮点数标准&#xff0c;NaN 不等于任何值&#xff0c;包括它自己。这意味着 NaN 是唯…

JAVA毕业设计158—基于Java+Springboot的二手车交易管理系统(源代码+数据库+万字论文+ppt)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringboot的二手车交易管理系统(源代码数据库万字论文ppt)158 一、系统介绍 本项目前后端不分离(可以改为ssm版本)&#xff0c;分为用户、管理员两种角色 1、用户&#…

Golang | Leetcode Golang题解之第326题3的幂

题目&#xff1a; 题解&#xff1a; func isPowerOfThree(n int) bool {return n > 0 && 1162261467%n 0 }