定时任务是每个业务的共同需求。1:CloudNative1,Java内置方案,使用Timer创建java.util.TimerTask任务,在run方法中实现业务逻辑。通过java.util.Timer调度支持以固定频率执行。所有TimerTasks在同一个线程中串行执行,相互影响。也就是说,对于同一个Timer中的多个TimerTask任务,如果一个TimerTask任务正在执行,其他TimerTask任务即使到了执行时间也只能排队等待。如果发生异常,线程就会退出,整个定时任务就会失败。导入java.util.Timer;导入java.util.TimerTask;公共类TestTimerTask{publicstaticvoidmain(String[]args){TimerTasktimerTask=newTimerTask(){@Overridepublicvoidrun(){System.out.println(“地狱世界”);}};计时器timer=newTimer();timer.schedule(timerTask,10,3000);}}2使用ScheduledExecutorService设计一个基于线程池的定时任务方案,每个定时任务都会分配到线程池中的一个线程去执行,解决Timer定时器不能并发执行的问题,支持fixedRate和fixedDelay.导入java.util.concurrent.Executors;导入java.util.concurrent.ScheduledExecutorService;导入java.util.concurrent.TimeUnit;公共类TestTimerTask{publicstaticvoidmain(String[]args){ScheduledExecutorServiceses=Executors.newScheduledThreadPool(5);//固定频率执行,每5秒运行一次ses.scheduleAtFixedRate(newRunnable(){@Overridepublicvoidrun(){System.out.println("hellofixedRate");}},0,5,TimeUnit.秒);//按照固定延时执行,在上次执行后3秒后运行ses.scheduleWithFixedDelay(newRunnable(){@Overridepublicvoidrun(){System.out.println("hellofixedDelay)在上次执行后3秒");}},0,3,TimeUnit.SECONDS);}}复制代码02Spring内置解决方案CloudNativeSpringboot提供了一套轻量级定时任务工具SpringTask,可以通过注解轻松配置,支持cron表达式、fixedRate、fixedDelay。importorg.springframework.scheduling.annotation.EnableScheduling;importorg.springframework.scheduling.annotation.Scheduled;importorg.springframework.stereotype.Component;@Component@EnableSchedulingpublicclassMyTask{/***每30秒运行一次*/@Scheduled(cron="30****?")publicvoidtask1()throwsInterruptedException{System.out.println("hellocron");}/***每5秒运行一次*/@Scheduled(fixedRate=5000)publicvoidtask2()throwsInterruptedException{System.out.println("hellofixedRate");}/***在上次运行后3秒后运行*/@Scheduled(fixedDelay=3000)publicvoidtask3()throwsInterruptedException{System.out.println("hellofixedDelay");}}复制代码SpringTask与上面提到的两种方案相比,最大的优点是支持cron表达式,可以按照标准时间处理业务以固定的周期执行,比如每天什么时间、多少分钟执行。03业务幂等方案CloudNative目前的应用基本都是分布式部署,所有机器的代码都是一样的。前面介绍的Java和Spring自带的解决方案都是进程级的,每台机器运行在同一个定时任务上,每个时间点都会执行。这样就会给需要幂等业务的定时任务业务带来问题。比如每个月定时给用户推送消息,会推送多次。因此,很多应用自然而然地想到了使用分布式锁的解决方案。即每次定时任务执行前,先抢锁,抢到锁则执行任务,抢不到锁则不执行任务。抢锁的方式五花八门,比如用DB,zookeeper,redis。1使用DB或Zookeeper抢锁使用DB或Zookeeper抢锁的结构类似,原理是:时间到了,在回调方法中,先去抢锁。如果抢到锁,继续执行方法,没有抢到锁直接返回。执行该方法后,释放锁。示例代码如下:分钟每30秒运行一次*/@Scheduled(cron="30****?")publicvoidtask1()throwsException{StringlockName="task1";if(tryLock(lockName)){System.out.println("hellocron");释放锁(锁名);}else{返回;}}privatebooleantryLock(StringlockName){//TODOreturntrue;}privatevoidreleaseLock(StringlockName){//TODO}}复制代码currentthisDesign,细心的同学可以发现,其实可能导致重复执行任务。例如,任务执行得非常快。机器A抢到锁,执行任务后很快就释放了锁。B抢到本机上的锁后,还是会抢到锁,重新执行任务。2使用redis抢锁使用redis抢锁,其实架构和DB/zookeeper类似,但是redis锁支持过期时间,不需要主动释放锁,可以充分利用这个过期时间解决了任务执行过快释放锁的问题。重复执行问题的结构如下:示例代码如下:@Component@EnableSchedulingpublicclassMyTask{/***每30秒运行一次*/@Scheduled(cron="30****?”)publicvoidtask1()throwsInterruptedException{StringlockName="task1";if(tryLock(lockName,30)){System.out.println("hellocron");释放锁(锁名);}else{返回;}}privatebooleantryLock(StringlockName,longexpiredTime){//TODOreturntrue;}privatevoidreleaseLock(StringlockName){//TODO}}复制代码看这里,可能有同学有问题,加一个过期时间不够严谨,还是有可能重复任务?——的确,如果突然有台机器长时间fullgc,或者之前的任务还没有处理完(SpringTask和ScheduledExecutorService还在通过线程池处理任务),30之后还是可以调度任务的秒。3使用QuartzQuartz[1]是一个轻量级的任务调度框架,只需要定义Job(任务)、Trigger(触发器)和Scheduler(调度器)就可以实现定时调度能力。支持基于数据库的集群模式,可以实现任务的幂等执行。Quartz支持幂等执行任务。理论上,它还是会抢DB锁。我们看一下quartz的表结构:其中QRTZ_LOCKS是Quartz集群实现同步机制的行锁表。表结构如下:--QRTZ_LOCKS表结构CREATETABLEQRTZ_LOCKS(LOCK_NAMEvarchar(40)NOTNULL,PRIMARYKEY(LOCK_NAME))ENGINE=InnoDBDEFAULTCHARSET=utf8;--QRTZ_LOCKSrecords+------------------+|LOCK_NAME|+------------------+|CALENDAR_ACCESS||工作访问||MISFIRE_ACCESS||STATE_ACCESS|TRIGGER_ACCESS复制代码,我们可以看到QRTZ_LOCKS中有5条记录,分别代表5把锁,用于实现同步控制多个QuartzNode对Job、Trigger和Calendar的访问。04上面提到的开源任务调度中间件CloudNative中的方案在架构上有一个问题,就是每次调度都需要去抢锁,尤其是使用DB和Zookeeper去抢锁的时候,性能会比较差。如果增加到一定量,就会有更明显的调度延迟。另一个痛点是,如果业务要修改调度配置或增加任务,就得修改代码,重新发布应用。于是,开源社区出现了一堆任务调度中间件,通过任务调度系统可以创建、修改、调度任务。其中,XXL-JOB和ElasticJob在国内最受欢迎。1ElasticJobElasticJob[2]是基于Quartz开发,依托Zookeeper作为注册中心,轻量级、去中心化的分布式任务调度框架,已通过Apache开源。与Quartz相比,ElasticJob在功能上最大的不同在于它支持sharding,可以将一个任务的分片参数分发到不同的机器上执行。架构上最大的区别是使用了Zookeeper作为注册中心。不同的任务分配给不同的节点调度,无需抢锁触发。性能比Quartz强很多。架构图如下:开发也比较简单,和springboot对比。那么,你可以在配置文件中定义任务如下:0/5****?timeZone:GMT+08:00shardingTotalCount:3shardingItemParameters:0=北京,1=上海,2=广州scriptJob:elasticJobType:SCRIPTcron:0/10****?shardingTotalCount:3props:script.command.line:"echoSCRIPTJob:"manualScriptJob:elasticJobType:SCRIPTjobBootstrapBeanName:manualScriptJobBeanshardingTotalCount:9props:script.command.line:"echoManualSCRIPTJob:"复制代码实现任务接口如下:@ComponentpublicclassSpringBootShardingJobimplements{@JobShardingJobimplements简单覆盖publicvoidexecute(ShardingContextcontext){System.out.println("碎片总数="+context.getShardingTotalCount()+",shardingnumber="+context.getShardingItem()+",shardingparameter="+context.getShardingParameter());}}复制代码运行结果如下:totalnumberofshards=3,shardingnumber=0,分片参数=北京总分片数=3,分片数=1,分片参数=上海总分片数=3,分片数=2,分片参数=广州抄码同时ElasticJob还提供了一个简洁的UI,可以查看任务列表,支持修改、触发、停止、生效、失效操作。不幸的是,ElasticJob不支持动态创建任务。2XXL-JOBXXL-JOB[3]是一个开箱即用的轻量级分布式任务调度系统。其核心设计目标是快速开发、易学、轻量、易扩展。它在开源社区中广受欢迎。XXL-JOB是一种主从架构。Master负责任务调度,Slave负责任务执行。架构图如下:XXL-JOB接入也很方便。与ElasticJob定义任务实现类不同的是,它通过@XxlJob注解来定义JobHandler。@ComponentpublicclassSampleXxlJob{privatestaticLoggerlogger=LoggerFactory.getLogger(SampleXxlJob.class);/***1.简单任务示例(Bean模式)*/@XxlJob("demoJobHandler")publicReturnT
