Redisson RedLock红锁加锁实现过程及原理

目录
  • 一、主从redis架构中分布式锁存在的问题
  • 二、红锁算法原理
  • 三、红锁算法的使用
  • 四、红锁加锁流程
  • 五、RedLock算法问题
  • 六、总结

本篇文章基于redisson-3.17.6版本源码进行分析

一、主从redis架构中分布式锁存在的问题

1、线程A从主redis中请求一个分布式锁,获取锁成功;

2、从redis准备从主redis同步锁相关信息时,主redis突然发生宕机,锁丢失了;

3、触发从redis升级为新的主redis;

4、线程B从继任主redis的从redis上申请一个分布式锁,此时也能获取锁成功;

5、导致,同一个分布式锁,被两个客户端同时获取,没有保证独占使用特性;

为了解决这个问题,redis引入了红锁的概念。

二、红锁算法原理

需要准备多台redis实例,这些redis实例指的是完全互相独立的Redis节点,这些节点之间既没有主从,也没有集群关系。客户端申请分布式锁的时候,需要向所有的redis实例发出申请,只有超过半数的redis实例报告获取锁成功,才能算真正获取到锁。

具体的红锁算法主要包括如下步骤:

1、应用程序获取当前系统时间(单位是毫秒);

2、应用程序使用相同的key、value依次尝试从所有的redis实例申请分布式锁,这里获取锁的尝试时间要远远小于锁的超时时间,防止某个master Down了,我们还在不断的获取锁,而被阻塞过长的时间;

3、只有超过半数的redis实例反馈获取锁成功,并且获取锁的总耗时小于锁的超时时间,才认为锁获取成功;

4、如果锁获取成功了,锁的超时时间就是最初的锁超时时间减去获取锁的总耗时时间;

5、如果锁获取失败了,不管是因为获取成功的redis节点没有过半,还是因为获取锁的总耗时超过了锁的超时时间,都会向已经获取锁成功的redis实例发出删除对应key的请求,去释放锁;

三、红锁算法的使用

在Redisson框架中,实现了红锁的机制,Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。当红锁中超过半数的RLock加锁成功后,才会认为加锁是成功的,这就提高了分布式锁的高可用。

使用的步骤如下:引入Redisson的maven依赖

<!-- JDK 1.8+ compatible -->
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.9.0</version>
</dependency> 

编写单元测试:

@Test
public void testRedLock() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    RedissonClient client1 = Redisson.create(config);
    RLock lock1 = client1.getLock("lock1");
    RLock lock2 = client1.getLock("lock2");
    RLock lock3 = client1.getLock("lock3");
    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
    try {
        /**
         * 4.尝试获取锁
         * redLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS)
         * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
         * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
         */
        // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
        boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
        if (res) {
            //成功获得锁,在这里处理业务
            System.out.println("成功获取到锁...");
        }
    } catch (Exception e) {
        throw new RuntimeException("aquire lock fail");
    } finally {
        // 无论如何, 最后都要解锁
        redLock.unlock();
    }
}

四、红锁加锁流程

RedissonRedLock红锁继承自RedissonMultiLock联锁,简单介绍一下联锁:

基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例,所有的锁都上锁成功才算成功。

RedissonRedLock的加锁、解锁代码都是使用RedissonMultiLock中的方法,只是其重写了一些方法,如:

failedLocksLimit():允许加锁失败节点个数限制。在RedissonRedLock中,必须超过半数加锁成功才能算成功,其实现为:

protected int failedLocksLimit() {
    return locks.size() - minLocksAmount(locks);
}
protected int minLocksAmount(final List<RLock> locks) {
    // 最小的获取锁成功数:n/2 + 1。 过半机制
    return locks.size()/2 + 1;
}

在RedissonMultiLock中,则必须全部都加锁成功才算成功,所以允许加锁失败节点个数为0,其实现为:

protected int failedLocksLimit() {
    return 0;
}

接下来,我们以tryLock()方法为例,详细分析红锁是如何加锁的,具体代码如下:

