当前位置: 首页 > news >正文

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 更为健壮的解决方案。

相关文章:

Redisson 实现分布式锁

文章目录 Redisson 是什么Redisson 使用客户端模式单节点模式哨兵模式主从模式集群模式Spring Boot 整合 Redisson 中的锁Redisson 可重入锁Redisson 公平锁Redisson 联锁Redisson 读写锁Redisson Redlock Redisson 的看门狗机制RedLock 解决单体故障问题如何使用 RedLockMarti…...

VMware ESXi学习笔记

esxi网络模型&#xff1a; 物理网卡: 一般会有多个物理网卡&#xff0c;用于管理口和其他(vsan) 虚拟交换机&#xff1a;创建虚拟交换机时&#xff0c;会要求选择至少1个上行链路(物理网卡) 端口组&#xff1a;一般一个虚拟交换机会创建两个端口组&#xff0c;一个虚机使用&a…...

Python 函数(2)

2、函数 2.1、函数传递列表 将列表传递给函数后&#xff0c;函数就能直接访问其内容。 下列为一个实例&#xff1a;将一个名字列表传递给一个名为greet_users()的函数&#xff0c;这个函数将会向列表中的每一个元素执行相应的信息。 def greet_users(name):for name in name…...

c++文件的读写

平常我们在编完代码后&#xff0c;基本都是从键盘输入&#xff0c;从屏幕输出&#xff08;显示&#xff09;&#xff0c;但可不可以从其他地方输入输出呢&#xff1f; ………………………………………………………………………………………………………………… 其实可以&…...

春秋云境 | 文件上传 | CVE-2022-30887

目录 靶标介绍 开启靶场 上传一句话木马 蚁剑连接 找到 flag 靶标介绍 多语言药房管理系统 (MPMS) 是用 PHP 和 MySQL 开发的, 该软件的主要目的是在药房和客户之间提供一套接口&#xff0c;客户是该软件的主要用户。该软件有助于为药房业务创建一个综合数据库&#xff0…...

大模型+XDR!打开网络安全攻防演练新范式!

网络安全领域面临着日益复杂的挑战&#xff0c;外部攻击与内部安全威胁交织的双重压力。技术革新成为筑牢安全防线的关键&#xff0c;随着新一代技术的崛起&#xff0c;特别是大数据与人工智能的深度融合&#xff0c;引领着网络安全进入全新阶段。 通过构建网络安全大模型&…...

C语言----字符串

字符串 第一种定义 利用字符数组 双引号的方式定义字符串 char str1[4]"abc"; printf("%s\n",str1); 细节1: 在底层&#xff0c;实际存储的时候&#xff0c;c语言还是会帮我们把字符串"abc"转换成字符数组进行保存&#xff0c;并且在末尾还…...

ThreadLocal 详解(三)内存泄露原因,以及强弱引用

1、ThreadLocal内存泄漏 在Threadlocal的内部静态类中Entry将Threadlocal作为一个key&#xff0c;值作为value保存&#xff0c;他继承WeakReference&#xff0c;super(k)&#xff0c;代表了Threadlocal对象是一个弱引用&#xff1b; static class Entry extends WeakReference…...

【Android面试八股文】说一说Android开发模式之MVC、MVP、MVVM的区别?

