集群部署中解决定时任务重复执行的问题-redis分布式锁应用

背景描述

有小伙伴私信我,关于存在定时任务的项目在集群环境下部署如何解决重复执行的问题,PS:定时任务没有单独拆分。

概述:之前的项目都是单机器部署,所以定时任务不会重复消费,只会执行一次。而在集群环境部署下,比如两台机器部署了当前的项目,如果不做任何处理的话势必会执行两次,通常重复执行会影响现有数据。所以要解决的就是在某个时间点,只能让一个项目执行这个定时任务。

考察知识点:锁。

正文部分

这个问题最简单的操作方式是啥?

答:那就是一个打包带定时任务,一个打包不带定时任务…

咳咳,开个玩笑。显然这样不行啊,要是用这种操作先不说后面升级时每次打两个包多麻烦,单说这种方式就完全失去了集群部署的意义… 存在单点故障。

如果能找到唯一值的话,其实也是一种解决思路,比如可以通过数据库的唯一索引、或者主键索引来实现等。

下文则主要通过找不到唯一值的情况进行分析。

实现思路:数据库行级锁、redis分布式锁。

前面不是写过 Redis 分布式锁的文章吗,这次正好实践一下。

所以这次的技术选型就用 Redis 分布式锁来解决集群模式下定时任务重复执行的问题。

Redis 分布式锁有两种实现方式,一种是 Redisson+RLock,另一种是 SetNX+Lua脚本实现。

如果不了解的可以看一下下面这两篇文章,内含源码,本文皆以该源码操作。

Redis分布式锁—SETNX+Lua脚本实现篇

Redis分布式锁—Redisson+RLock可重入锁实现篇

简单分析:

这两篇 Redis 分布式锁的 demo,主要就是为了解决,在分布式部署中的商品接口避免超卖的情况。简单点说就是,无论用户的下单请求落在哪个服务实例上,首先你要保证顺序性,也就是你不能两个实例的同一方法同时执行业务逻辑,而是同一时间内只能由一个实例完成操作(减库存操作);一个实例完成操作,则另一个才正常往下走。

和定时任务重复执行的问题有点类似了,但是与本文模拟的例子还是有一点点区别的,一个实例执行了定时任务,而另一个实例的定时任务是不能再继续执行业务代码的,因为换做以前可以通过商品的库存来进行判断,然后return掉,但是现在的情况是找不到唯一值,或者说找不到判定的条件,如果直接套上之前的代码,那么是没法阻止另一个实例定时任务执行的。

如下是之前 RLock 示例,用户下单的方法:

这里面有个判断库存的地方,大家可以看一下注释,定时任务遇到的问题。

@Transactional(rollbackFor = Exception.class)
public boolean createOrder(String userId, String productId) {/**  如果不加锁,必然超卖 **/RLock lock = redissonClient.getLock("stock:" + productId);try {/** 这一步相当于锁住,串连 **/lock.lock(10, TimeUnit.SECONDS);/** 第一个实例执行完或者说锁在10秒后释放后,第二个实例永远也会走到下面这一步* 无非就是在之前的例子中可以判断库存的形式进行返回,但是定时任务不行,* 商品可以通过库存来判断,但是定时任务做不到,* 所以加下来就是对当前这段代码进行改造。*/int stock = stockService.get(productId).getStockNum();log.info("剩余库存:{}", stock);if (stock <= 0) {return false;}String orderNo = UUID.randomUUID().toString().replace("-", "").toUpperCase();/** 减库存操作 **/if (stockService.decrease(productId)) {Order order = new Order();order.setUserId(userId);order.setProductId(productId);order.setOrderNo(orderNo);Date now = new Date();order.setCreateTime(now);order.setUpdateTime(now);orderDao.save(order);return true;}} catch (Exception ex) {log.error("下单失败", ex);} finally {lock.unlock();}return false;
}
1、SETNX+Lua脚本实现篇

至于 Lua 脚本怎么写的我就不在这赘述了,大家可以翻看上面的文章链接。

直接从代码下手,没什么变化,方法后面说一下过程。

