百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Redis锁过期,任务没执行完,怎么处理?自己动手实现加解锁逻辑

wptr33 2024-12-17 16:46 41 浏览

相信在日常开发中,基于 Redis 天然支持分布式锁,大家在线上分布式项目中都使用过 Redis 锁。本文主要针对某些异常场景下,加锁代码执行时间超过了加锁时间,导致任务还没执行完,但是锁已经释放的问题进行讲解并给出实践代码。本文版本说明如下:

  • Spring Boot 版本 3.0.2
  • 演示项目地址:https://github.com/wayn111/newbee-mall-pro
  • github地址:http://github.com/wayn111 欢迎大家关注,点个star

一、过期时间

一般情况下我们加锁时,都会指定过期时间参数,当任务执行时间超过了锁过期时间,下一个任务进来时就会获取到锁,造成异常。

针对过期时间常见有两种处理方法:

  • 自动续期:锁快到期时,通过定时任务自动续期
  • 加锁不设置过期时间:任务不执行完,锁就不会过期

这里博主给出自己的分析:

第一种方案:当设置了过期时间后,如果还执行自动续期操作,那么这个锁的实际过期时间就与我们在加锁时设置的过期时间不符合,产生了逻辑上的冲突!所以博主认为自动续期操作对已经设置了过期时间的锁不适用。

第二种方案:加锁不设置过期时间的话,理论上好像是可以解决这个问题,任务不执行完,锁就不会释放。但是实际针对一些极端异常场景下,如果任务执行过程中,服务器宕机、网络断连等都可能造成锁释放不了,比如加锁成功了,执行中发生了宕机,程序直接没了,但是锁还在,另一个任务就一直获取不到锁。

综合来看:博主认为如果加锁代码需要添加过期时间,其实不需要进行自动续期操作。当我们需要确保当前任务没执行完,下一个任务一定不能获取到锁时,可以不设置过期时间。

那怎么避免第二种方案中,异常场景下,锁一直未释放的问题嘞?

答案是在加锁成功时,如果没有指定过期时间,则给一个默认过期时间比如三十秒,通过定时任务给我们的锁进行自动续期,这样就既可以解决锁一直未释放的问题,又能保证下一任务获取不到当前任务的锁。

二、 代码实践

2.1 加锁

首先看加锁操作,如果不指定过期时间,则会指定默认过期时间,通过 lua 脚本加锁

private String buildLuaLockScript() {
    return """
            local key = KEYS[1]
            local value = ARGV[1]
            local time_out = ARGV[2]
            local result = redis.call('setnx', key, value)
            if tonumber(result) == 1 then
                redis.call('expire', key, time_out)
                return 1;
            else
                return 0;
            end
            """;
}

成功后,启动一个定时任务每隔 默认过期时间 / 3 秒后执行一次续期操作

@Autowired
public RedisTemplate redisTemplate;
public static final Integer DEFAULT_TIME_OUT = 30;
private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
private ThreadLocal<ExecutorService> executorServiceThreadLocal = new ThreadLocal<>();

/**
 * 加锁,不指定过期时间
 *
 * @param key key名称
 * @return boolean
 */
public boolean lock(String key) {
    return lock(key, null);
}

/**
 * 加锁
 *
 * @param key     key名称
 * @param timeout 过期时间
 * @return boolean
 */
public boolean lock(String key, Integer timeout) {
    Integer timeoutTmp = timeout;
    if (timeout == null) {
        timeoutTmp = DEFAULT_TIME_OUT;
    }
    String nanoId = IdUtil.nanoId();
    stringThreadLocal.set(nanoId);
    RedisScript<Long> redisScript = new DefaultRedisScript<>(buildLuaLockScript(), Long.class);
    Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), nanoId, timeoutTmp);
    boolean flag = execute != null && execute == 1;
    if (flag && timeout <= 0) {
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        executorServiceThreadLocal.set(scheduledExecutorService);
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            RedisScript<Long> renewRedisScript = new DefaultRedisScript<>(buildLuaRenewScript(), Long.class);
            Long result = (Long) redisTemplate.execute(renewRedisScript, Collections.singletonList(key), nanoId, DEFAULT_TIME_OUT);
            if (result != null && result == 2) {
                ThreadUtil.shutdownAndAwaitTermination(scheduledExecutorService);
            }
        }, 0, 10, TimeUnit.SECONDS);
    }
    return flag;
}

lua 续期脚本如下:

private String buildLuaRenewScript() {
    return """
            local key = KEYS[1]
            local value = ARGV[1]
            local timeout = ARGV[2]
            local result = redis.call('get', key)
            if result ~= value then
                return 2;
            end
            local ttl = redis.call('ttl', key)
            if tonumber(ttl) < tonumber(timeout) / 2 then
                redis.call('expire', key, timeout)
                return 1;
            else
                return 0;
            end
            """;
}

当发现锁过期剩余时间小于默认超时时间时,重新赋值过期时间。

2.1 释放锁:

public boolean unLock(final String key) {
    String nanoId = stringThreadLocal.get();
    RedisScript<Long> redisScript = new DefaultRedisScript<>(buildLuaUnLockScript(), Long.class);
    Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), nanoId);
    boolean flag = execute != null && execute == 1;
    if (flag) {
        if (executorServiceThreadLocal.get() != null) {
            ThreadUtil.shutdownAndAwaitTermination(executorServiceThreadLocal.get());
        }
    }
    return flag;
}
private String buildLuaUnLockScript() {
    return """
            local key = KEYS[1]
            local value = ARGV[1]
            local result = redis.call('get', key)
            if result ~= value then
                return 0;
            else
                redis.call('del', key)
            end
            return 1;
            """;
}

到这里我们就全部完成了不设置超时时间的自动续期以及锁释放操作。

三、总结

简而言之,博主认为对于主动设置了过期时间的锁不应该再进行续期操作,我们通过加锁时不设置过期时间(指定默认超时时间),添加自动续期逻辑,可以比较完美的解决锁过期但是任务没执行完的问题。

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...