文章目录 一、 MVC(Model-View-Controller)二、 MVP(Model-View-Presenter)三、MVVM(Model-View-ViewModel)四、示例代码4.1. MVC 示例4.2. MVP 示例4.3. MVVM 示例五、总结在Android开发中, MVC(Model-View-Controller)MVP(Model-View-Presenter)MVVM(Model-View-…...

多叉树的深度优先遍历(以电话号码的字母组合为例)

在我们的座机上&#xff0c;都有这种数字与字母对应的按键。 以此为例&#xff0c;讲解多叉树的深度优先遍历 问题 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同…...

【YashanDB数据库】PHP无法通过ODBC连接到数据库

【问题分类】驱动使用 【关键字】ODBC、驱动使用、PHP 【问题描述】应用使用php-fpmnginx架构&#xff0c;通过php的ODBC拓展连接YashanDB时出现报错&#xff1a; [unixODBC][Driver Manager]Cant open lib /home/yashandb_odbc/libyas_odbc.so: file not found但是在应用所…...

C++ | Leetcode C++题解之第326题3的幂

题目&#xff1a; 题解&#xff1a; class Solution { public:bool isPowerOfThree(int n) {return n > 0 && 1162261467 % n 0;} };...

Ubuntu20.4上搭建FFMPEG开发环境

编译ffmpeg命令如下: 1.安装yasm(ffmpeg里面有汇编语言的部分,所以需要安装一下yasm) wget 5http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz tar xvzf yasm-1.3.0.tar.gz cd yasm-1.3.0 ./configure make && make install 2.安装nasm(2.13以上…...

谷粒商城实战笔记-144-性能压测-性能监控-堆内存与垃圾回收

文章目录 一&#xff0c;两种类型的应用1&#xff0c;CPU密集型应用示例&#xff1a;Apache Spark 2&#xff0c;IO密集型应用示例&#xff1a;MySQL 二&#xff0c;监控 我们通过压力测试对接口进行了性能评估&#xff0c;以确定其是否满足性能要求。 如果不符合&#xff0c;就…...

大模型综述

《Harnessing the Power of LLMs in Practice: A Survey on ChatGPT and Beyond》论文阅读 模型架构 两种架构&#xff1a; encoder-decoder架构/encoder架构:T5/BERTdecoder架构:GPT4 特点LLMsencoder-decoderorencoder-onlyBERT-style训练:掩码语言模型类型&#xff1a;…...

Python 常用内置函数

目录 1、enumerate函数 1.1、for循环中使用 1.2、enumerate指定索引的起始值 1.3、enumerate在线程中的作用 2、Map 函数 2.1、map()函数可以传多个迭代器对象 3、lambda表达式&#xff08;匿名函数&#xff09; 示例 4、sort函数和sorted函数 4.1、sort()函数 4.2、…...

什么是大数据?

1. 大数据定义 大数据到底是什么&#xff1f; 大数据的定义是数据种类更多、数量更多、速度更快。这也被称为三个“V”。 简单来说&#xff0c;大数据是更大、更复杂的数据集&#xff0c;尤其是来自新数据源的数据集。这些数据集非常庞大&#xff0c;传统数据处理软件根本无…...

Linux 内核源码分析---资源分配及系统总线

资源管理 Linux提供通用的构架&#xff0c;用于在内存中构建数据结构。这些结构描述了系统中可用的资源&#xff0c;使得内核代码能够管理和分配资源。 其中关键的数据结构resource如下&#xff1a; 用于连接parent, child, sibling成员规则如下&#xff1a; 1、每个子结点只…...

C# POST请求 各种实现方法梳理

目录 1.首先是基础的参数 2.使用RestClient 3.使用封装库 4.使用微软原生库进行请求 5.使用HttpClient进行请求 C#代码中&#xff0c;实现Http/Https 中的POST请求&#xff0c;可以有很多种方式&#xff0c;下面就梳理下我常用的几种方式&#xff0c;给大家借鉴 1.首先…...

《MySQL数据库》数据导入、导出、表处理—/—<4>

一、插入数据 1、可使用外部工具navicat导入数据的情况下 因为部分公司不允许使用外部工具去导入数据 对于大批量数据&#xff0c;除了上节课中使用导入向导插入数据&#xff0c;也可在vscode中打开csv文件&#xff0c;然后选中光标&#xff0c;长按shiftctrl&#xff0c;拖动…...

业务系统对接大模型的基础方案:架构设计与关键步骤

业务系统对接大模型&#xff1a;架构设计与关键步骤 在当今数字化转型的浪潮中&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中&#xff0c;不仅可以优化用户体验&#xff0c;还能为业务决策提供…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘

美国西海岸的夏天&#xff0c;再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至&#xff0c;这不仅是开发者的盛宴&#xff0c;更是全球数亿苹果用户翘首以盼的科技春晚。今年&#xff0c;苹果依旧为我们带来了全家桶式的系统更新&#xff0c;包括 iOS 26、iPadOS 26…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)

Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败&#xff0c;具体原因是客户端发送了密码认证请求&#xff0c;但Redis服务器未设置密码 1.为Redis设置密码&#xff08;匹配客户端配置&#xff09; 步骤&#xff1a; 1&#xff09;.修…...

【JavaSE】多线程基础学习笔记

多线程基础 -线程相关概念 程序&#xff08;Program&#xff09; 是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码 进程 进程是指运行中的程序&#xff0c;比如我们使用QQ&#xff0c;就启动了一个进程&#xff0c;操作系统就会为该进程分配内存…...

代码规范和架构【立芯理论一】(2025.06.08)

1、代码规范的目标 代码简洁精炼、美观&#xff0c;可持续性好高效率高复用&#xff0c;可移植性好高内聚&#xff0c;低耦合没有冗余规范性&#xff0c;代码有规可循&#xff0c;可以看出自己当时的思考过程特殊排版&#xff0c;特殊语法&#xff0c;特殊指令&#xff0c;必须…...

【Linux系统】Linux环境变量:系统配置的隐形指挥官

。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量&#xff1a;setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...