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

Redisson分布式锁实现及原理详解

        随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。

场景分析:

     假如现在有个卫生间,里面只有一个坑位,此时A、B、C三名同学都想上厕所,A拉粑粑需要30s,B拉粑粑需要40s,C拉粑粑需要50s,于是乎A、B、C三名同学来到卫生间后,只有一名同学能够获得坑位的使用权。假设A先到的,进入坑位后将门关住,表示厕所有人,此时B和C只能在外面等待。但是会遇到如下几种情况:

        1、A在进入坑位上厕所时,一不小心掉进坑里了,由于A没有出来,导致门口的锁一直处于被锁住的状态,此时B和C由于锁未释放无法进去,因此只能无限等待,造成了资源的浪费。

        2、A进入厕所后对着B和C说我大概20s就好了(对应设置锁过期时间为20s的操作),20s后A还没有上完厕所,此时B或C看到锁释放了,便进入了厕所,导致A和他人共用厕所的情况发生。

        3、A在进入厕所后在门口加了一个锁,表示此时是A在厕所里,B和C此时看到A在厕所里,只能在外面等待,后来A上完厕所,释放A的锁,此时B进入厕所加了一个锁表示B在厕所里,但A把B的锁给释放掉了,会导致C以为厕所内现在没有人,C进入厕所,也会导致B和C共有厕所的情况。

以上三种情况对应于分布式锁要解决的三个问题:

        1、锁要设置过期时间,不能让某个线程长时间持有锁,会导致资源浪费。

        2、在方法未执行完成时,若锁过期,则需要延长锁的过期时间(看门狗机制),直至方法执行完毕。

        3、每个线程只能释放掉自己加的锁,不能释放掉其他线程获得锁,如果当前线程对应的锁不存在,说明该锁已过期,不做任何操作即可。

1. Redisson分布式锁

        Redisson基于Rediss的Java库,封装了常用功能(如数据缓存、消息队列等)以及分布式系统开发的工具,如分布式锁、分布式集合、分布式信号量、分布式执行器等,同时也封装了 Redis 中的常见数据结构,如 MapSetListQueueDeque 等。

示例代码:

@Scheduled(cron = "0 26 16 * * *")
public void doCacheRecommendUser() {
    RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
    try {
        //tryLock(long waitTime, long leaseTime, TimeUnit unit)
        //waitTime表示 等待获取锁的时间。如果设置为 0,意味着不会等待,会立即尝试获取锁。
        //leaseTime表示 锁的租约时间,即锁的有效时间。在 Redisson 中,如果设置为 -1,则表示 锁永不过期,除非显式解锁 (unlock()),否则锁会一直存在。
        if (lock.tryLock(0, -1, TimeUnit.MICROSECONDS)) {
            System.out.println("getLock: " + Thread.currentThread().getId());
            Thread.sleep(40000);
            for (Long userId : mainUserList) {
                QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
                String redisKey = String.format("yupao:user:recommend:%s", userId);
                ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
                // 写缓存
                try {
                    valueOperations.set(redisKey, userPage, 30000, TimeUnit.SECONDS);
                } catch (Exception e) {
                    log.error("redis set key error", e);
                }
            }
        }
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        //只能自己释放自己的锁
        if (lock.isHeldByCurrentThread()) {
            System.out.println("unlock: " + Thread.currentThread().getId());
            lock.unlock();
        }else{
            System.out.println("当前线程不持有锁,不能释放锁。");
        }
    }
}

分析:

        RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock")redissonClient.getLock("yupao:precachejob:docache:lock") 只是通过RedissonClient获取了一个名为"yupao:precachejob:docache:lock"锁对象(RLock但在实际使用之前,Redis中不会存储该锁的任何信息。

        只有当调用lock.lock()lock.tryLock()等方法时,才会在Redis中创建实际的锁

  lock.tryLock(0, -1, TimeUnit.MICROSECONDS)表示当前线程会尝试获取可重入lock锁,0表示立即尝试获取,不进行等待。-1表示锁的过期时间为-1(无限制),表明不手动设置过期时间,系统会为该锁默认设置30s的过期时间,如果方法实际的运行时间大于30s,会根据看门狗机制来为该锁续期,直至方法执行完毕,调用lock.unlock()方法来手动释放锁。

        而在释放锁的过程中需要判断当前线程持有的是哪个锁,每个线程只能释放自己加的锁,不能释放掉其他线程加的锁

        具体实现原理:redissonClient.getLock("yupao:precachejob:docache:lock")会获取一个名为"yupao:precachejob:docache:lock"锁对象(RLock),当调用lock.lock()lock.tryLock()方法时,会创建一个名为lock的map对象,这个map中的key由Redisson客户端id(UUID)和持有锁的线程id构成,value是锁重入计数。这样当释放锁时,先通过lock.isHeldByCurrentThread()来判断当前线程持有的锁是否是自己的,若是则释放,否则无法释放。