@Scheduled(cron = "0 47 23 * * ?")
public void  generateData() {/** 定时任务的名称作为key **/String key = "generateData";/** 设置随机key **/String value = UUID.randomUUID().toString().replace("-", "");/** setIfAbsent <=> SET key value [NX] [XX] [EX <seconds>] [PX [millseconds]]* set expire time 20 s*/Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value, 20000, TimeUnit.MILLISECONDS);if (flag != null && flag) {log.info("{} 锁定成功,开始处理业务", key);try {/** 模拟处理业务逻辑,15秒 **/Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}/** 业务逻辑处理完毕,释放锁,正常情况下,由于上边 setIfAbsent 已经设置过期时间了,* 所以在规定时间内,Redis 会自动删除过期的 key,但是这个删除由于不确实是什么删除策略,* 所以最后执行完再删除一遍比较保险。*/String lockValue = (String) redisTemplate.opsForValue().get(key);/** 只有:值未被释放(也就是当前未达到过期时间),且是自己加锁设置的值(不要释放别人的所),这种情况下才会释放锁 **/if (lockValue != null && lockValue.equals(value)) {System.out.println("lockValue========:" + lockValue);List<String> keys = new ArrayList<>();keys.add(key);Long execute = redisTemplate.execute(script, keys, lockValue);System.out.println("execute执行结果,1表示执行del,0表示未执行 ===== " + execute);log.info("{} 解锁成功,结束处理业务", key);}} else {log.info("{} 获取锁失败", key);}
}

首先方法顶部是一个 cron 的表达式,在每天的 23 点 47 分执行。

核心部分仍是 setIfAbsent() 方法,在这设置了一个 20 秒的过期时间,过期时间一到,默认会对 key 进行删除操作。

这个方法是个原子操作,所以两个实例同时执行的话,会产生锁竞争,返回的 Boolean 类型的 flag 即表示加锁状态。

为 true 表示获取锁成功,则另一个实例,或者另外所有的实例都会获取锁失败,即 flag = false 走 else 逻辑。

中间模拟了个 15 秒的业务执行,如果业务逻辑执行时间超过设置的 key 的过期时间,则 redisTemplate.opsForValue().get(key) 拿到的可能为 null 或者不一定为 null,为 null 说明 redis 自动触发了删除操作,不为 null 则虽然 key 值过期了,但是并没有立刻删除。

所以这种情况就需要删除一下。

删除也是一个小的细节,怎么讲?代码删除之前一定要判断是否是当前线程设置的 value,否则会出现释放别的线程锁的情况。

这个地方可能比较绕。

举个例子:比如A、B线程同时进入该方法执行,从 setIfAbsent() 方法加锁,到处理业务业务代码15秒一切都很正常,此过程也只会有一个线程获得锁,另一个线程有 else 操作。但是需要注意的是,你没法保证两个定时任务同时执行,???因为你无法保证两台机器的时间永远一直,也就是会出现误差,这种情况就很恶心了,所以在设置 value 的时候用的是随机参数,这有个好处就是在删除之前先从 redis 再查询一遍,一致就删除释放锁,不一致就不释放。

2、Redisson + RLock

上面的问题代码贴过了,修改后如下:

@Scheduled(cron = "0 21 14 * * ?")
public void test(){RLock lock = redissonClient.getLock("test");/** 加锁状态 **/boolean flag = false;try {flag = lock.tryLock(10,20, TimeUnit.SECONDS);if(flag){log.info("加锁成功,开始执行业务");try {log.info("模拟处理业务逻辑");/** 模拟处理业务逻辑,15秒 **/Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}}else{log.info("加锁失败,没有获取到锁");}} catch (Exception ex) {log.error("下单失败", ex);} finally {if(!flag){return;}lock.unlock();log.info("Redisson分布式锁释放锁");}
}

简单分析一下代码。

核心代码主要是 lock.tryLock(0,20, TimeUnit.SECONDS),tryLock 方法有好几个重载方法,在上篇 [Redisson + RLock] 分布式锁中有写过,而今天我们用的是带三个参数的 tryLock。

