大家好,我是三友~~

推迟使命在咱们日常日子中比较常见,比方订单付出超时撤销订单功用,又比方自动确认收货的功用等等。

所以本篇文章就来从完成到原理来盘点推迟使命的11种完成办法,这些办法并没有肯定的好坏之分,仅仅适用场景的不大相同。

微信大众号:三友的java日记

如何实现延迟任务,这11种方式才算优雅!

DelayQueue

DelayQueue是JDK供给的api,是一个推迟行列

如何实现延迟任务,这11种方式才算优雅!

DelayQueue泛型参数得完成Delayed接口,Delayed继承了Comparable接口。

如何实现延迟任务,这11种方式才算优雅!

getDelay办法回来这个使命还剩多久时刻能够履行,小于0的时分阐明能够这个推迟使命到了履行的时刻了。

compareTo这个是对使命排序的,确保最先到推迟时刻的使命排到行列的头。

来个demo

@Getter
publicclassSanYouTaskimplementsDelayed{
privatefinalStringtaskContent;
privatefinalLongtriggerTime;
publicSanYouTask(StringtaskContent,LongdelayTime){
this.taskContent=taskContent;
this.triggerTime=System.currentTimeMillis()+delayTime*1000;
}
@Override
publiclonggetDelay(TimeUnitunit){
returnunit.convert(triggerTime-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
@Override
publicintcompareTo(Delayedo){
returnthis.triggerTime.compareTo(((SanYouTask)o).triggerTime);
}
}

SanYouTask完成了Delayed接口,结构参数

  • taskContent:推迟使命的详细的内容
  • delayTime:推迟时刻,秒为单位

测验

@Slf4j
publicclassDelayQueueDemo{
publicstaticvoidmain(String[]args){
DelayQueue<SanYouTask>sanYouTaskDelayQueue=newDelayQueue<>();
newThread(()->{
while(true){
try{
SanYouTasksanYouTask=sanYouTaskDelayQueue.take();
log.info("获取到推迟使命:{}",sanYouTask.getTaskContent());
}catch(Exceptione){
}
}
}).start();
log.info("提交推迟使命");
sanYouTaskDelayQueue.offer(newSanYouTask("三友的java日记5s",5L));
sanYouTaskDelayQueue.offer(newSanYouTask("三友的java日记3s",3L));
sanYouTaskDelayQueue.offer(newSanYouTask("三友的java日记8s",8L));
}
}

敞开一个线程从DelayQueue中获取使命,然后提交了三个使命,推迟时刻分为别5s,3s,8s。

测验成果:

如何实现延迟任务,这11种方式才算优雅!

成功完成了推迟使命。

完成原理

如何实现延迟任务,这11种方式才算优雅!

offer办法在提交使命的时分,会经过依据compareTo的完成对使命进行排序,将最先需求被履行的使命放到行列头。

take办法获取使命的时分,会拿到行列头部的元素,也便是行列中最早需求被履行的使命,经过getDelay回来值判别使命是否需求被立刻履行,假如需求的话,就回来使命,假如不需求就会等候这个使命到推迟时刻的剩余时刻,当时刻到了就会将使命回来。

Timer

Timer也是JDK供给的api

先来demo

@Slf4j
publicclassTimerDemo{
publicstaticvoidmain(String[]args){
Timertimer=newTimer();

log.info("提交推迟使命");
timer.schedule(newTimerTask(){
@Override
publicvoidrun(){
log.info("履行推迟使命");
}
},5000);
}
}

经过schedule提交一个推迟时刻为5s的推迟使命

如何实现延迟任务,这11种方式才算优雅!

完成原理

提交的使命是一个TimerTask

publicabstractclassTimerTaskimplementsRunnable{
//疏忽其它特点

longnextExecutionTime;
}

TimerTask内部有一个nextExecutionTime特点,代表下一次使命履行的时刻,在提交使命的时分管帐算出nextExecutionTime值。

Timer内部有一个TaskQueue方针,用来保存TimerTask使命的,会依据nextExecutionTime来排序,确保能够快速获取到最早需求被履行的推迟使命。

在Timer内部还有一个履行使命的线程TimerThread,这个线程就跟DelayQueue demo中敞开的线程效果是相同的,用来履行到了推迟时刻的使命。

所以总的来看,Timer有点像整体封装了DelayQueue demo中的一切东西,让用起来简略点。

尽管Timer用起来比较简略,可是在阿里规范中是不推荐运用的,主要是有以下几点原因:

  • Timer运用单线程来处理使命,长时刻运转的使命会导致其他使命的延时处理
  • Timer没有对运转时反常进行处理,一旦某个使命触发运转时反常,会导致整个Timer溃散,不安全

ScheduledThreadPoolExecutor

由于Timer在运用上有一定的问题,所以在JDK1.5版本的时分供给了ScheduledThreadPoolExecutor,这个跟Timer的效果差不多,而且他们的办法的命名都是差不多的,可是ScheduledThreadPoolExecutor解决了单线程和反常溃散等问题。

来个demo

@Slf4j
publicclassScheduledThreadPoolExecutorDemo{
publicstaticvoidmain(String[]args){
ScheduledThreadPoolExecutorexecutor=newScheduledThreadPoolExecutor(2,newThreadPoolExecutor.CallerRunsPolicy());
log.info("提交推迟使命");
executor.schedule(()->log.info("履行推迟使命"),5,TimeUnit.SECONDS);
}
}

成果

如何实现延迟任务,这11种方式才算优雅!

完成原理

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,也便是继承了线程池,所以能够有许多个线程来履行使命。

ScheduledThreadPoolExecutor在结构的时分会传入一个DelayedWorkQueue堵塞行列,所以线程池内部的堵塞行列是DelayedWorkQueue。

如何实现延迟任务,这11种方式才算优雅!

在提交推迟使命的时分,使命会被封装一个使命会被封装成ScheduledFutureTask方针,然后放到DelayedWorkQueue堵塞行列中。

如何实现延迟任务,这11种方式才算优雅!

ScheduledFutureTask

ScheduledFutureTask完成了前面说到的Delayed接口,所以其实能够猜到DelayedWorkQueue会依据ScheduledFutureTask对于Delayed接口的完成来排序,所以线程能够获取到最早到推迟时刻的使命。

当线程从DelayedWorkQueue中获取到需求履行的使命之后就会履行使命。

RocketMQ

RocketMQ是阿里开源的一款音讯中心件,完成了推迟音讯的功用,假如有对RocketMQ不熟悉的小伙伴能够看一下我之前写的RocketMQ保姆级教程和RocketMQ音讯时间短而又精彩的一生 这两篇文章。

RocketMQ推迟音讯的推迟时刻默认有18个等级。

如何实现延迟任务,这11种方式才算优雅!

当发送音讯的时分只需求指定推迟等级即可。假如这18个等级的推迟时刻不符和你的要求,能够修改RocketMQ服务端的装备文件。

来个demo

依靠

<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>

<!--web依靠-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

装备文件

rocketmq:
name-server:192.168.200.144:9876#服务器ip:nameServer端口
producer:
group:sanyouProducer

controller类,经过DefaultMQProducer发送推迟音讯到sanyouDelayTaskTopic这个topic,推迟等级为2,也便是推迟时刻为5s的意思。

@RestController
@Slf4j
publicclassRocketMQDelayTaskController{
@Resource
privateDefaultMQProducerproducer;
@GetMapping("/rocketmq/add")
publicvoidaddTask(@RequestParam("task")Stringtask)throwsException{
Messagemsg=newMessage("sanyouDelayTaskTopic","TagA",task.getBytes(RemotingHelper.DEFAULT_CHARSET));
msg.setDelayTimeLevel(2);
//发送音讯并得到音讯的发送成果,然后打印
log.info("提交推迟使命");
producer.send(msg);
}
}

创立一个顾客,监听sanyouDelayTaskTopic的音讯。

@Component
@RocketMQMessageListener(consumerGroup="sanyouConsumer",topic="sanyouDelayTaskTopic")
@Slf4j
publicclassSanYouDelayTaskTopicListenerimplementsRocketMQListener<String>{
@Override
publicvoidonMessage(Stringmsg){
log.info("获取到推迟使命:{}",msg);
}
}

发动运用,浏览器输入以下链接增加使命

http://localhost:8080/rocketmq/add?task=sanyou

测验成果:

如何实现延迟任务,这11种方式才算优雅!

完成原理

如何实现延迟任务,这11种方式才算优雅!

生产者发送推迟音讯之后,RocketMQ服务端在接收到音讯之后,会去依据推迟级别是否大于0来判别是否是推迟音讯

  • 假如不大于0,阐明不是推迟音讯,那就会将音讯保存到指定的topic中
  • 假如大于0,阐明是推迟音讯,此刻RocketMQ会进行一波偷梁换柱的操作,将音讯的topic改成SCHEDULE_TOPIC_XXXX中,XXXX不是占位符,然后存储

在BocketMQ内部有一个推迟使命,相当所以一个守时使命,这个使命就会获取SCHEDULE_TOPIC_XXXX中的音讯,判别音讯是否到了推迟时刻,假如到了,那么就会将音讯的topic存储到本来真实的topic(拿咱们的例子来说便是sanyouDelayTaskTopic)中,之后顾客就能够从真实的topic中获取到音讯了。

如何实现延迟任务,这11种方式才算优雅!

守时使命

RocketMQ这种完成办法相比于前面说到的三种愈加牢靠,由于前面说到的三种使命内容都是存在内存的,服务器重启使命就丢了,假如要完成使命不丢还得自己完成逻辑,可是RocketMQ音讯有耐久化机制,能够确保使命不丢失。

RabbitMQ

RabbitMQ也是一款音讯中心件,经过RabbitMQ的死信行列也可所以先推迟使命的功用。

demo

引进RabbitMQ的依靠

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

装备文件

spring:
rabbitmq:
host:192.168.200.144#服务器ip
port:5672
virtual-host:/

RabbitMQ死信行列的装备类,后边说原理的时分会介绍干啥的

@Configuration
publicclassRabbitMQConfiguration{

@Bean
publicDirectExchangesanyouDirectExchangee(){
returnnewDirectExchange("sanyouDirectExchangee");
}
@Bean
publicQueuesanyouQueue(){
returnQueueBuilder
//指定行列名称,并耐久化
.durable("sanyouQueue")
//设置行列的超时时刻为5秒,也便是推迟使命的时刻
.ttl(5000)
//指定死信交换机
.deadLetterExchange("sanyouDelayTaskExchangee")
.build();
}
@Bean
publicBindingsanyouQueueBinding(){
returnBindingBuilder.bind(sanyouQueue()).to(sanyouDirectExchangee()).with("");
}
@Bean
publicDirectExchangesanyouDelayTaskExchange(){
returnnewDirectExchange("sanyouDelayTaskExchangee");
}
@Bean
publicQueuesanyouDelayTaskQueue(){
returnQueueBuilder
//指定行列名称,并耐久化
.durable("sanyouDelayTaskQueue")
.build();
}
@Bean
publicBindingsanyouDelayTaskQueueBinding(){
returnBindingBuilder.bind(sanyouDelayTaskQueue()).to(sanyouDelayTaskExchange()).with("");
}
}

RabbitMQDelayTaskController用来发送音讯,这里没指定推迟时刻,是由于在声明行列的时分指定了推迟时刻为5s

@RestController
@Slf4j
publicclassRabbitMQDelayTaskController{
@Resource
privateRabbitTemplaterabbitTemplate;
@GetMapping("/rabbitmq/add")
publicvoidaddTask(@RequestParam("task")Stringtask)throwsException{
//音讯ID,需求封装到CorrelationData中
CorrelationDatacorrelationData=newCorrelationData(UUID.randomUUID().toString());
log.info("提交推迟使命");
//发送音讯
rabbitTemplate.convertAndSend("sanyouDirectExchangee","",task,correlationData);
}
}

发动运用,浏览器输入以下链接增加使命

http://localhost:8080/rabbitmq/add?task=sanyou

测验成果,成功完成5s的推迟使命

如何实现延迟任务,这11种方式才算优雅!

完成原理

如何实现延迟任务,这11种方式才算优雅!

整个工作流程如下:

  • 音讯发送的时分会将音讯发送到sanyouDirectExchange这个交换机上
  • 由于sanyouDirectExchange绑定了sanyouQueue,所以音讯会被路由到sanyouQueue这个行列上
  • 由于sanyouQueue没有顾客消费音讯,而且又设置了5s的过期时刻,所以当音讯过期之后,音讯就被放到绑定的sanyouDelayTaskExchange死信交换机中
  • 音讯抵达sanyouDelayTaskExchange交换机后,由于跟sanyouDelayTaskQueue进行了绑定,所以音讯就被路由到sanyouDelayTaskQueue中,顾客就能从sanyouDelayTaskQueue中拿到音讯了

上面说的行列与交换机的绑定关系,便是上面的装备类所干的事。

其实从这个单从音讯流通的视点能够看出,RabbitMQ跟RocketMQ完成有相似之处。

音讯最开端都并没有放到终究顾客消费的行列中,而都是放到一个中心行列中,等音讯到了过期时刻或许说是推迟时刻,音讯就会被放到终究的行列供顾客音讯。

只不过RabbitMQ需求你显示的手动指定音讯地点的中心行列,而RocketMQ是在内部已经做好了这块逻辑。

除了根据RabbitMQ的死信行列来做,RabbitMQ官方还供给了延时插件,也能够完成推迟音讯的功用,这个插件的大致原理也跟上面说的相同,延时音讯会被先保存在一个中心的地方,叫做Mnesia,然后有一个守时使命去查询最近需求被投递的音讯,将其投递到方针行列中。

监听Redis过期key

在Redis中,有个发布订阅的机制

如何实现延迟任务,这11种方式才算优雅!

生产者在音讯发送时需求到指定发送到哪个channel上,顾客订阅这个channel就能获取到音讯。图中channel了解成MQ中的topic。

而且在Redis中,有许多默认的channel,只不过向这些channel发送音讯的生产者不是咱们写的代码,而是Redis自身。这里面就有这么一个channel叫做__keyevent@<db>__:expired,db是指Redis数据库的序号。

当某个Redis的key过期之后,Redis内部会发布一个事情到__keyevent@<db>__:expired这个channel上,只需监听这个事情,那么就能够获取到过期的key。

所以根据监听Redis过期key完成推迟使命的原理如下:

  • 将推迟使命作为key,过期时刻设置为推迟时刻
  • 监听__keyevent@<db>__:expired这个channel,那么一旦推迟使命到了过期时刻(推迟时刻),那么就能够获取到这个使命

来个demo

Spring已经完成了监听__keyevent@*__:expired这个channel这个功用,__keyevent@*__:expired中的*代表通配符的意思,监听一切的数据库。

所以demo写起来就很简略了,只需4步即可

依靠

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

装备文件

spring:
redis:
host:192.168.200.144
port:6379

装备类

@Configuration
publicclassRedisConfiguration{
@Bean
publicRedisMessageListenerContainerredisMessageListenerContainer(RedisConnectionFactoryconnectionFactory){
RedisMessageListenerContainerredisMessageListenerContainer=newRedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(connectionFactory);
returnredisMessageListenerContainer;
}
@Bean
publicKeyExpirationEventMessageListenerredisKeyExpirationListener(RedisMessageListenerContainerredisMessageListenerContainer){
returnnewKeyExpirationEventMessageListener(redisMessageListenerContainer);
}
}

KeyExpirationEventMessageListener完成了对__keyevent@*__:expiredchannel的监听

如何实现延迟任务,这11种方式才算优雅!

当KeyExpirationEventMessageListener收到Redis发布的过期Key的音讯的时分,会发布RedisKeyExpiredEvent事情

如何实现延迟任务,这11种方式才算优雅!

所以咱们只需求监听RedisKeyExpiredEvent事情就能够拿到过期音讯的Key,也便是推迟音讯。

对RedisKeyExpiredEvent事情的监听完成MyRedisKeyExpiredEventListener

@Component
publicclassMyRedisKeyExpiredEventListenerimplementsApplicationListener<RedisKeyExpiredEvent>{
@Override
publicvoidonApplicationEvent(RedisKeyExpiredEventevent){
byte[]body=event.getSource();
System.out.println("获取到推迟音讯:"+newString(body));
}
}

代码写好,发动运用

之后我直接经过Redis指令设置音讯,就没经过代码发送音讯了,音讯的key为sanyou,值为task,值不重要,过期时刻为5s

set sanyou task
expire sanyou 5

成功获取到推迟使命

如何实现延迟任务,这11种方式才算优雅!

尽管这种办法能够完成推迟使命,可是这种办法比较多

使命存在推迟

Redis过期事情的发布不是指key到了过期时刻就发布,而是key到了过期时刻被铲除之后才会发布事情。

而Redis过期key的两种铲除策略,便是面试八股文常背的两种:

  • 惰性铲除。当这个key过期之后,拜访时,这个Key才会被铲除
  • 守时铲除。后台会定期检查一部分key,假如有key过期了,就会被铲除

所以即便key到了过期时刻,Redis也不一定会发送key过期事情,这就到导致尽管推迟使命到了推迟时刻也或许获取不到推迟使命。

丢音讯太频繁

Redis完成的发布订阅形式,音讯是没有耐久化机制,当音讯发布到某个channel之后,假如没有客户端订阅这个channel,那么这个音讯就丢了,并不会像MQ相同进行耐久化,等有顾客订阅的时分再给顾客消费。

所以说,假定服务重启期间,某个生产者或许是Redis自身发布了一条音讯到某个channel,由于服务重启,没有监听这个channel,那么这个音讯天然就丢了。

音讯消费只要播送形式

Redis的发布订阅形式音讯消费只要播送形式一种。

所谓的播送形式便是多个顾客订阅同一个channel,那么每个顾客都能消费到发布到这个channel的一切音讯。

如何实现延迟任务,这11种方式才算优雅!

如图,生产者发布了一条音讯,内容为sanyou,那么两个顾客都能够同时收到sanyou这条音讯。

所以,假如经过监听channel来获取推迟使命,那么一旦服务实例有多个的话,还得确保音讯不能重复处理,额外地增加了代码开发量。

接收到一切key的某个事情

这个不属于Redis发布订阅形式的问题,而是Redis自身事情告诉的问题。

当监听了__keyevent@<db>__:expired的channel,那么一切的Redis的key只需发生了过期事情都会被告诉给顾客,不论这个key是不是顾客想接收到的。

所以假如你只想消费某一类音讯的key,那么还得自行加一些标记,比方音讯的key加个前缀,消费的时分判别一下带前缀的key便是需求消费的使命。

Redisson的RDelayedQueue

Redisson他是Redis的儿子(Redis son),根据Redis完成了非常多的功用,其间最常运用的便是Redis分布式锁的完成,可是除了完成Redis分布式锁之外,它还完成了推迟行列的功用。

先来个demo

引进pom

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.1</version>
</dependency>

封装了一个RedissonDelayQueue类

@Component
@Slf4j
publicclassRedissonDelayQueue{
privateRedissonClientredissonClient;
privateRDelayedQueue<String>delayQueue;
privateRBlockingQueue<String>blockingQueue;
@PostConstruct
publicvoidinit(){
initDelayQueue();
startDelayQueueConsumer();
}
privatevoidinitDelayQueue(){
Configconfig=newConfig();
SingleServerConfigserverConfig=config.useSingleServer();
serverConfig.setAddress("redis://localhost:6379");
redissonClient=Redisson.create(config);
blockingQueue=redissonClient.getBlockingQueue("SANYOU");
delayQueue=redissonClient.getDelayedQueue(blockingQueue);
}
privatevoidstartDelayQueueConsumer(){
newThread(()->{
while(true){
try{
Stringtask=blockingQueue.take();
log.info("接收到推迟使命:{}",task);
}catch(Exceptione){
e.printStackTrace();
}
}
},"SANYOU-Consumer").start();
}
publicvoidofferTask(Stringtask,longseconds){
log.info("增加推迟使命:{}推迟时刻:{}s",task,seconds);
delayQueue.offer(task,seconds,TimeUnit.SECONDS);
}
}

这个类在创立的时分会去初始化推迟行列,创立一个RedissonClient方针,之后经过RedissonClient方针获取到RDelayedQueue和RBlockingQueue方针,传入的行列姓名叫SANYOU,这个姓名无所谓。

当推迟行列创立之后,会敞开一个推迟使命的消费线程,这个线程会一向从RBlockingQueue中经过take办法堵塞获取推迟使命。

增加使命的时分是经过RDelayedQueue的offer办法增加的。

controller类,经过接口增加使命,推迟时刻为5s

@RestController
publicclassRedissonDelayQueueController{
@Resource
privateRedissonDelayQueueredissonDelayQueue;
@GetMapping("/add")
publicvoidaddTask(@RequestParam("task")Stringtask){
redissonDelayQueue.offerTask(task,5);
}
}

发动项目,在浏览器输入如下衔接,增加使命

http://localhost:8080/add?task=sanyou

静静等候5s,成功获取到使命。

如何实现延迟任务,这11种方式才算优雅!

完成原理

如下是Redisson推迟行列的完成原理

如何实现延迟任务,这11种方式才算优雅!

SANYOU前面的前缀都是固定的,Redisson创立的时分会拼上前缀。

  • redisson_delay_queue_timeout:SANYOU,sorted set数据类型,寄存一切推迟使命,依照推迟使命的到期时刻戳(提交使命时的时刻戳 + 推迟时刻)来排序的,所以列表的最前面的第一个元素便是整个推迟行列中最早要被履行的使命,这个概念很重要
  • redisson_delay_queue:SANYOU,list数据类型,也是寄存一切的使命,可是研讨下来发现好像没什么用。。
  • SANYOU,list数据类型,被称为方针行列,这个里面寄存的使命都是已经到了推迟时刻的,能够被顾客获取的使命,所以上面demo中的RBlockingQueue的take办法是从这个方针行列中获取到使命的
  • redisson_delay_queue_channel:SANYOU,是一个channel,用来告诉客户端敞开一个推迟使命

使命提交的时分,Redisson会将使命放到redisson_delay_queue_timeout:SANYOU中,分数便是提交使命的时刻戳+推迟时刻,便是推迟使命的到期时刻戳

Redisson客户端内部经过监听redisson_delay_queue_channel:SANYOU这个channel来提交一个推迟使命,这个推迟使命能够确保将redisson_delay_queue_timeout:SANYOU中到了推迟时刻的使命从redisson_delay_queue_timeout:SANYOU中移除,存到SANYOU这个方针行列中。

所以顾客就能够从SANYOU这个方针行列获取到推迟使命了。

所以从这能够看出,Redisson的推迟使命的完成跟前面说的MQ的完成都是异曲同工,最开端使命放到中心的一个地方,叫做redisson_delay_queue_timeout:SANYOU,然后会敞开一个类似于守时使命的一个东西,去判别这个中心地方的音讯是否到了推迟时刻,到了再放到终究的方针的行列供顾客消费。

Redisson的这种完成办法比监听Redis过期key的完成办法愈加牢靠,由于音讯都存在list和sorted set数据类型中,所以音讯很少丢。

上述说的两种Redis的计划更详细的介绍,能够查看我之前写的用Redis完成推迟行列,我研讨了两种计划,发现并不简略这篇文章。

Netty的HashedWheelTimer

先来个demo

@Slf4j
publicclassNettyHashedWheelTimerDemo{
publicstaticvoidmain(String[]args){
HashedWheelTimertimer=newHashedWheelTimer(100,TimeUnit.MILLISECONDS,8);
timer.start();
log.info("提交推迟使命");
timer.newTimeout(timeout->log.info("履行推迟使命"),5,TimeUnit.SECONDS);
}
}

测验成果

如何实现延迟任务,这11种方式才算优雅!

完成原理

如何实现延迟任务,这11种方式才算优雅!

如图,时刻轮会被分成许多格子(上述demo中的8就代表了8个格子),一个格子代表一段时刻(上述demo中的100就代表一个格子是100ms),所以上述demo中,每800ms会走一圈。

当使命提交的之后,会依据使命的到期时刻进行hash取模,计算出这个使命的履行时刻地点详细的格子,然后增加到这个格子中,经过假如这个格子有多个使命,会用链表来保存。所以这个使命的增加有点像HashMap储存元素的原理。

HashedWheelTimer内部会敞开一个线程,轮询每个格子,找到到了推迟时刻的使命,然后履行。

由于HashedWheelTimer也是单线程来处理使命,所以跟Timer相同,长时刻运转的使命会导致其他使命的延时处理。

前面Redisson中说到的客户端推迟使命便是根据Netty的HashedWheelTimer完成的。

Hutool的SystemTimer

Hutool东西类也供给了推迟使命的完成SystemTimer

demo

@Slf4j
publicclassSystemTimerDemo{
publicstaticvoidmain(String[]args){
SystemTimersystemTimer=newSystemTimer();
systemTimer.start();
log.info("提交推迟使命");
systemTimer.addTask(newTimerTask(()->log.info("履行推迟使命"),5000));
}
}

履行成果

如何实现延迟任务,这11种方式才算优雅!

Hutool底层其实也用到了时刻轮。

Qurtaz

Qurtaz是一款开源作业调度框架,根据Qurtaz供给的api也能够完成推迟使命的功用。

demo

依靠

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>

SanYouJob完成Job接口,当使命抵达履行时刻的时分会调用execute的完成,从context能够获取到使命的内容

@Slf4j
publicclassSanYouJobimplementsJob{
@Override
publicvoidexecute(JobExecutionContextcontext)throwsJobExecutionException{
JobDetailjobDetail=context.getJobDetail();
JobDataMapjobDataMap=jobDetail.getJobDataMap();
log.info("获取到推迟使命:{}",jobDataMap.get("delayTask"));
}
}

测验类

publicclassQuartzDemo{
publicstaticvoidmain(String[]args)throwsSchedulerException,InterruptedException{
//1.创立Scheduler的工厂
SchedulerFactorysf=newStdSchedulerFactory();
//2.从工厂中获取调度器实例
Schedulerscheduler=sf.getScheduler();
//6.发动调度器
scheduler.start();
//3.创立JobDetail,Job类型便是上面说的SanYouJob
JobDetailjb=JobBuilder.newJob(SanYouJob.class)
.usingJobData("delayTask","这是一个推迟使命")
.build();
//4.创立Trigger
Triggert=TriggerBuilder.newTrigger()
//使命的触发时刻便是推迟使命到的推迟时刻
.startAt(DateUtil.offsetSecond(newDate(),5))
.build();
//5.注册使命和守时器
log.info("提交推迟使命");
scheduler.scheduleJob(jb,t);
}
}

履行成果:

如何实现延迟任务,这11种方式才算优雅!

完成原理

核心组件

  • Job:表示一个使命,execute办法的完成是对使命的履行逻辑
  • JobDetail:使命的详情,能够设置使命需求的参数等信息
  • Trigger:触发器,是用来触发业务的履行,比方说指定5s后触发使命,那么使命就会在5s后触发
  • Scheduler:调度器,内部能够注册多个使命和对应使命的触发器,之后会调度使命的履行

如何实现延迟任务,这11种方式才算优雅!

发动的时分会敞开一个QuartzSchedulerThread调度线程,这个线程会去判别使命是否到了履行时刻,到的话就将使命交给使命线程池去履行。

无限轮询推迟使命

无限轮询的意思便是敞开一个线程不停的去轮询使命,当这些使命抵达了推迟时刻,那么就履行使命。

demo

@Slf4j
publicclassPollingTaskDemo{
privatestaticfinalList<DelayTask>DELAY_TASK_LIST=newCopyOnWriteArrayList<>();
publicstaticvoidmain(String[]args){
newThread(()->{
while(true){
try{
for(DelayTaskdelayTask:DELAY_TASK_LIST){
if(delayTask.triggerTime<=System.currentTimeMillis()){
log.info("处理推迟使命:{}",delayTask.taskContent);
DELAY_TASK_LIST.remove(delayTask);
}
}
TimeUnit.MILLISECONDS.sleep(100);
}catch(Exceptione){
}
}
}).start();
log.info("提交推迟使命");
DELAY_TASK_LIST.add(newDelayTask("三友的java日记",5L));
}
@Getter
@Setter
publicstaticclassDelayTask{
privatefinalStringtaskContent;
privatefinalLongtriggerTime;
publicDelayTask(StringtaskContent,LongdelayTime){
this.taskContent=taskContent;
this.triggerTime=System.currentTimeMillis()+delayTime*1000;
}
}
}

使命能够存在数据库又或许是内存,看详细的需求,这里我为了简略就放在内存里了。

履行成果:

如何实现延迟任务,这11种方式才算优雅!

这种操作简略,可是便是效率低下,每次都得遍历一切的使命。

最终

最终,本文一切示例代码地址:

github.com/sanyou3/del…

查找关注大众号三友的java日记,及时干货不错失,大众号致力于经过画图加上通俗易懂的语言讲解技能,让技能愈加简单学习。

往期抢手文章推荐

怎么去阅读源码,我总结了18条心法

怎么写出美丽代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

两万字盘点那些被玩烂了的设计形式

扒一扒Bean注入到Spring的那些姿态

RocketMQ音讯时间短而又精彩的一生