原理图:

2. 看门狗机制

        在分布式锁中,看门狗机制通常用于自动续期锁,以确保当任务执行时间超过预期时,锁不会意外过期和被其他进程或线程抢占。

两种锁使用方式对比:
1. 手动设置锁的过期时间(不会自动续期)

当你手动设置锁的过期时间时,例如:

RLock lock = redissonClient.getLock("myLock");
lock.lock(10, TimeUnit.SECONDS);  // 手动设置锁过期时间为10秒
try {
    // 执行任务
    Thread.sleep(20000);  // 模拟长时间任务(超过了锁的过期时间)
} finally {
    lock.unlock();  // 释放锁
}

        在上述例子中,锁的有效期是 10 秒,但任务执行时间是 20 秒。因此,锁会在任务执行期间被 Redis 自动释放,因为它达到了手动设置的过期时间。这时,其他进程或线程可能会获取锁,导致并发冲突。

2. 使用看门狗机制(自动续期锁)

        为了确保锁在任务执行过程中不会过期,可以不设置过期时间,这会启用 Redisson 的看门狗机制。看门狗机制会定期检查任务状态,并在任务未完成时自动续期锁的过期时间:

RLock lock = redissonClient.getLock("myLock");
lock.lock();  // 不设置过期时间,启用看门狗机制
try {
    // 执行任务
    Thread.sleep(20000);  // 模拟长时间任务
} finally {
    lock.unlock();  // 任务完成后显式释放锁
}

        在这种情况下,Redisson 的看门狗机制会在任务执行过程中每隔 10 秒自动续期锁的过期时间(默认是延长 30 秒),直到任务完成并手动释放锁。这样,即使任务执行时间超过了最初的锁的过期时间,锁仍然不会被其他线程抢占。

Redis 分布式锁中的看门狗工作原理

        当执行lock.tryLock(0, -1, TimeUnit.MICROSECONDS)时,当前线程会尝试获取锁,由于锁的过期时间设置为-1,因此会启用看门狗机制,此时该锁的默认过期时间为30000毫秒(30秒),看门狗机制会异步执行一个监听器,每隔internalLockLeaseTime/3之后执行,算下来就是大约10秒钟执行一次,如果当前方法未完成,则会延长锁的过期时间,直至方法完成释放锁。

        lock.tryLock(0, 30000, TimeUnit.MICROSECONDS),此时手动设置锁的过期时间为30000毫秒,当30000毫秒后,该锁会自动释放(无论方法是否执行完成),因此会导致线程不安全,引起并发问题。

  1. 锁的获取
    • 当某个客户端成功获取 Redis 分布式锁时,它会为锁设置一个默认的过期时间,例如 30 秒。这意味着如果客户端在 30 秒内没有释放锁,Redis 会自动释放锁,以避免死锁。
  1. 看门狗的启动
    • Redisson 在客户端成功获取锁后,会启动看门狗线程。
    • 默认情况下,这个看门狗会在锁的到期时间快到时自动续期锁的过期时间,例如每 10 秒检查一次是否需要将锁的过期时间延长 30 秒。
  1. 自动续期
    • 如果客户端仍在持有锁并且任务还没有完成,看门狗会自动续期,防止锁被 Redis 自动释放,从而确保任务可以在锁的保护下继续执行。
    • 如果任务执行时间超出了最初的 30 秒,看门狗会每隔 10 秒续期一次锁的过期时间,确保锁的有效性。
  1. 锁的释放
    • 一旦任务执行完成,客户端会显式调用 unlock() 方法释放锁,锁被释放后,看门狗线程会停止工作,锁的续期也随之停止。
    • 如果任务因为某些异常情况未能完成(如客户端崩溃),看门狗机制会确保锁在没有续期的情况下最终被 Redis 释放,防止锁被永远占用

3 RedLock解决Redis主从不一致性问题

