对Redis锁延期的一些讨论与思考

news2024/11/19 23:14:56

上一篇文章提到使用针对不同的业务场景如何合理使用Redis分布式锁,并引入了一个新的问题

若定义锁的过期时间是10s,此时A线程获取了锁然后执行业务代码,但是业务代码消耗时间花费了15s。这就会导致A线程还没有执行完业务代码,A线程却释放了锁(因为10s到了),第11s B线程发现锁已经释放,重新获取锁也开始执行业务代码。
此时多个线程同时执行业务代码,我们使用锁就是为了保证仅有一个线程执行这一块业务代码,说明这个锁是失效的!

本文将尝试探讨如何处理这个问题!

在这里插入图片描述

下面这个图解释了重置超时时间是什么意思,写一个定时任务,并单独使用一个线程每3s去检查一下是否到终点(任务是否执行完毕),第3s时发现没到终点,重置时间。 假设任务执行完毕需要花费11s。那么锁一共会延期3次,第11s之后,锁被手动释放,如果没释放。等到第19s时,会被自动释放。
在这里插入图片描述
如何实现锁的延期

伪代码:

定义锁的结构
key:uuid
value:订单服务

if key(锁的唯一标识)是否存在
	存在,if 锁是否被修改
		未修改,重置超时时间

这部分有一点需要解释:

  1. 为什么判断锁是否被修改?
    A线程获取了锁之后,B线程修改锁的value为 “文件下载服务”,不加一层校验,A线程就会对修改后的锁操作,而不是原始的锁。

此时你会直接写一个定时任务去实现,会有什么问题吗?
锁延期分为2步(第一步:判断锁;第二步:重置锁),这2步之间是存在间隙的,完全可以在判断锁后,重置锁前发生一些事情(例如恰巧在重置时间前锁被其他线程修改了)。如何才能避免这个间隙不发生意外?

使用lua脚本:使用lua语法实现锁的延期,然后执行这个脚本。lua语法将这两个步骤绑定成一个操作。这也就是为什么提到锁延期的实现,基本都是采用lua实现的根本原因。redis分布式锁自身是有局限性的,不能满足我们的需求,所以我们提出了锁延期。

巧在Redis很支持lua语法,我们只需要按照lua语法要求写好命令,调用Redis提供的方法入口传进去,Redis会自动解析这些命令。更巧在lua语法实现锁延期解决了上面的隐患。。。

        /**
         * 锁续期
         */
        if (redis.call('exists', KEYS[1]) == 1) then // 锁还存在
              if (redis.call('get', KEYS[1]) == ARGV[1]) then 
                   redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间
                       return 1
              end
        end
        return 0

接下来完整的看一下如何使用Redis锁延期


/**
 * redis分布式锁
 * 为了文件拉取加的,可能存在拉取任务耗时很久的情况,增加锁延时操作
 * @author lixinyu
 */
public class RedisDistributeLock {
    private static final Logger log = LoggerFactory.getLogger(RedisDistributeLock.class);

    // 默认30秒后自动释放锁
    private static long defaultExpireTime = 10 * 60 * 1000; // 默认10分钟

    // 用于锁延时任务的执行
    private static ScheduledThreadPoolExecutor renewExpirationExecutor;

    // 加锁和解锁的lua脚本 重入和不可重入两种
    private static String lockScript;
    private static String unlockScript;
    private static String renewScript;// 锁延时脚本
    private static String lockScript_reentrant;
    private static String unlockScript_reentrant;
    private static String renewScript_reentrant;// 锁延时脚本

