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

上一篇文章提到使用针对不同的业务场景如何合理使用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);
            }
        }

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024年【起重机司机(限桥式起重机)】找解析及起重机司机(限桥式起重机)考试总结

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【起重机司机(限桥式起重机)】找解析及起重机司机(限桥式起重机)考试总结&#xff0c;包含起重机司机(限桥式起重机)找解析答案和解析及起重机司机(限桥式起重机)考试总结练习。安全生产模拟考试一点通结合国家…

游戏平台如何定制开发?

随着科技的飞速发展和互联网的普及&#xff0c;游戏平台已成为人们休闲娱乐的重要选择。为了满足用户多样化的需求&#xff0c;游戏平台的定制开发显得尤为重要。本文将探讨游戏平台定制开发的过程、关键要素以及注意事项&#xff0c;为有志于涉足此领域的开发者提供参考。 一、…

springboot自定义starter,实现自动装配

1、SpringBoot Starter介绍 随着Spring的日渐臃肿&#xff0c;为了简化配置、开箱即用、快速集成&#xff0c;Spring Boot 横空出世。 目前已经成为 Java 目前最火热的框架了。平常我们用Spring Boot开发web应用。Spring mvc 默认使用tomcat servlet容器&#xff0c; 因为Sprin…

ChatGPT 4 教你完成学生表,教师表,课程表,选课表之间的SQL学习

数据源准备&#xff1a; # 学生表 create table student( sno varchar(10) primary key, #学号sname varchar(20), #姓名sage int(2), #年龄ssex varchar(5) #性别 ); #教师表 create table teacher( tno varchar(10) primary …

git切换仓库地址

已有git仓库&#xff0c;要切换提交的仓库地址&#xff0c;用以下命令 git remote set-url origin 自己的仓库地址 用以下命令&#xff0c;查看当前仓库地址&#xff1a; git remote show origin 切换仓库后&#xff0c;用以下命令初始化提交仓库&#xff1a; git push -u o…

ES6 | (一)ES6 新特性(上) | 尚硅谷Web前端ES6教程

文章目录 &#x1f4da;ES6新特性&#x1f4da;let关键字&#x1f4da;const关键字&#x1f4da;变量的解构赋值&#x1f4da;模板字符串&#x1f4da;简化对象写法&#x1f4da;箭头函数&#x1f4da;函数参数默认值设定&#x1f4da;rest参数&#x1f4da;spread扩展运算符&a…

Java-Arrays工具类的常见方法总结

在Java中&#xff0c;提供了一个专门用于操作数组的工具类&#xff0c;即Arrays类&#xff0c;该类提供了一些方法对数组进行排序&#xff0c;打印&#xff0c;复制等操作。下面是一些该工具类方法总结。 1.Arrays.aslist() 作用&#xff1a;当我们想直接将数组中的全部内容(…

【计算机网络】数据链路层--以太网/MTU/ARP/RARP协议

文章目录 一、以太网1.以太网帧格式2.MAC地址3.局域网的转发原理 二、MTU1.什么是MTU2.MTU对IP协议的影响3.MTU对UDP影响4.MTU对于TCP协议的影响 三、ARP协议1.ARP协议的作用2.ARP数据报的格式3.ARP协议的工作流程 一、以太网 “以太网” 不是一种具体的网络, 而是一种技术标准…

命令执行 [WUSTCTF2020]朴实无华1

做题&#xff1a; 打开题目 我们用dirsearch扫描一下看看 扫描到有robots.txt&#xff0c;访问一下看看 提示我们 /fAke_f1agggg.php 那就访问一下&#xff0c;不是真的flag bp抓包一下 得到提示&#xff0c; /fl4g.php&#xff0c;访问一下看看 按alt&#xff0c;点击修复文…

使用 C++23 协程实现第一个 co_await 同步风格调用接口--Qt计算文件哈希值

C加入了协程 coroutine的特性&#xff0c;一直没有动手实现过。看了网上很多文章&#xff0c;已经了解了协程作为“可被中断和恢复的函数”的一系列特点。在学习过程中&#xff0c;我发现大多数网上的例子&#xff0c;要不就是在main()函数的控制台程序里演示yeild,await, resu…

Nginx网络服务二-----(虚拟机和location)

一、HTTP设置 1.设置虚拟主机 1.1Nginx 基于域名---虚拟主机 include /apps/nginx/conf.d/*.conf; 1.2Nginx 基于端口---虚拟主机 在做了域名的基础上&#xff0c;按照以下步骤继续 1.3Nginx 基于IP---虚拟主机 2.server下的root root路径格式 指定文件的路径 url …

1.1_1 计算机网络的概念、功能、组成和分类

文章目录 1.1_1 计算机网络的概念、功能、组成和分类&#xff08;一&#xff09;计算机网络的概念&#xff08;二&#xff09;计算机网络的功能&#xff08;三&#xff09;计算机网络的组成1.组成部分2.工作方式3.功能组成 &#xff08;四&#xff09;计算机网络的分类 总结 1.…

线性代数:向量空间

目录 向量空间 Ax 0 的解空间S Ax b 的全体解向量所构成集合不是向量空间 基、维数、子空间 自然基与坐标 例1 例2 向量空间 Ax 0 的解空间S Ax b 的全体解向量所构成集合不是向量空间 基、维数、子空间 自然基与坐标 例1 例2

使用redisMQ-spring-boot-starter实现消息队列和延时队列

简介 redisMQ-spring-boot-starter是一个轻量级的、基于Redis实现的消息队列中间件&#xff0c;它有如下优点&#xff1a; 开箱即用&#xff0c;你几乎不用添加额外的配置支持消息队列、延时队列&#xff0c;并提供精细化配置参数提供消息确认机制支持虚拟空间&#xff0c;不…

非洲数字经济持续崛起 本地化策略让传音提前入局

非洲市场&#xff0c;被誉为全球最后的“边疆级”市场&#xff0c;吸引着全球目光。近日&#xff0c;非洲开发银行最新报告指出&#xff0c;未来两年非洲的经济增长将优于世界其他地区&#xff0c;2023 年和 2024 年实际国内生产总值 (GDP) 平均约为 4%。广阔的非洲大陆焕发着勃…

PLC设置网口通讯的原因

PLC设置网口通讯功能&#xff0c;是现场总线向工业以太网的迈进&#xff0c;更是为了在网口之上构建更为庞大、开放的大一统的生态系统。 有了以太网&#xff0c;特别是TCP/IP协议后&#xff0c;全员工控人的日常工作更为便利了。 主要体现在以下4点&#xff1a; 1.再也不需要…

TiDB 社区智慧合集丨TiDB 相关 SQL 脚本大全

非常感谢各位 TiDBer 在之前 【TiDBer 唠嗑茶话会 48】非正式 TiDB 相关 SQL 脚本征集大赛&#xff01;( https://asktug.com/t/topic/996635 )里提供的各种常用脚本。 在这篇文章中&#xff0c;我们整理了社区同学提供的一系列 TiDB 相关 SQL 脚本&#xff0c;希望能为大家在…

基于springboot+vue的车辆管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

【云动世纪:Apache Doris 技术之光】

本文节选自《基础软件之路&#xff1a;企业级实践及开源之路》一书&#xff0c;该书集结了中国几乎所有主流基础软件企业的实践案例&#xff0c;由 28 位知名专家共同编写&#xff0c;系统剖析了基础软件发展趋势、四大基础软件&#xff08;数据库、操作系统、编程语言与中间件…