场景:根据前面的分析解决了多个服务器之间的线程安全问题,防止了超卖和超买等问题,但是上述方案的前提都是有多个服务器,单Redis服务器的情况,此时所有的资源都存在一个主Redis服务器上,如果主服务器发生宕机,依然会导致线程不安全。

        针对于上述问题,当Redis是集群架构时,为防止主从服务器的锁不一致性问题,Redisson使用RedLock来实现,核心思想是:有N个Redis实例时,只有当N/2 + 1个Redis实例成功获得锁时,才表示当前锁获取成功,否则重新获取。

RedLock 的基本流程:
  1. 多实例锁获取
    • 系统中假设有 N 个 Redis 实例(推荐使用 3 或 5 个 Redis 实例)。客户端(如应用程序中的某个进程)会尝试依次向所有 Redis 实例获取同一个锁。
    • 客户端使用相同的锁键(例如 my-lock)和相同的过期时间来请求每个 Redis 实例。
  1. 设置锁的唯一标识
    • 每个锁请求会生成一个 唯一标识符(通常是 UUID),以确保不同的客户端或进程不会混淆彼此的锁请求。
  1. 获取多数锁
    • 客户端尝试在 N 个 Redis 实例中获取锁,只有当客户端能在超过 半数的 Redis 实例(即至少 N/2 + 1)中成功获得锁时,才认为锁获取成功。
    • 锁请求必须在设置的超时时间内完成(通常是 Redis 实例的网络延迟和 RTT 时间的几倍),以确保客户端不会因为网络或实例故障无限期等待锁。
  1. 锁的过期时间
    • 每个锁设置时都会有一个过期时间,以避免死锁。当客户端获取锁后,如果客户端崩溃或者没有手动释放锁,Redis 会在锁的过期时间到达时自动释放锁。
  1. 锁的释放
    • 一旦客户端完成了对共享资源的操作,它会向所有持有锁的 Redis 实例发送一个解锁命令来释放锁。
    • 只有持有该锁唯一标识符的客户端才能成功释放锁,防止其他客户端误释放不属于自己的锁。
RedLock 的过程总结为以下五步:
  1. 客户端从 N 个 Redis 实例中请求锁,并设置相同的键、值和过期时间。
  2. 客户端计算从每个 Redis 实例成功获取锁的时间。如果客户端能在大多数(即 N/2 + 1 个)Redis 实例中获取到锁,并且获取锁的总时间小于设定的超时时间,那么客户端成功获得了锁。
  3. 如果客户端在大多数实例中获取了锁,客户端可以进行对共享资源的操作。
  4. 如果锁超时后仍未成功获得足够多的实例锁,或者在操作过程中锁过期,则客户端应放弃该锁,等待下一次重试。
  5. 操作完成后,客户端会向所有 Redis 实例发出解锁命令,以释放锁。

http://www.kler.cn/news/307224.html

相关文章:

  • Visual Studio(vs)下载安装C/C++运行环境配置和基本使用注意事项
  • 深度学习之微积分预备知识点
  • HTTP 的请求方式有哪些和有什么区别
  • MATLAB 可视化基础:绘图命令与应用
  • C#笔记8 线程是什么?多线程怎么实现和操作?
  • Python精选200Tips:121-125
  • Spring Boot集成Akka Cluster实现在分布式节点中执行任务
  • Android中如何处理运行时权限?
  • 02 信念·组织力·战略 - 目标管理
  • [OpenCV] 数字图像处理 C++ 学习——15像素重映射(cv::remap) 附完整代码
  • 数据中台建设(六)—— 数据资产管理
  • uniapp 携带网址跳转webview页面报错解决
  • tp6.0.8反序列化漏洞的一些看法
  • Redis详细解析
  • (c++)猜数字(含根据当前时间生成伪随机数代码)
  • C++ Primer Plus(速记版)-面向对象与泛型编程
  • 浅谈 React Fiber
  • 关于less的基本使用
  • 【 html+css 绚丽Loading 】000050 乾坤合璧轮
  • 常用 Git 命令
  • c++ #include <string> 介绍
  • Java 之 IO流
  • Java读取寄存器数据的方法
  • memo和useMemo的区别
  • Js中的pick函数
  • 【Python基础】Python 装饰器(优雅的代码增强工具)
  • 如何通过Chrome浏览器轻松获取视频网站的TS文件
  • 什么是交换机级联?
  • 使用Python生成多种不同类型的Excel图表
  • HTML5元素定位