    static {
        /**
         * 如果指定的锁键(KEYS[1])不存在,则通过set命令设置锁的值(ARGV[1])和超时时间(ARGV[2])。
         * 如果锁键已存在,则通过pttl命令返回锁的剩余超时时间。
         */
        StringBuilder sb = new StringBuilder();
        sb.setLength(0);
        sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 如果不存在这个lockKey锁
        sb.append("     redis.call('set', KEYS[1], ARGV[1]) ");// 设置锁 ,key-value结构
        sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 设置锁超时时间
        sb.append("     return nil ");
        sb.append(" end ");
        sb.append(" return redis.call('pttl', KEYS[1]) ");// 如果别的线程已经加锁,返回剩余时间
        lockScript = sb.toString();

        /**
         * 如果锁存在,则删除锁
         */
        sb.setLength(0);
        sb.append(" if (redis.call('get', KEYS[1]) == ARGV[1]) then ");
        sb.append("      return redis.call('del', KEYS[1]) ");
        sb.append(" else return 0 ");
        sb.append(" end");
        unlockScript = sb.toString();

        /**
         * 可重入锁主要解决的是同一个线程能够多次获取锁的问题,而不是防止多个线程同时获取锁
         * 这通常发生在方法递归调用、嵌套调用或者同一个方法内部多次执行加锁操作的情况下
         */
        sb.setLength(0);
        sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 如果不存在这个lockKey锁
        sb.append("     redis.call('hset', KEYS[1], ARGV[1], 1) ");// 设置锁 ,hash结构,hashkey为当前线程id,加锁数为1
        sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 设置锁超时时间
        sb.append("     return nil ");
        sb.append(" end ");
        sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then ");// 如果当前线程已经加锁
        sb.append("     redis.call('hincrby', KEYS[1], ARGV[1], 1) ");// 可重入,增加锁计数
        sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重设置锁超时时间
        sb.append("     return nil ");
        sb.append(" end ");
        sb.append(" return redis.call('pttl', KEYS[1]) ");// 如果别的线程已经加锁,返回剩余时间
        lockScript_reentrant = sb.toString();

        /**
         * 释放锁,通过判断锁的存在、当前线程是否是加锁的线程、以及锁的计数器等条件来实现解锁的操作
         */
        sb.setLength(0);
        sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 不存在锁,返回1表示解锁成功
        sb.append("     return 1 ");
        sb.append(" end ");
        sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then ");// 存在锁,不是本人加的,返回0失败
        sb.append("     return 0 ");
        sb.append(" end ");
        sb.append(" local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1) ");// 存在自己加的锁,锁计数减一
        sb.append(" if (counter > 0) then ");// 判断是否要删除锁,或重置超时时间
        sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");
        sb.append("     return 0 ");
        sb.append(" else ");
        sb.append("     redis.call('del', KEYS[1]) ");
        sb.append("     return 1 ");
        sb.append(" end ");
        sb.append(" return nil ");
        unlockScript_reentrant = sb.toString();

        /**
         * 锁续期
         */
        sb.setLength(0);
        sb.append(" if (redis.call('exists', KEYS[1]) == 1) then ");// 锁还存在
        sb.append("     if (redis.call('get', KEYS[1]) == ARGV[1]) then ");
        sb.append("        redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间
        sb.append("        return 1");
        sb.append("     end ");
        sb.append(" end ");
        sb.append(" return 0 ");
        renewScript = sb.toString();

        /**
         * 可重入锁续期
         */
        sb.setLength(0);
        sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then ");// 锁还存在
        sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间
        sb.append("     return 1 ");
        sb.append(" end ");
        sb.append(" return 0 ");
        renewScript_reentrant = sb.toString();

        renewExpirationExecutor = new ScheduledThreadPoolExecutor(2);
    }


    private String uuid;// 当前锁对象标识
    private boolean reentrant;// 当前锁是可重入还是不可重入
    private RedisUtils redisUtils;

    public RedisDistributeLock(boolean reentrant) {
        this.uuid = UUIDUtils.randomUUID8();
        this.reentrant = reentrant;
        this.redisUtils = SpringApplicationUtils.getBean(RedisUtils.class);
    }

    /**
     * 尝试对lockKey加锁
     * @author: lixinyu 2023/4/25
     **/
    public boolean tryLock(String lockKey) {
        String script = lockScript;
        if (reentrant) {
            script = lockScript_reentrant;
        }

        Object result = redisUtils.evalScript(script, ReturnType.INTEGER, 1, lockKey, uuid, String.valueOf(defaultExpireTime));
        boolean isSuccess = result == null;
        if (isSuccess) {
            // 若成功,增加延时任务
            scheduleExpirationRenew(lockKey, uuid, reentrant);
        }

        return isSuccess;
    }

    /**
     * 解锁
     * @author: lixinyu 2023/4/25
     **/
    public void unlock(String lockKey){
        if (reentrant) {
            redisUtils.evalScript(unlockScript_reentrant, ReturnType.INTEGER, 1, lockKey, uuid, String.valueOf(defaultExpireTime));
        } else {
            redisUtils.evalScript(unlockScript, ReturnType.INTEGER, 1, lockKey, uuid);
        }
    }

    /**
     * 锁延时,定时任务队列,定时判断一次是否续期
     */
    private void scheduleExpirationRenew(String lockKey, String lockValue, boolean reentrant) {
        Runnable renewTask = new Runnable(){

            @Override
            public void run() {
                try {
                    String script = renewScript;
                    if (reentrant) {
                        script = renewScript_reentrant;
                    }
					// 将lua语法传给redis解析
                    Object result = evalScript(script, ReturnType.INTEGER, 1, lockKey, lockValue, String.valueOf(defaultExpireTime));
                    if (result != null && !result.equals(false) && result.equals(Long.valueOf(1))) {
                        // 延时成功,再定时执行
                        scheduleExpirationRenew(lockKey, lockValue, reentrant);

                        log.info("redis锁【" + lockKey + "】延时成功!");
                    }
                } catch (Exception e) {
                    log.error("scheduleExpirationRenew run异常", e);
                }
            }
        };

        renewExpirationExecutor.schedule(renewTask, defaultExpireTime / 3, TimeUnit.MILLISECONDS);
    }
}