org.redisson.RedissonMultiLock#tryLock(long, long, java.util.concurrent.TimeUnit)

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//        try {
//            return tryLockAsync(waitTime, leaseTime, unit).get();
//        } catch (ExecutionException e) {
//            throw new IllegalStateException(e);
//        }
    long newLeaseTime = -1;
    if (leaseTime > 0) {
        if (waitTime > 0) {
            newLeaseTime = unit.toMillis(waitTime)*2;
        } else {
            newLeaseTime = unit.toMillis(leaseTime);
        }
    }
    // 获取当前系统时间,单位:毫秒
    long time = System.currentTimeMillis();
    long remainTime = -1;
    if (waitTime > 0) {
        remainTime = unit.toMillis(waitTime);
    }
    long lockWaitTime = calcLockWaitTime(remainTime);
    // 允许加锁失败节点个数限制(N - ( N / 2 + 1 ))
    // 假设有三个redis节点,则failedLocksLimit = 1
    int failedLocksLimit = failedLocksLimit();
    // 存放调用tryLock()方法加锁成功的那些redis节点
    List<RLock> acquiredLocks = new ArrayList<>(locks.size());
    // 循环所有节点,通过EVAL命令执行LUA脚本进行加锁
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        // 获取到其中一个redis实例
        RLock lock = iterator.next();
        String lockName = lock.getName();
        System.out.println("lockName = " + lockName + "正在尝试加锁...");
        boolean lockAcquired;
        try {
            // 未指定锁超时时间和获取锁等待时间的情况
            if (waitTime <= 0 && leaseTime <= 0) {
                // 调用tryLock()尝试加锁
                lockAcquired = lock.tryLock();
            } else {
                // 指定了超时时间的情况,重新计算获取锁的等待时间
                long awaitTime = Math.min(lockWaitTime, remainTime);
                // 调用tryLock()尝试加锁
                lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            }
        } catch (RedisResponseTimeoutException e) {
            // 如果抛出RedisResponseTimeoutException异常,为了防止加锁成功,但是响应失败,需要解锁所有节点
            unlockInner(Arrays.asList(lock));
            // 表示获取锁失败
            lockAcquired = false;
        } catch (Exception e) {
            // 表示获取锁失败
            lockAcquired = false;
        }
        if (lockAcquired) {
            // 如果当前redis节点加锁成功,则加入到acquiredLocks集合中
            acquiredLocks.add(lock);
        } else {
            // 计算已经申请锁失败的节点是否已经到达 允许加锁失败节点个数限制 (N-(N/2+1)), 如果已经到达,就认定最终申请锁失败,则没有必要继续从后面的节点申请了。因为 Redlock 算法要求至少N/2+1 个节点都加锁成功,才算最终的锁申请成功
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }
            if (failedLocksLimit == 0) {
                unlockInner(acquiredLocks);
                if (waitTime <= 0) {
                    return false;
                }
                failedLocksLimit = failedLocksLimit();
                acquiredLocks.clear();
                // reset iterator
                while (iterator.hasPrevious()) {
                    iterator.previous();
                }
            } else {
                failedLocksLimit--;
            }
        }
        // 计算 目前从各个节点获取锁已经消耗的总时间,如果已经等于最大等待时间,则认定最终申请锁失败,返回false
        if (remainTime > 0) {
            // remainTime: 锁剩余时间,这个时间是某个客户端向所有redis节点申请获取锁的总等待时间, 获取锁的中耗时时间不能大于这个时间。
            // System.currentTimeMillis() - time: 这个计算出来的就是当前redis节点获取锁消耗的时间
            remainTime -= System.currentTimeMillis() - time;
            // 重置time为当前时间,因为下一次循环的时候,方便计算下一个redis节点获取锁消耗的时间
            time = System.currentTimeMillis();
            // 锁剩余时间减到0了,说明达到最大等待时间,加锁超时,认为获取锁失败,需要对成功加锁集合 acquiredLocks 中的所有锁执行锁释放
            if (remainTime <= 0) {
                unlockInner(acquiredLocks);
                // 直接返回false,获取锁失败
                return false;
            }
        }
    }
    if (leaseTime > 0) {
        // 重置锁过期时间
        acquiredLocks.stream()
                .map(l -> (RedissonBaseLock) l)
                .map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
                .forEach(f -> f.toCompletableFuture().join());
    }
    // 如果逻辑正常执行完则认为最终申请锁成功,返回true
    return true;
}

从源码中可以看到,红锁的加锁,其实就是循环所有加锁的节点,挨个执行LUA脚本加锁,对于加锁成功的那些节点,会加入到acquiredLocks集合中保存起来;如果加锁失败的话,则会判断已经申请锁失败的节点是否已经到达允许加锁失败节点个数限制 (N-(N/2+1)), 如果已经到达,就认定最终申请锁失败,则没有必要继续从后面的节点申请了。

并且,每个节点执行完tryLock()尝试获取锁之后,无论是否获取锁成功,都会判断目前从各个节点获取锁已经消耗的总时间,如果已经等于最大等待时间,则认定最终申请锁失败,需要对成功加锁集合 acquiredLocks 中的所有锁执行锁释放,然后返回false。

五、RedLock算法问题

1、持久化问题

假设一共有5个Redis节点:A, B, C, D, E:

客户端1成功锁住了A, B, C,获取锁成功,但D和E没有锁住。

节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。

节点C重启后,客户端2锁住了C, D, E,获取锁成功。

这样,客户端1和客户端2同时获得了锁(针对同一资源)。

