集群部署中解决定时任务重复执行的问题-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,一经查实,立即删除!

相关文章

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

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

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

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

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

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

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

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

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

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

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

下载地址 https://t.1yb.co/k49w 关于固件 最新的悦虎1562M 144固件&#xff0c;适用于华强北二代悦虎主板&#xff0c;1562M洛达芯片。 关于本次升级&#xff1a;优化功耗&#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;正品就算别的…

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

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

Django 2.0 学习(20):Django 中间件详解

Django 中间件详解 Django中间件 在Django中&#xff0c;中间件(middleware)其实就是一个类&#xff0c;在请求到来和结束后&#xff0c;Django会根据自己的规则在合适的时机执行中间件中相应的方法。 1.执行完所有的request方法到达执行流程&#xff1b;2.执行中间件的其他方法…

华强北二代悦虎1562M升级固件图文教程(详细多图文)

本文演示的固件在下方链接获取。 悦虎固件链接&#xff1a;https://t.1yb.co/kQH4 如下是刷固件(151版本)的图文步骤&#xff0c;注意&#xff0c;必须是悦虎1562M芯片。 PS&#xff1a;注意&#xff0c;此时所有的操作都是建立在耳机放在充电仓外的情况下执行。请结合图片文…

华强北三代悦虎1562A怎么样?

最近华强北三代耳机的更新频率有点高啊… 我这更文都快跟不上了。。 比如最近新出的慧联A6pro、悦虎三代1562A、大公牛等等&#xff0c;相信手快的小伙伴&#xff0c;估计这会都已经用上了吧&#xff1f; 刚好手里有一个悦虎&#xff0c;结合最近的使用情况&#xff0c;这一…

Java多线程(五) —— 线程并发库之锁机制

参考文献&#xff1a; http://www.blogjava.net/xylz/archive/2010/07/08/325587.html 一、Lock与ReentrantLock 前面的章节主要谈谈原子操作&#xff0c;至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来整体说明。从这一章开始花少量的篇幅谈谈锁机制。 上一个章…

改丝印的假华强北三代1562A,用芯良苦!

今天带大家了解的主要内容&#xff0c;含j商在线battle&#xff1a; 不要再使用老版本检测软件检测了&#xff01;其他芯片改丝印洛达1562A&#xff0c;用芯良苦&#xff01;辨别真假洛达1562A的几种方式上当的一些小伙伴的亲身案例(在线battle) 1、不要再使用老版本检测软件…

记一次Linux磁盘满盘/dev/vda1目录清理记录

记一次 Linux 磁盘满盘记录&#xff0c;最近发现自己的个人站非常卡顿&#xff0c;因为前段时间才加的带宽&#xff0c;所以肯定不是网速问题&#xff0c;查了下磁盘。。差点满了。。 1、磁盘占用情况命令&#xff1a;df -h df -h2、找出占比比较高的目录 #查看各目录空间占…

安装 Power BI 报表服务器

开始之前 建议在安装 Power BI 报表服务器之前先查看安装 Power BI 报表服务器所要满足的硬件和软件要求。 Power BI 报表服务器产品密钥 Power BI Premium 如果已购买 Power BI Premium&#xff0c;则可以在 Power BI 管理门户的“Premium 设置”选项卡中访问 Power BI 报表服…

DT100pro上手体验

好久没写过手表类的了&#xff0c;年初的时候持"巨资"拿了块HQB手表(u98plus)&#xff0c;然后写了下面这篇文章(没看过的感兴趣的可以看看)&#xff0c;从手表不菲的售价到实际到手后的体验&#xff0c;个人感觉一般&#xff0c;尤其是u98p匹配的APP做的真的很一般&…

SpringBoot集成thymeleaf增删改查示例

有小伙伴找我要个 thymeleaf 的 demo&#xff0c;说自己集成的总是报错&#xff0c;所以就有了这篇… 关于什么是 thymeleaf 我就不赘述了&#xff0c;相信搜到这篇的大部分是奔着如何集成来的。 本文源码先附上&#xff1a;https://gitee.com/niceyoo/springboot-thymeleaf-…

转:基于TLS1.3的微信安全通信协议mmtls介绍

转自&#xff1a; https://mp.weixin.qq.com/s?__bizMzAwNDY1ODY2OQ&mid2649286266&idx1&snf5d049033e251cccc22e163532355ddf&scene0&keyb28b03434249256b2a5d4fdf323a185a798eaf972317ca3a47ef060d35c5cd8a4ae35715466d5bb5a558e424d20bef6c&ascene…

慧联A8最新检测使用教程V2.0.3

有小伙伴反馈旧版本 TWS106、TWSVerification 软件无法检测慧联A8&#xff0c;如下方截图所示&#xff1a; 由于之前版本确实太低 1.0.5 或者 2.0.2 都无法检测慧联A8&#xff0c;需要安装 2.0.3 版本APP才可以检测&#xff0c;如下为慧联A8检测截图 2.0.3 版本APP&#xff1a;…