 /**
  *  将lua语法传给redis
  */ 
 public Object evalScript(String script, ReturnType returnType, int numKeys,
                             String... keysAndArgs)
    {
        Object value = false;
        try
        {
            value = redisTemplate.execute((RedisCallback<Object>)conn -> {
                try
                {
                    byte[][] keysAndArgsByte = new byte[keysAndArgs.length][];
                    for (int i = 0; i < keysAndArgs.length; i++ )
                    {
                        keysAndArgsByte[i] = redisTemplate.getStringSerializer().serialize(keysAndArgs[i]);
                    }
                    return conn.eval(redisTemplate.getStringSerializer().serialize(script), returnType, numKeys,
                            keysAndArgsByte);
                }
                catch (SerializationException e)
                {
                    log.error("异常", e);
                    return false;
                }
            });
        }
        catch (Exception e)
        {
           log.error("异常", e);
        }
        return value;
    }

使用锁

 private void demo() {
            RedisDistributeLock lock = new RedisDistributeLock(false);
            String lockKey = redisSeqPrefix + "lock:" + seqName;
            try {
                if (lock.tryLock(lockKey)) {
                    String redisValue = redisUtils.get(redisSeqPrefix + seqName);

                    // 加锁之后再次判断是否超出规定长度,防止并发时重置多次
                    if (redisValue != null && redisValue.length() > seqLength) {
                        redisUtils.set(redisSeqPrefix + seqName, "1");
                    }
                }
            } catch (Exception e) {
                logger.error("resetSeqValue异常", e);
            } finally {
                lock.unlock(lockKey);
            }
        }

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

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

相关文章

C++案例->评委打分、员工分组

#include<iostream> using namespace std; #include<vector> #include<string> #include<deque> #include<algorithm> #include<ctime> /* 有5名选手&#xff1a;选手ABCDE&#xff0c;10个评委分别对每一名选手打分&#xff0c;去除最高分…

ACL权限管理

一&#xff0c;简介 ACL是Access Control List的缩写&#xff0c;即访问控制列表。可以通过下列的实例来理解ACL的作用&#xff1a; 二&#xff0c;操作步骤 1. 添加测试目录&#xff0c;用户&#xff0c;组&#xff0c;并将用户添加到组&#xff08;创建zs,ls添加到tgroup组中…

STM32F103x 的时钟源

AHB (Advanced High-performance Bus) 高速总线&#xff0c;用来接高速外设的。 APB (Advanced Peripheral Bus) 低速总线&#xff0c;用来接低速外设的&#xff0c;包含APB1 和 APB2。 APB1&#xff1a;上面连接的是低速外设&#xff0c;包括电源接口、备份接口、 CAN 、 US…

爬取m3u8视频

网址&#xff1a;https://www.bhlsm.com/cupfoxplay/609-3-1/ 相关代码&#xff1a; #采集网址&#xff1a;https://www.bhlsm.com/cupfoxplay/609-3-1/ #正常视频网站&#xff1a;完整视频内容 # pip install pycryptodomex #流媒体文件&#xff1a;M3U8&#xff08;把完整的…

计网运输层

文章目录&#xff1a; 文章目录 概述运输层端口号、复用与分用UDP与TCP对比UDPTCP流量控制拥塞控制拥塞控制算法慢开始(slow-start)拥塞避免(congestion avoidance)快重传(fast retransmit)快恢复(fast recovery) 超时重传时间选择可靠传输实现运输连接管理建立连接连接释放 首…

台式电脑黑屏无法开机怎么办 电脑开机黑屏的解决方法

经常有朋友电脑一开机&#xff0c;发现电脑黑屏没法用了。很多人看到黑屏就懵了&#xff0c;以为电脑要报废了&#xff0c;这是什么原因?电脑开机黑屏怎么解决?一般常说的黑屏故障分为两种&#xff0c;显示屏没有任何显示以及显示英文。下面小编要为大家带来的是台式电脑黑屏…

HTMLElement.click()的回调触发踩坑

先看看以下代码 const el document.getElementById("btn") el.addEventListener("click", () > {Promise.resolve().then(() > console.log("microtask 1"));console.log("1"); }); el.addEventListener("click", (…

基于机器学习、遥感和Penman-Monteith方程的农田蒸散发混合模型研究_刘燕_2022

基于机器学习、遥感和Penman-Monteith方程的农田蒸散发混合模型研究_刘燕_2022 摘要关键词 1 绪论2 数据与方法2.1 数据2.2 机器学习算法2.3 Penman-Monteith方程2.4 Medlyn公式2.5 模型性能评估 3 基于机器学习算法的混合模型估算农田蒸散量的评价与比较4 利用人工神经网络算法…

信息矩阵、hessian矩阵与协方差矩阵

文章目录 协方差矩阵联合概率密度hessian矩阵marginalize 本节探讨信息矩阵、hessian矩阵与协方差矩阵的关系&#xff0c;阐明边缘化的原理。 一个简单的示例&#xff0c;如下&#xff1a; 来自 David Mackay. “The humble Gaussian distribution”. In: (2006). 以及手写vio第…

CSS基础(下)

一 CSS样式重置 【面试题】&#xff1a;你知道浏览器的兼容性问题有哪些&#xff1f;你进行过样式重置吗&#xff1f;进行过样式标准化吗&#xff1f; 样式重置reset/样式标准化normalize /*******第1步:样式重置(标准化):将浏览器提供的默认样式统一化 实用化***…

UnityWebGL 设置全屏

这是Unity导出Web默认打开的页面尺寸 修改后效果 修改 index.html 文件 1.div元素的id属性值为"unity-container"&#xff0c;宽度和高度都设置为100%&#xff0c;意味着该div元素将占据整个父容器的空间。canvas元素的id属性值为"unity-canvas"&#xff…

CAN转WIFI

一、 产品概述 SG-CAN-WIFI 是专为 CAN 总线网络与无线 IP 网络&#xff08;WLAN 或 Wi-Fi&#xff09;之 间或多个 CAN 总线网络之间通过无线 IP 网络&#xff08;WLAN 或 Wi-Fi&#xff09;传输 CAN 总 线数据而设计&#xff0c;无线 IP 网络&#xff08;WLAN 或 Wi-Fi&…

计算机网络面经-从浏览器地址栏输入 url 到显示主页的过程?

大概的过程比较简单&#xff0c;但是有很多点可以细挖&#xff1a;DNS解析、TCP三次握手、HTTP报文格式、TCP四次挥手等等。 DNS 解析&#xff1a;将域名解析成对应的 IP 地址。TCP连接&#xff1a;与服务器通过三次握手&#xff0c;建立 TCP 连接向服务器发送 HTTP 请求服务器…

JavaSE-05笔记【面向对象02】

文章目录 1. 类之间的关系2. is-a、is-like-a、has-a2.1 is-a2.2 is-like-a2.3 has-a 3. Object类3.1 toString()3.2 finalize()&#xff08;了解即可&#xff09;3.3 与 equals 方法 4. package 和 import4.1 package4.2 import4.3 JDK 常用开发包 5. 访问权限控制5.1 privat…

CLion的bundled MinGW能用在VSCode上吗?

跟着前辈做一个项目&#xff0c;用的极海的MCU&#xff0c;主要用到SPI和USB功能。 官方提供的SDK中的例程有 Eclipse/ Keil/ IAR 版本。 前辈根据Eclipse版本的工程信息文件&#xff08;.project 和.cproject&#xff09; 看里面链接到了哪些文件&#xff0c;然后自己手动写…

Ansible service 模块 该模块用于服务程序的管理

目录 参数将服务设置为 自启动检查端口关闭服务再次查看端口 参数 arguments #命令行提供额外的参数 enabled #设置开机启动。 name #服务名称 runlevel #开机启动的级别&#xff0c;一般不用指定。 sleep #在重启服务的过程中&#xff0c;是否等待。如在服务关闭以后等待2秒再…

gnss尾矿库安全监测系统是什么

【TH-WY1】GNSS尾矿库安全监测系统是一种利用全球导航卫星系统&#xff08;GNSS&#xff09;技术对尾矿库进行安全监测的系统。尾矿库是矿山企业的重要设施之一&#xff0c;用于存放矿山开采过程中产生的尾矿。由于尾矿库具有高能势和复杂的地质环境&#xff0c;存在溃坝、滑坡…

Python:函数

目录 前言&#xff1a; 一、函数的定义 二、函数的调用 三、函数的分类 四、全局变量和局部变量 五、函数的参数 5.1 位置参数 5.2 默认值参数 5.3 可变参数 5.4 关键字参数 5.5 命名关键字参数 5.6 参数的组合 六、函数的递归 前言&#xff1a; 函数就是一个过程…

nginx+keepalived实现nginx高可用集群以及nginx实现Gateway网关服务集群

一、前言 1、简介 Nginx作为一款高性能的Web服务器和反向代理服务器&#xff0c;被广泛使用。且现如今很多高并发场景需要后端服务集群部署&#xff0c;因此nginx也需要支持集群部署从而避免单点故障的问题。 本文将详细介绍使用 KeepalivedNginx 来实现Nginx的高可用集群和N…

Leetcode2583. 二叉树中的第 K 大层和

Every day a Leetcode 题目来源&#xff1a;2583. 二叉树中的第 K 大层和 解法1&#xff1a;层序遍历 排序 先使用层序遍历计算出树的每一层的节点值的和&#xff0c;保存在数组 levelSum 中。然后将数组进行排序&#xff0c;返回第 k 大的值。需要考虑数组长度小于 k 的边…