2、客户端长时间阻塞,导致获得的锁释放,访问的共享资源不受保护的问题。

3、Redlock算法对时钟依赖性太强, 若某个节点中发生时间跳跃(系统时间戳不正确),也可能会引此而引发锁安全性问题。

六、总结

红锁其实也并不能解决根本问题,只是降低问题发生的概率。完全相互独立的redis,每一台至少也要保证高可用,还是会有主从节点。既然有主从节点,在持续的高并发下,master还是可能会宕机,从节点可能还没来得及同步锁的数据。很有可能多个主节点也发生这样的情况,那么问题还是回到一开始的问题,红锁只是降低了发生的概率。

其实,在实际场景中,红锁是很少使用的。这是因为使用了红锁后会影响高并发环境下的性能,使得程序的体验更差。所以,在实际场景中,我们一般都是要保证Redis集群的可靠性。同时,使用红锁后,当加锁成功的RLock个数不超过总数的一半时,会返回加锁失败,即使在业务层面任务加锁成功了,但是红锁也会返回加锁失败的结果。另外,使用红锁时,需要提供多套Redis的主从部署架构,同时,这多套Redis主从架构中的Master节点必须都是独立的,相互之间没有任何数据交互。

到此这篇关于Redisson RedLock红锁加锁实现过程及原理的文章就介绍到这了,更多相关Redisson RedLock内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redisson公平锁的源码解读分享

    目录 前言 公平锁 加锁 解锁 总结 前言 我在上一篇文章聊了Redisson的分布式锁,这次继续来聊聊Redisson的公平锁.下面是官方原话: 它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程.所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒. 源码版本:3.17.7 这是我 fork 的分支,添加了自己理解的中文注释:https://g

  • Springboot基于Redisson实现Redis分布式可重入锁源码解析

    目录 一.前言 二.为什么使用Redisson 1.我们打开官网 2.我们可以看到官方让我们去使用其他 3.打开官方推荐 4.找到文档 三.Springboot整合Redisson 1.导入依赖 2.以官网为例查看如何配置 3.编写配置类 4.官网测试加锁例子 5.根据官网简单Controller接口编写 6.测试 四.lock.lock()源码分析 1.打开RedissonLock实现类 2.找到实现方法 3.按住Ctrl进去lock方法 4.进去尝试获取锁方法 5.查看tryLockInne

  • Redisson如何解决redis分布式锁过期时间到了业务没执行完问题

    目录 面试问题 问题分析 如何回答 一.写在前面 二.Redisson实现Redis分布式锁的底层原理 (1)加锁机制 (2)锁互斥机制 (3)watch dog自动延期机制 (4)可重入加锁机制 (5)释放锁机制 (6)上述Redis分布式锁的缺点 总结 面试问题 Redis锁的过期时间小于业务的执行时间该如何续期? 问题分析 首先如果你之前用Redis的分布式锁的姿势正确,并且看过相应的官方文档的话,这个问题So easy.我们来看 很多同学在用分布式锁时,都是直接百度搜索找一个Redis分

  • Redisson分布式锁的源码解读分享

    目录 前言 前置知识 分布式锁的思考 Redis订阅/发布机制 Redisson 加锁 订阅 解锁 看门狗 前言 Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid).Redisson有一样功能是可重入的分布式锁.本文来讨论一下这个功能的特点以及源码分析. 前置知识 在讲Redisson,咱们先来聊聊分布式锁的特点以及Redis的发布/订阅机制,磨刀不误砍柴工. 分布式锁的思考 首先思考下,如果我们自己去实现一个分布式锁,这个锁需要具备

  • Redisson可重入锁解锁逻辑详细讲解

    目录 主动释放 自动释放 本篇文章基于redisson-3.17.6版本源码进行分析 相比较Redisson可重入锁的加锁逻辑,释放锁的逻辑就相对简单一些.释放锁分为主动释放和自动释放两种方式. 主动释放 我们查看org.redisson.RedissonLock#unlock()方法: public void unlock() { try { get(unlockAsync(Thread.currentThread().getId())); } catch (RedisException e)

  • Redisson分布式信号量RSemaphore的使用超详细讲解

    目录 一.RSemaphore的使用 二.RSemaphore设置许可数量 三.RSemaphore的加锁流程 四.RSemaphore的解锁流程 本篇文章基于redisson-3.17.6版本源码进行分析 一.RSemaphore的使用 @Test public void testRSemaphore() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379&quo

  • Go与Redis实现分布式互斥锁和红锁

    目录 前言 互斥锁 TryLock和Unlock实现 Lock实现 实现看门狗机制 看门狗实现 红锁 加锁实现 看门狗实现 解锁实现 前言 在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,这篇文章主要是使用Go+Redis实现互斥锁和红锁. 下面的代码使用go-redis客户端和gofakeit库. 代码地址 互斥锁 Redis里有一个设置如果不存在的命令,我们可以通过这个命令来实现互斥锁功能,在Redis官方文档里面推荐的标准实现方式是SET resour

  • Redis中Redisson红锁(Redlock)使用原理

    目录 简介 为什么使用Redis的红锁 解决方案:使用红锁 Redisson红锁实例 Redisson红锁原理 参考文章 简介 说明 本文介绍为什么要使用Redis的红锁(Redlock).什么是Redis的红锁以及Redis红锁的原理. 本文用Redisson来介绍Redis红锁的用法. Redisson 高版本会根据redisClient的模式来决定getLock返回的锁类型,如果集群模式,满足红锁的条件,则会直接返回红锁. 官网 REDIS distlock -- Redis中国用户组(C

  • Java编程redisson实现分布式锁代码示例

    最近由于工作很忙,很长时间没有更新博客了,今天为大家带来一篇有关Redisson实现分布式锁的文章,好了,不多说了,直接进入主题. 1. 可重入锁(Reentrant Lock) Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁. public void testReentrantLock(RedissonClient redisson){ RLock lock = redisson.getL

  • Redis分布式锁之红锁的实现

    目录 一.问题 二.办法 三.原理 四.实战 一.问题 分布式锁,当我们请求一个分布式锁的时候,成功了,但是这时候slave还没有复制我们的锁,masterDown了,我们的应用继续请求锁的时候,会从继任了master的原slave上申请,也会成功. 这就会导致,同一个锁被获取了不止一次. 二.办法 Redis中针对此种情况,引入了红锁的概念. 三.原理 用Redis中的多个master实例,来获取锁,只有大多数实例获取到了锁,才算是获取成功.具体的红锁算法分为以下五步: 获取当前的时间(单位是

  • Redisson实现分布式锁、锁续约的案例

    目录 一.基础 0)Redisson版本说明.案例 1)Redisson连接Redis的方式 2)用到的Redis命令 3)用到的lua脚本语义 二.源码分析 1.RLock 2.加锁流程 3.释放锁流程 一.基础 0)Redisson版本说明.案例 使用当前(2022年12月初)最新的版本:3.18.1: <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifa

  • SpringBoot集成Redisson实现分布式锁的方法示例

    上篇 <SpringBoot 集成 redis 分布式锁优化>对死锁的问题进行了优化,今天介绍的是 redis 官方推荐使用的 Redisson ,Redisson 架设在 redis 基础上的 Java 驻内存数据网格(In-Memory Data Grid),基于NIO的 Netty 框架上,利用了 redis 键值数据库.功能非常强大,解决了很多分布式架构中的问题. Github的wiki地址: https://github.com/redisson/redisson/wiki 官方文档

  • SpringBoot使用Redisson实现分布式锁(秒杀系统)

    前面讲完了Redis的分布式锁的实现,接下来讲Redisson的分布式锁的实现,一般提及到Redis的分布式锁我们更多的使用的是Redisson的分布式锁,Redis的官方也是建议我们这样去做的.Redisson点我可以直接跳转到Redisson的官方文档. 1.1.引入Maven依赖 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter&l

  • Springboot中如何使用Redisson实现分布式锁浅析

    目录 前言 1. 概述 2. Redisson 在 Springboot 中的使用 2.1 引入依赖 2.2 在 Springboot 配置中配置Redis 2.3 Demo代码 3. 综述 前言 在分布式场景下为了保证数据最终一致性.在单进程的系统中,存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步(lock-synchronized),使其在修改这种变量时能够线性执行消除并发修改变量.但分布式系统是多部署.多进程的,开发语言提供的并发处理API在此场景下就无能为

  • SpringBoot整合Redisson实现分布式锁

    目录 一.添加依赖 二.redis配置文件 三.新建配置类 四.使用分布式锁 可重入锁 读写锁 信号量(Semaphore) 闭锁(CountDownLatch) Redisson是架设在redis基础上的一个Java驻内存数据网格(In-Memory Data Grid).充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类.使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低

  • 浅谈Java锁的膨胀过程以及一致性哈希对锁膨胀的影响

    目录 1.锁优化 1.1.锁消除 1.2.锁粗化 1.3.自旋锁 1.4.自适应自旋锁 1.5.锁膨胀 2.锁膨胀实战 2.1.jol工具 2.2.锁膨胀测试代码 2.3.输出分析 2.4.锁释放 3.一致性哈希对锁膨胀的影响 4.锁性能测试 1.锁优化 在JDK6之前,通过synchronized来实现同步效率是很低的,被synchronized包裹的代码块经过javac编译后,会在代码块前后加上monitorenter和monitorexit字节码指令,被synchronized修饰的方法则

随机推荐