背景描述

有小伙伴私信我,关于存在定时任务的项目在集群环境下部署如何解决重复执行的问题,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面经。

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

  1. k8s集群部署中etcd启动报错request sent was ignored (cluster ID mismatch: peer[c39bdec535db1fd5]=cdf818194e3a8c

    k8s集群部署中etcd启动报错处理 报错信息如下,主要报错信息有两条 原因是/var/lib/etcd/目录下缓存导致 解决方法 问题2: Error starting daemon: SELinu ...

  2. 使用Tomcat-redis-session-manager来实现Tomcat集群部署中的Session共享

    2019独角兽企业重金招聘Python工程师标准>>> 一.工作中因为要使用到Tomcat集群部署,此时就涉及到了Session共享问题,主要有三种解决方案: 1.使用数据库来存储S ...

  3. Elasticsearch在Linux中的单节点部署和集群部署

    目录 一.Elasticsearch简介 二.Linux单节点部署 1.软件下载解压 2.创建用户 3.修改配置文件 4.切换到刚刚创建的用户启动软件 5.测试 三.Linux集群配置 1.拷贝文件 ...

  4. 永洪Bi架构部署与集群部署

    永洪Bi架构部署与集群部署 ​ 永洪Bi是一款先进的数据技术与数据可视化的一站式大数据分析平台.他的优势在于:1.可靠的多数据源对接能力:2.丰富精致的数据图表样式:3.灵活高效的可视化探索式分析:4 ...

  5. Greenplum集群部署和架构优化,我总结了5000字的心得

    这是学习笔记的第 2361篇文章 最近对离线数仓体系进行了扩容和架构改造,也算是一波三折,出了很多小插曲,有一些改进点对我们来说也是真空地带,通过对比和模拟压测总算是得到了预期的结果,这方面尤其值得一 ...

  6. KingbaseES集群部署工具安装

    目录 1 安装前准备 1.1 软件环境要求 1.2 安装包准备 1.3 注意事项 1.3.1 安装用户 1.3.2 安装目录 2 执行定制安装 3 测试集群部署工具 3.1 执行集群部署工具的可执行文 ...

  7. 使用redis分布式锁+lua脚本实现分布式定时任务控制demo

    2019独角兽企业重金招聘Python工程师标准>>> 分布式系统经常要遇到定时任务执行的问题,不能重复执行,但很多时候又不能统一到一个微服务里面,因为这样就失去了微服务的意义.由于 ...

  8. 集群服务器定时任务重复执行的解决方案

    集群服务器定时任务重复执行的解决方案 服务器采用了负载均衡,有两台服务器,部署的代码一样,所以里面的定时任务在某一时间会被同时执行,这就导致了很多其他意外的发生,想要解决的问题基本就三个:单点执行,故 ...

  9. JAVA_OA(十四)番外:JAVAWEB防止表单重复提交的方法整合(包括集群部署)

    因为自己要用,所以查找了网络上javaweb项目防止表单重复提交的方法,有些部分不太好找,所以整合后贴出来,首先是孤傲苍狼的一部分博客文章,集群部署的解决方案在后面(注意大红字) 原文出处:孤傲苍狼的 ...

最新文章

  1. 你必须了解的微服务架构设计的10个要点!
  2. python+soket实现UDP协议的局域网广播程序
  3. 数据结构实验之栈与队列四:括号匹配
  4. 【视频课】模型部署课程更新!ncnn框架快速实践!
  5. python在人工智能应用锁_解读! Python在人工智能中的作用
  6. Xposed源码剖析——Xposed初始化
  7. iOS之深入解析Xcode编译运行的原理与应用
  8. Spring Boot注解
  9. Linux下双线双ip访问内网服务器之另类解决办法
  10. java取两个10_30的随机整数_产生10个[30,90]区间上的随机整数,然后对其用选择法...
  11. 光照强度曲线图android,有关光合作用的曲线图的分析(一) - xyz的日志 - 网易博客...
  12. 第一、二章 引论、算法分析
  13. 计算机专业科研经费排名2015,2017中国大学科研经费排名
  14. Shopee平台如何实现多店铺管理?虾扑erp实现智能管理!
  15. 今日头条推荐算法相关博客集合
  16. 安卓超级压缩管理器(ZArchiver) 汉化版 v0.5
  17. OneZero第一次站立会议Sprint Planning Meeting(2016.3.21)
  18. Lake Counting S
  19. 性能监控与调优篇之【3. JVM 监控及诊断工具-GUI 篇】
  20. Cesium基础知识-加载json数据

热门文章

  1. [vue] 说说你对vue组件的设计原则的理解
  2. 前端学习(2704):重读vue电商网站25之保存token
  3. 工作217:重置逻辑
  4. 前端学习(2425):复习
  5. 前端学习(2378):使用vue-cli初始目录说明
  6. 前端学习(1854)vue之电商管理系统电商系统之git push后出现错误 ![rejected] master -> master(non-fast-forward) error:failed t
  7. 前端学习(660):比较运算符
  8. 前端学习(100):float注意点整理1
  9. mybatis学习(43):一级缓存被刷新情况
  10. html:(27):类和ID选择器的区别和子选择器