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

news2024/9/23 17:21:56

        随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 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.coloradmin.cn/o/2126751.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Makefile中的override

若对变量进行赋值一部分需要由命令行&#xff08;注意命令行只能使用/:/进行赋值&#xff0c;不能使用&#xff1f;进行赋值&#xff09;输入完成&#xff0c;一部分需要写在Makefile文件里&#xff0c;Makefile规则默认会让命令行的赋值覆盖文件里的赋值。要想解决这个问题&am…

【源码+文档+调试讲解】高校研招信息共享系统

摘 要 近年来&#xff0c;科技飞速发展&#xff0c;在经济全球化的背景之下&#xff0c;互联网技术将进一步提高社会综合发展的效率和速度&#xff0c;互联网技术也会涉及到各个领域&#xff0c;而高校研招信息共享系统在网络背景下有着无法忽视的作用。信息管理系统的开发是…

log4j靶场,反弹shell

1.用vulhub靶场搭建&#xff0c;首先进入目录CVE-2021-44228中&#xff0c;docker启动命令 2.发现端口是8983&#xff0c;浏览器访问http://172.16.1.18:8983/ 3.用dnslog平台检测dns回显&#xff0c;看看有没有漏洞存在 4.反弹shell到kali&#xff08;ip为172.16.1.18&#xf…

【白话Spring】三级缓存

快速导航 一、Spring的三级缓存是什么&#xff1f;三级缓存SpringBean 的生命周期&#xff1a;BeanFactory关于Bean初始化注释&#xff1a;分析&#xff1a;Bean的创建过程&#xff1a;Bean的销毁过程&#xff1a; Spring Bean创建的核心逻辑&#xff1a; 二、Spring的三级缓存…

CentOS7 使用yum报错:[Errno 14] HTTP Error 404 - Not Found 正在尝试其它镜像。

CentOS7 使用yum报错&#xff1a;[Errno 14] HTTP Error 404 - Not Found 正在尝试其它镜像。 CentOS镜像下载、VM虚拟机下载 下载地址&#xff1a;www.macfxb.cn 一、问题描述 安装完CentOS7 后 使用yum报错 如下图 二、解决方案 1.查看自己的系统架构 我的是aarch64 uname …

【MySQL】查询语句之inner、left、right、full join 的区别

前言&#xff1a; INNER JOIN 和 OUTER JOIN 是SQL中常用的两种连接方式&#xff0c;用于从两表活多表中提取相关的数据。两者区别主要在于返回的 结果集 如何处理 匹配 与 不匹配 的行。 目录 1、INNER JOIN 2、OUTER JOIN 3、总结 1、INNER JOIN 称为内连接&#xff0c;只…

ComfyUI-AdvancedLivePortrait:实时编辑人脸让图像动起来

经常使用Stable Diffiusion的朋友都知道&#xff0c;webUI和comfyUI底层都是Stable Diffiusion&#xff0c;但是它们的显示界面有非常大的区别&#xff1a;webUI界面简洁&#xff0c;新手比较容易上手&#xff1b;而ComfyUI 是采用基于节点的图形界面&#xff0c;通过连接不同的…

面向 NetworkX 用户的加速生产就绪型图形分析

目录 借助 NetworkX 轻松进行图形分析 使用 cuGraph 加速图形分析 使用 ArangoDB 进行生产就绪型图形分析 借助 cuGraph 和 ArangoDB 实现 GPU 加速分析 实施示例 测试环境 下载数据 创建 NetworkX 图形 在不使用 ArangoDB 的情况下运行 cuGraph 算法 将 NetworkX 图…

【Qt 即时通讯项目】登录验证码是如何做到的呢

文章目录 1. 登录注册功能验证码实现2. 验证码生成的流程3. 细节部分 1. 登录注册功能验证码实现 &#x1f427;①目的&#xff1a;引入验证码&#xff0c;目的是用来避免程序被其它程序暴力破解的方式找到密码。 2. 验证码生成的流程 ①&#x1f34e;首先通过Qt的QRandomGen…

初始Linux 和 各种常见指令

