美文网首页技术分享JAVA
定时任务的防重设计

定时任务的防重设计

作者: fantuanjiaozi | 来源:发表于2019-11-19 19:53 被阅读0次

一、起因

金融、支付类公司,易产生资损的业务当属代发、转账、卡券权益兑换类等出金交易。每一位致力于此的架构师、开发工程师最担心重复代发、重复兑换的问题,尤其对于批量的出金类业务,由于设计不当导致的大量的资金、资产损失后果惨重。因此批处理任务的防重设计极为重要。

二、定时任务演进

古代

以每5分钟执行一次批量代发交易为例,早期大部分系统都是单体应用,通常采用Spring+Cron表达式来实现定时任务:

spring-quartz.xml:
<bean id="issue" ...>
    <property name="cronExpression" value="* 0/5 * * * ?"></property>
</bean>

为了防止定时任务重复启动,开发工程师们需要注意两点。

  • 1.确保spring-quartz.xml只能被加载一次,如被多次加载,易造成定时任务重复执行。
  • 2.等上一个任务执行完后再开启新的任务:
<!--concurrent : false表示等上一个任务执行完后再开启新的任务-->
<property name="concurrent" value="false"></property>

近代

随着集群部署的广泛使用,单体应用逐渐被替代。为了防止集群内多个实例的定时任务同时启动:

  • 开发工程师通常会在工程内保留两份不同的spring-quartz.xml配置文件:一份是有该定时任务的,另一份则没有。
  • 运维工程师根据不同配置文件产出的程序包,部署到不同的生产服务器上。
集群部署.png

显而易见,由于加入了人为操作,较容易出现打包、部署、核验等操作失误,导致定时任务被重复启动。
为了解决一个项目两份程序包的问题,可采用配置“白名单服务器”来实现:项目中保留该定时任务的spring-quartz.xml配置文件,所有的实例都启动该定时任务,在代发逻辑真正执行前,判断本机的IP地址是否是白名单地址,一致则执行后面的代发逻辑,不一致则终止。

现代

通过docker容器化部署时,应用实例的IP地址不固定,会散落部署在云平台内,采用配置“白名单服务器”的方式来防重已不可行。通过集中式的调度中心来发起调度任务是解决问题的一种思路。可使用elastic-job,xxl-job等开源框架,也可自研。
基本思路是:

  • 各个业务系统只实现业务逻辑,并暴露标准化接口。
  • 调度中心系统是所有任务的发起点,通过抢占锁资源等方式,确保每个任务只能发起一次,最终调用业务系统提供的接口。

潜在风险点

开发工程师在规避定时任务重复执行的同时,往往忽视了dubbo等组件,以及nginx、HAproxy等中间件带来的自动重试问题。想从根本上解决问题,开发工程师还需从系统设计与实现入手:即使定时任务重复启动也能确保交易不重复

三、定时任务实现

原始需求

产品经理:我要在代发工资系统内,实现每5分钟给200名员工发放工资,要求本系统不能重复发放工资。

需求分析

关键诉求:不能重复

数据结构

id name amount status batch_id
1 zhang 6110 0
2 zhou 1280 0
3 liu 16280 0
4 li 8021 0
5 wang 900 0
6 chen 3280 0
.. ... ... ... ...

  • 注:以下所有的实现,都默认有重复执行定时任务的可能。

思路1:一查二发

实现过程大致是一查二发的步骤,伪代码:

//1.查出最多200条
select * from t_order where status=‘0’ and rownum<=200;
//2.循环单笔发,并逐笔更新订单状态
foreach i++{
    Send(i);
    update t_order set status=#{status} where id=#{id} and status='0'
}

不难发现,当两个定时任务同时执行,步骤1的sql可能抓取到重复的数据,继而在步骤2造成重复代发工资。那么在执行send()前先校验下订单状态?不可行,可能会存在脏读。

思路2:幂等表

实现步骤:代发前,插入幂等表,如主键冲突记录异常,并终止本笔代发。

//1.查出最多200条
select * from t_order where status=‘0’ and rownum<=200;
//2.循环单笔发
foreach i++{
    insert into mideng(…) values(…);
    if 主键冲突  then 记录异常 return;
    Send(i);
    update t_order set status=#{status} where id=#{id} and status='0'
}

通过幂等表可以做到防重,但有洁癖的开发工程师会觉得:

  • 1.每次交易会增加数据库交互的开销。
  • 2.长时间后,幂等表的记录数会越来越多,不利于维护。

是否可以从根本上杜绝多个定时任务取同样的数据?

思路3:一锁二发

实现步骤:在每个定时任务的线程内生成唯一标识UUID,先把UUID更新至数据库中,再根据UUID做为查询条件取数进行后续的代发。

//1.锁定数据
String uuid=createUUID();//a73266fc0aa411eaae330221860e9b7e
upate t_order set batch_id=#{uuid} where status=‘0’ and batch_id is null and rownum<=200;
//2.取出本线程锁定的数据
select * from t_order where status=‘0’ and batch_id=#{uuid};
foreach i++{
    Send(i);
    update t_order set status=#{status} where id=#{id} and status='0' and batch_id=#{uuid}
}
数据结构
id name amount status batch_id
1 zhang 6110 0 a73266fc0aa411eaae330221860e9b7e
2 zhou 1280 0 a73266fc0aa411eaae330221860e9b7e
3 liu 16280 0 a73266fc0aa411eaae330221860e9b7e
4 li 8021 0 a73266fc0aa411eaae330221860e9b7e
5 wang 900 0 a73266fc0aa411eaae330221860e9b7e
6 chen 3280 0 a73266fc0aa411eaae330221860e9b7e
.. ... ... ... ...

通过一锁二发的步骤可以保证每个定时任务只执行当前线程锁定的数据。开发工程师也可以根据实际的业务需求,同时使用一锁二发+幂等表。

相关文章

  • 定时任务的防重设计

    一、起因 金融、支付类公司,易产生资损的业务当属代发、转账、卡券权益兑换类等出金交易。每一位致力于此的架构师、开发...

  • 防骚扰处理逻辑(一)

    标签:redis elasticsearch nlu 防骚扰定时任务 线程 CheckAnnoyResultThr...

  • 分布式定时器的设计

    1、背景 如何设计一个分布式定时器服务 2、设计 2.1、版本v1 设计:所有服务都轮训所有的任务,但是为了避免重...

  • 定时器的设计与实现

    参照Ceph的定时器的设计。设计原理: 定时器线程,处理定时任务 利用context_map = std::mul...

  • 分布式定时任务(一)

    1,什么是分布式定时任务;2,为什么要采用分布式定时任务;3,怎么样设计实现一个分布式定时任务;4,当前比较流行的...

  • 防抖和节流

    概念 防抖: 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。 节流:指定时间间隔内只会...

  • 9012年了,不做不懂函数防抖和函数节流的前端

    概述 函数防抖: 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。 函数节流: 指定时间...

  • 2019-07-31定时任务

    定时任务 定时任务实现方法 系统默认定时任务 用户自定义设置定时任务 定时任务配置文件 定时任务启动 定时任务样例...

  • 在定时任务系统基础上支持Crontab Job

    在定时任务系统基础上支持Crontab Job 前言 上一篇我们提到如何设计一个简单高效的定时任务系统[https...

  • Ruby&Rails---Ubuntu使用crontab定时任务

    使用crontab做定时任务,例子是重启rails项目服务器 先介绍常用的命令 定制任务 列出任务 开启 关闭 重...

网友评论

    本文标题:定时任务的防重设计

    本文链接:https://www.haomeiwen.com/subject/ormgictx.html