/*** 这里比上面多一个参数,多添加一个锁的有效时间** @param waitTime  等待时间* @param leaseTime 锁有效时间* @param unit      时间单位 小时、分、秒、毫秒等*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

方法解释:在尝试获取锁时,如果被其他线程先拿到锁则会进入等待状态,等待 waitTime 时间后,如果还没用机会获取到锁就放弃,返回 false;如果获得了锁,除非是调用 unlock 释放,否则会一直持有锁,一直持有到超过 leaseTime 时间后自动释放锁。

套入解释:线程尝试加锁,但最多等待 10 秒,上锁以后 20 秒后自动释放锁,返回 true 表示加锁成功,返回 false 则表示加锁失败。

细节补充:需要注意的是,在 finally 释放锁的时候,一定要判断当前的线程是否持有锁,只有在持有锁的情况下才能释放锁,否则会造成释放别的线程的锁。

其实这个地方单单靠否持有锁 flag 标志还是会存在问题。

前面也有提到了服务器时间不一致的问题,但是正常情况下,这个误差不会太大,但假如说,如果误差超过业务逻辑执行的时间或者设置的锁有效时间,那么问题就很明显了,第一个实例执行完,无论是自己释放的锁,还是20秒后自动释放的锁,都会出现重复执行的问题。

最后补充

无论是采用 Redisson+RLock 还是 SetNX+Lua,在一定程度上确实可以解决集群部署下,定时任务重复执行的问题。

但是从严谨性来看,并不代表不会出现问题。

1、首先 Redis 分布式锁依赖的是 Redis 集群,如果不是使用 Redis 集群的小伙伴,建议理性选择如上方案,毕竟单机 Redis 挂了,那么定时任务这块的代码基本也就挂了。

2、使用了 Redis 集群还是会存在故障重启带来的锁的安全性问题。

我在之前的文中有提到过,master / slave 主从节点切换导致数据丢失的情况,为了解决这种情况如果加入了持久化操作,任然会存在锁的安全性问题,比如节点重启~

3、上面这1、2项都是说的Redis自身的问题,再就是服务器本身的时间差问题。

如果服务器的时间出现误差的话,那么就需要考虑释放锁的这一步骤了,我们可以尽量的选择使用自动的过期时间,而不是自己通过代码去释放锁,因为不同于别的接口,如果是一个正常的接口的话,你长时间的(过期时间)占着锁不释放,那么肯定是有问题的,相当于这个接口在这段时间内就是挂掉了。但是对于定时任务就不一样了,通常定时任务是每隔多长时间执行一次,或者说一天就执行一次,那么我们就可以考虑在过期时间或者等待时间上做功夫了。

比如定时任务每天就执行一次,但是又怕服务器存在时间差,那么就可以选择一个2小时的过期时间,总不能误差超过2小时吧?

再就是并不是不能保证服务器时间存在误差的问题。

PS:既然有问题,那么 Redis 分布式还可选吗?

可选,其实关于Redis分布式锁,在很多商城项目中也有应用,考虑好误删、原子性、超时等待等情况是没什么问题的。

如果对数据要求比较高则可以考虑 Zookeeper 分布式锁。后面会准备码一下 Zookeeper 锁相关的 demo。

博客地址:https://niceyoo.cnblogs.com

更多原创内容可以移步我的公众号,回复「面试」获取我整理的2020面经。

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

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

相关文章

[js] 在设置keyup监听事件后按F5刷新和按浏览器中刷新键刷新有什么区别?

[js] 在设置keyup监听事件后按F5刷新和按浏览器中刷新键刷新有什么区别&#xff1f; 按浏览器中刷新键刷新不会触发keyup事件个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关…

洛达检测软件AB1562UT_1.4.4新版本下载,适用洛达全系列

络达芯片检测调试工具&#xff0c;2020年12月发布的版本&#xff0c;仅支持Android系统手机&#xff1b; 适用于AB1536U,1562A,1562M,1562F。不是络达芯片的不能用&#xff01; 真洛达推荐链接&#xff1a;https://item.taobao.com/item.htm?ftt&id641479139773 一、软件…

[js] 用js写一个方法检测浏览器是否支持css3的属性

[js] 用js写一个方法检测浏览器是否支持css3的属性 var div document.createElement(div); console.log(div.style.transition); //如果支持的话, 会输出 "" //如果不支持的话, 会输出 undefined.个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃…

支付宝新版SDK-PC扫码支付-手机浏览器H5支付

一、前言 支付宝支付—沙箱环境使用 支付宝新版SDK-PC端扫码支付 手机浏览器H5支付「本文」 PC端扫码支付&#xff0c;其实就是就是 电脑网站支付&#xff0c;本文基于支付宝沙箱环境&#xff0c;不了解的可以看一下上边的链接。 PS&#xff1a;本文是基于支付宝新版 SDK …

华强北耳机修改序列号|支持中英文|自定义修改|傻瓜式一键修改序列号~

软件下载链接>>>&#xff1a;https://t.1yb.co/kQIu 你还在纠结买到的华强北耳机序列号查询不到吗&#xff1f; 你还在纠结因为刷机导致的序列号与充电仓序列号不一致吗&#xff1f; 你还在纠结自己的序列号不够有面吗… 先看一张效果图&#xff1a; 今天分享一下…

[js] 对`a == (‘1‘||‘2‘||‘3‘) ? false : true`写法进行改进,写出你优化后的方法

[js] 对a (1||2||3) ? false : true写法进行改进&#xff0c;写出你优化后的方法 ![1,2,3].includes(a) or ![1, 2, 3].includes(a ) or !{1: true, 2: true, 3: true}[a]个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很…

[BZOJ 1452] Count

Link: BZOJ 1452 传送门 Solution: 二维树状数组模板题 发现颜色数很少$c<100$&#xff0c;因此对于每个颜色都建一棵二维线段树即可 &#xff08;第一次写二维数据结构&#xff0c;发现套个循环就行了&#xff1f;&#xff09; Code: #include <bits/stdc.h>using na…

CentOS离线安装gcc环境(附安装包+图文并茂)

1、关于gcc linux内核本身不依赖gcc&#xff0c;gcc只是一个编译软件&#xff0c;是在kernel的源码变成可执行文件的时候起作用&#xff0c;真正使用起来就没有什么关系。 查看 gcc 版本 gcc -v 如果没有则显示&#xff1a; 2、安装步骤 2.1、下载gcc安装包 gcc下载地址&…

[js] Geolocation.getCurrentPosition()用来做什么的?在什么浏览器不受兼容?

[js] Geolocation.getCurrentPosition()用来做什么的&#xff1f;在什么浏览器不受兼容&#xff1f; 获取当前设备的位置 Android不支持个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌…

CentOS7安装redis并配置外网可访问(局域网可参考)

1、安装gcc编辑器 安装redis需要依赖gcc环境&#xff0c;执行如下命令安装&#xff1a; yum install -y gcc如果机器没有网络的话&#xff0c;可以参考这篇文章&#xff1a; CentOS离线安装gcc环境(附安装包图文并茂) 2、下载redis安装包 redis官网&#xff1a;https://re…

OSPF两种组播地址的区别和联系

1.点到点网络: 是连接单独的一对路由器的网络,点到点网络上的有效邻居总是可以形成邻接关系的,在这种网络上,OSPF包的目标地址使用的是224.0.0.52.广播型网络, 比如以太网,Token Ring和FDDI,这样的网络上会选举一个DR和BDR,DR/BDR的发送的OSPF包的目标地址为224.0.0.5;而除了DR…

[js] setTimeout的第三个参数有什么用?

[js] setTimeout的第三个参数有什么用&#xff1f; 第三个参数将作为定时器到期触发的函数的参数个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

CodeForces 931C Laboratory Work 水题,构造

*这种题好像不用写题解... 题意: 一个人要改动别人的实验记录,实验记录记录是一个集合 实验记录本身满足:$max(X)-min(X)<2$ 改动结果要求: 1.新的集合平均值和之前的一样 2.新的集合,$max(Y)<max(X),min(Y)>min(X)$ 求新一个和之前相同数值最少的新记录 题解: 首先考…

悦虎144固件,华强北二代悦虎144固件,1562M芯片144固件

下载地址 https://t.1yb.co/k49w 关于固件 最新的悦虎1562M 144固件&#xff0c;适用于华强北二代悦虎主板&#xff0c;1562M洛达芯片。 关于本次升级&#xff1a;优化功耗&#xff0c;提升续航。

[js] 解释下深度优先遍历和广度优先遍历的区别及如何实现

[js] 解释下深度优先遍历和广度优先遍历的区别及如何实现 1、深度优先采用堆栈结构&#xff0c;先进后出&#xff0c;所占的空间较小&#xff0c;执行时间较长&#xff1b; 2、广度优先采用队列结构先进先出&#xff0c;所占空间较大&#xff0c;执行时间短&#xff0c;空间换…

RDD特性

转载于:https://www.cnblogs.com/ggzhangxiaochao/p/9237510.html

MMI_UT洛达检测软件使用,检测1562A

MMI_UT软件下载链接 > https://t.1yb.co/nLFU 如下为使用教程 关于华强北耳机如何检测已经写过几篇文章了&#xff0c;从后台看到大家给到我的私信&#xff0c;有时候真的挺多感慨的&#xff0c;买个jia耳机确实不容易啊~ 突然发现比买个正品还难&#xff0c;正品就算别的…

[js] 你认为es5的设计缺陷有哪些?

[js] 你认为es5的设计缺陷有哪些&#xff1f; 可以反过来想&#xff0c;es6新增的特性就是为了解决es5的设计缺陷个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

安卓如何修改华强北二代耳机敲击指令?

经常有小伙伴在公中号后台私信我&#xff0c;小源&#xff01;为啥我的二代耳机敲击不管用啊~ 害&#xff0c;先检查一下是不是敲击的这个部位~ 如果敲击部位确认没问题的话&#xff0c;就要看一下是不是敲击设置的问题&#xff0c;默认左耳双击切换下一曲&#xff0c;默认右耳…

[js] 使用ajax轮询接口有什么优缺点?

[js] 使用ajax轮询接口有什么优缺点&#xff1f; 首先&#xff0c;所谓轮询接口的原理是 利用 setTimeout 定时请求API接口优点&#xff1a; 1&#xff0c;可以简单不用二次 开发websocket 实现所需功能 几乎没有学习成本 2&#xff0c;使用简单 缺点&#xff1a; 1&#xff0…