目录 Linux背景 1. 发展史 Linux发展历史 1.历史 2. 开源 Linux下基本指令 01. ls 指令 02. pwd命令 03. cd 指令 04. touch指令 05.mkdir指令&#xff08;重要&#xff09;&#xff1a; 06.rmdir指令 && rm 指令&#xff08;重要&#xff09;&#xff1a; …

OpenCV结构分析与形状描述符(20)计算一个包围给定点集的最小外接圆函数minEnclosingCircle()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 找到一个包围二维点集的最小面积的圆。 该函数使用迭代算法来寻找一个二维点集的最小外接圆。这意味着函数将会通过反复逼近的过程来计算出能够…

【Shiro】Shiro 的学习教程(四)之 SpringBoot 集成 Shiro 原理

目录 1、第一阶段&#xff1a;启动服务&#xff0c;构建类的功能2、第二阶段&#xff1a;正式请求 1、第一阶段&#xff1a;启动服务&#xff0c;构建类的功能 查看 Shiro 配置类 ShiroConfiguration&#xff1a; Configuration public class ShiroConfiguration {// 创建 sh…

算法里面的离散化

一、离散化&#xff08;discretization&#xff09;在算法和数据结构中指的是将连续的输入数据映射到离散的值或者范围&#xff0c;从而使得处理和计算变得更高效。通常用于处理大范围或者无限可能的输入&#xff0c;以便将其转化为有限的、可以有效处理的范围。 离散化的定义…

opencv之傅里叶变换

文章目录 前言理论基础Numpy实现傅里叶变换实现傅里叶变换实现逆傅里叶变换 高通滤波示例OpenCV实现傅里叶变换实现傅里叶变换实现逆傅里叶变换 低通滤波示例 前言 图像处理一般分为空间域处理和频率域处理。 空间域处理是直接对图像内的像素进行处理。空间域处理主要划分为灰…

Mysql基础练习题 1757.可回收且低脂的产品(力扣)

编写解决方案找出既是低脂又是可回收的产品编号。 题目链接&#xff1a; https://leetcode.cn/problems/recyclable-and-low-fat-products/description/ 建表插入数据&#xff1a; Create table If Not Exists Products (product_id int, low_fats ENUM(Y, N), recyclable …

Kernel 内核 BUG_ON()和WARN_ON()

WARN_ON() DEBUG_ON() 不是一个标准的 Linux 内核宏&#xff0c;它可能是特定内核版本或者特定内核配置中的一个宏&#xff0c;或者在某些内核代码中自定义的宏。一般来说&#xff0c;如果存在 DEBUG_ON()&#xff0c;它可能被用作一个调试开关&#xff0c;用于在调试版本中启…

2024.9.11

时钟 头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPaintEvent> #include <QTimer> #include <QPainter> #include <QPen> #include <QBrush> #include <QTime> #include <QDebug> QT_BEGIN_NA…

国内如何优雅的用Google?无需安装任何工具!

前言 Google可以说时每个程序猿的标配&#xff0c;但由于网络问题&#xff0c;在访问Google的时候或多或少都会遇到一系列的问题&#xff0c;接下来介绍一个非常“炸裂”的Google打开方式。 LiteIcoding 在此之前&#xff0c;你需要访问这个地址 https://lite.icoding.ink 这…

【Qt】QSS的设置方式

QSS的设置方式 QWidget 中包含了 setStyleSheet ⽅法, 可以直接设置样式. 上述代码我们已经演⽰了上述设置⽅式 还可以通过 QApplication 的 setStyleSheet ⽅法设置整个程序的全局样式. 设置全局样式&#xff0c;可以将界面上所有的样式都集中到一起来组织。 全局样式优点:…

56 - II. 数组中数字出现的次数 II

comments: true difficulty: 中等 edit_url: https://github.com/doocs/leetcode/edit/main/lcof/%E9%9D%A2%E8%AF%95%E9%A2%9856%20-%20II.%20%E6%95%B0%E7%BB%84%E4%B8%AD%E6%95%B0%E5%AD%97%E5%87%BA%E7%8E%B0%E7%9A%84%E6%AC%A1%E6%95%B0%20II/README.md 面试题 56 - II. 数…