关于XXL-JOB-ONION


XXL-JOB-ONION是基于XXL-JOB的二次开发,我们基于XXL-JOB二次开发做了扩展,如添加ONION_BEAN运行模式、完善告警功能。

基于XXL-JOB二次开发:为kill job引入安全检查点机制_java

关于ONION_BEAN模式:

  • 对于不需要分片的定时任务:通过继承OnionJobHandler接口实现,而不是使用XXL-JOB提供的注解;

  • 对于需要分片的定时任务:通过继承OnionShardingJobHandler接口实现,而不是使用XXL-JOB提供的注解;


本篇内容:

  • 背景

  • 概要设计

  • 关于安全检查点的设计

  • 两种使用方式


背景


根据我们目前项目中的定时任务考察,发现定时任务大多都有一个共同点:每个定时任务都是从数据库查询一批数据出来处理,并且将每条数据转为一个Runable放入业务隔离的线程池中执行,例如:


@Component
public class OnionXxlJob implements OnionJobHandler<JobParam> {
   @Override
   public void doExecute(JobParam param) throws Exception {
       // 实际从数据库查询
       List<Integer> orderIds = Arrays.asList(10000, 11111);
       // 这里使用并行流模拟将每个订单放入线程池处理
       orderIds.parallelStream().forEach(orderService::handlerOrder);
   }
}


由于定时任务每次执行需要处理大批量数据,可能执行一次需要一个半小时,为解决每次版本更新或者应对突发数据量需要修改配置重启时都需要等待定时任务执行完成、想kill不敢kill的尴尬,我们为xxl-job引入安全检查点机制,灵感来源于jvm垃圾回收的安全检查点机制。


为什么说想kill不敢kill?


虽然需要涉及到数据库的操作都放在事务中,但并不是所有操作都能回滚,例如修改redis缓存数据、ES数据的操作都无法回滚(一般能放到事务提交之后的都放到后面执行),如果处理数据的过程中需要调用第三方接口,那么会更复杂(这点我们已经通过回放机制实现)。


假设几百个线程同时在处理,突然kill掉执行中的任务,意味着可能几百条数据处理出现问题,我们不得不找出这些数据,将未完成的步骤完成。


概要设计


我们先分析下XXL-JOB的kill job流程:xxl-job-admin前端发起kill请求,admin接收后向执行器发起kill请求并携带jobId,执行器根据jobId判断当前任务是否再执行,如果是则取消。


是否有一种方案,当定时任务管理员从后台发起kill job操作时,先等待任务进入一个安全点再kill任务呢?


我们来看下面这张图:

流程说明:

  • 1、管理员在admin操作向执行器发起kill请求、或执行器自身为实现阻塞策略向自己发起kill请求;

  • 2、执行器接收到命令后,先获取JobHandler,通知JobHandler的安全检查点管理员,不让新的Runable进入检查点;

  • 3、询问JobHandler的安全检查点计数器,可实现阻塞等待计数器的值为0再kill,或者异步等待计数器为0再kill;

  • 4、XXL-JOB会为每个Job分配一个ID,通过每个JobId持有一个计数器可以隔离同Job不同时间段触发的任务,当然,XXL-JOB也不允许同时执行多个相同的Job;


关于安全检查点的设计

基于XXL-JOB二次开发:为kill job引入安全检查点机制_java_02


安全检查点:定时任务中Runable调用的业务方法


比如定时关闭超时未支付订单的定时任务,查询出的每个超时订单,都会调用一个关闭订单的方法,那么这个方法就可以设置为安全检查点。


由于安全检查点需要结合业务考虑,因此我们在设计上,只提供一个检查点注解,让使用者自己决定将哪个方法设置为安全检查点、以及是否需要设置安全检查点。


缺点:通过AOP方式,只能支持将public的方法设置为安全检查点。


考虑过使用字节码增强方式,强行将检查行为插入private方法,但可能会导致应用启动速度变慢,如果有需要的话,我们再考虑这种方法。


但我们也提供了编程式解决这个缺点,与Spring实现的事务机制一样,安全检查点支持注解方式使用,也支持编程式使用。


关于安全检查管理员角色的设计:


只通过计数器统计当前进入安全检查点方法的线程数无法做到“原子性”,在任务执行的过程中,每时每刻都有新的Runable进入安全检查点方法,也每时每刻都有Runable从安全检查点方法出来。


通过安全检查管理员控制Runable是否可以进入安全检查点方法,在等待计数器值变为0之前,不再让Runable进入安全检查点,能够解决“原子性”等待问题。


两种使用方式


1、注解方式使用

基于XXL-JOB二次开发:为kill job引入安全检查点机制_java_03

注解方式使用要求方法必须是public的,与Spring注解式事务一样,也存在不生效的问题。


2、编程方式使用

基于XXL-JOB二次开发:为kill job引入安全检查点机制_java_04

编程式使用也仅需三个步骤:

  • 1、先判断当前是否存在安全检查点标志,如果存在则可以放弃执行;

  • 2、计数器加一,这是在OnionShardingJobHandler或OnionJobHandler接口中定义的default方法;

  • 3、计数器减一,这是在OnionShardingJobHandler或OnionJobHandler接口中定义的default方法;

https://mp.weixin.qq.com/s/eNRmYUxV5REdo8Dol8eQYQ