延迟任务的实现总结文档格式.docx

上传人:b****5 文档编号:16346486 上传时间:2022-11-23 格式:DOCX 页数:9 大小:20.67KB
下载 相关 举报
延迟任务的实现总结文档格式.docx_第1页
第1页 / 共9页
延迟任务的实现总结文档格式.docx_第2页
第2页 / 共9页
延迟任务的实现总结文档格式.docx_第3页
第3页 / 共9页
延迟任务的实现总结文档格式.docx_第4页
第4页 / 共9页
延迟任务的实现总结文档格式.docx_第5页
第5页 / 共9页
点击查看更多>>
下载资源
资源描述

延迟任务的实现总结文档格式.docx

《延迟任务的实现总结文档格式.docx》由会员分享,可在线阅读,更多相关《延迟任务的实现总结文档格式.docx(9页珍藏版)》请在冰豆网上搜索。

延迟任务的实现总结文档格式.docx

使用最小堆来实现优先级队列主要是因为最小堆在插入和获取时,时间复杂度相对都比较好,都是O(logN)。

下面例子实现了未来某个时间要触发的消息。

我把这些消息放在DelayQueue中,当消息的触发时间到,消费者就能拿到消息,并且消费,实现处理方法。

示例代码:

/*

*定义放在延迟队列中的对象,需要实现Delayed接口

*/

publicclassDelayedTaskimplementsDelayed{

privateint_expireInSecond=0;

publicDelayedTask(intdelaySecond){

Calendarcal=Calendar.getInstance();

cal.add(Calendar.SECOND,delaySecond);

_expireInSecond=(int)(cal.getTimeInMillis()/1000);

}

publicintcompareTo(Delayedo){

longd=(getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS));

return(d==0)?

0:

((d&

lt;

0)?

-1:

1);

publiclonggetDelay(TimeUnitunit){

//TODOAuto-generatedmethodstub

return_expireInSecond-(cal.getTimeInMillis()/1000);

}

下面定义了三个延迟任务,分别是10秒,5秒和15秒。

依次入队列,期望5秒钟后,5秒的消息先被获取到,然后每个5秒钟,依次获取到10秒数据和15秒的那个数据。

publicstaticvoidmain(String[]args)throwsInterruptedException{

SimpleDateFormatsdf=newSimpleDateFormat("

yyyy-MM-ddHH:

mm:

ss"

);

//定义延迟队列

DelayQueue&

DelayedTask&

gt;

delayQueue=newDelayQueue&

();

//定义三个延迟任务

DelayedTasktask1=newDelayedTask(10);

DelayedTasktask2=newDelayedTask(5);

DelayedTasktask3=newDelayedTask(15);

delayQueue.add(task1);

delayQueue.add(task2);

delayQueue.add(task3);

System.out.println(sdf.format(newDate())+"

start"

while(delayQueue.size()!

=0){

//如果没到时间,该方法会返回

DelayedTasktask=delayQueue.poll();

if(task!

=null){

Datenow=newDate();

System.out.println(sdf.format(now));

Thread.sleep(1000);

输出结果如下图:

DelayQueue是一种很好的实现方式,虽然是单机,但是可以多线程生产和消费,提高效率。

拿到消息后也可以使用异步线程去执行下一步的任务。

如果有分布式的需求可以使用Redis来实现消息的分发,如果对消息的可靠性有非常高的要求可以使用消息中间件:

使用DelayQueue需要考虑程序挂掉之后,内存里面未处理消息的丢失带来的影响。

3.JDKScheduledExecutorService

JDK自带的一种线程池,它能调度一些命令在一段时间之后执行,或者周期性的执行。

文章开头的一些业务场景主要使用第一种方式,即,在一段时间之后执行某个操作。

代码例子如下:

publicstaticvoidmain(String[]args){

ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(100);

for(inti=10;

i&

0;

i--){

executor.schedule(newRunnable(){

publicvoidrun(){

System.out.println(

"

Workstart,threadid:

"

+Thread.currentThread().getId()+"

+sdf.format(newDate()));

},i,TimeUnit.SECONDS);

执行结果:

ScheduledExecutorService的实现类ScheduledThreadPoolExecutor提供了一种并行处理的模型,简化了线程的调度。

DelayedWorkQueue是类似DelayQueue的实现,也是基于最小堆的、线程安全的数据结构,所以会有上例排序后输出的结果。

ScheduledExecutorService比上面一种DelayQueue更加实用。

因为,一般来说,使用DelayQueue获取消息后触发事件都会实用多线程的方式执行,以保证其他事件能准时进行。

而ScheduledThreadPoolExecutor就是对这个过程进行了封装,让大家更加方便的使用。

同时在加强了部分功能,比如定时触发命令。

4.时间轮

时间轮是一种非常惊艳的数据结构。

其在Linux内核中使用广泛,是Linux内核定时器的实现方法和基础之一。

按使用场景,大致可以分为两种时间轮:

原始时间轮和分层时间轮。

分层时间轮是原始时间轮的升级版本,来应对时间“槽”数量比较大的情况,对内存和精度都有很高要求的情况。

我们延迟任务的场景一般只需要用到原始时间轮就可以了。

原始时间轮:

如下图一个轮子,有8个“槽”,可以代表未来的一个时间。

如果以秒为单位,中间的指针每隔一秒钟转动到新的“槽”上面,就好像手表一样。

如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。

那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。

位置是在2圈之后的5上面(20%8+1)。

这个圈数需要记录在槽中的数据结构里面。

这个数据结构最重要的是两个指针,一个是触发任务的函数指针,另外一个是触发的总第几圈数。

时间轮可以用简单的数组或者是环形链表来实现。

相比DelayQueue的数据结构,时间轮在算法复杂度上有一定优势。

DelayQueue由于涉及到排序,需要调堆,插入和移除的复杂度是O(lgn),而时间轮在插入和移除的复杂度都是O

(1)。

时间轮比较好的开源实现是Netty的

//创建Timer,精度为100毫秒,

HashedWheelTimertimer=newHashedWheelTimer();

System.out.println(sdf.format(newDate()));

MyTasktask1=newMyTask();

MyTasktask2=newMyTask();

MyTasktask3=newMyTask();

timer.newTimeout(task1,5,TimeUnit.SECONDS);

timer.newTimeout(task2,10,TimeUnit.SECONDS);

timer.newTimeout(task3,15,TimeUnit.SECONDS);

//阻塞main线程

try{

System.in.read();

}catch(IOExceptione){

//TODOAuto-generatedcatchblock

e.printStackTrace();

其中HashedWheelTimer有多个构造函数。

其中:

ThreadFactory:

创建线程的类,默认Executors.defaultThreadFactory()。

TickDuration:

多少时间指针顺时针转一格,单位由下面一个参数提供。

TimeUnit:

上一个参数的时间单位。

TicksPerWheel:

时间轮上的格子数。

如果一个任务要在120s后执行,时间轮是默认参数的话,那么这个任务在时间轮上需要经过

120000ms/(512*100ms)=2轮

120000ms%(512*100ms)=176格。

在使用HashedWheelTimer的过程中,延迟任务的实现最好使用异步的,HashedWheelTimer的任务管理和执行都在一个线程里面。

如果任务比较耗时,那么指针就会延迟,导致整个任务就会延迟。

4.Quartz

quartz是一个企业级的开源的任务调度框架,quartz内部使用TreeSet来保存Trigger,如下图。

Java中的TreeSet是使用TreeMap实现,TreeMap是一个红黑树实现。

红黑树的插入和删除复杂度都是logN。

和最小堆相比各有千秋。

最小堆插入比红黑树快,删除顶层节点比红黑树慢。

相比上述的三种轻量级的实现功能丰富很多。

有专门的任务调度线程,和任务执行线程池。

quartz功能强大,主要是用来执行周期性的任务,当然也可以用来实现延迟任务。

但是如果只是实现一个简单的基于内存的延时任务的话,quartz就稍显庞大。

5.RedisZSet

Redis中的ZSet是一个有序的Set,内部使用HashMap和跳表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

publicclassZSetTest{

privateJedisPooljedisPool=null;

//Redis服务器IP

privateStringADDR="

10.23.22.42"

;

//Redis的端口号

privateintPORT=6379;

privateSimpleDateFormatsdf=newSimpleDateFormat("

publicvoidintJedis(){

jedisPool=newJedisPool(ADDR,PORT);

publicstaticvoidmain(String[]args){

ZSetTestzsetTest=newZSetTest();

zsetTest.intJedis();

zsetTest.addItem();

zsetTest.getItem();

zsetTest.deleteZSet();

publicvoiddeleteZSet(){

Jedisjedis=jedisPool.getResource();

jedis.del("

zset_test"

publicvoidaddItem(){

Calendarcal1=Calendar.getInstance();

cal1.add(Calendar.SECOND,10);

intsecond10later=(int)(cal1.getTimeInMillis()/1000);

Calendarcal2=Calendar.getInstance();

cal2.add(Calendar.SECOND,20);

intsecond20later=(int)(cal2.getTimeInMillis()/1000);

Calendarcal3=Calendar.getInstance();

cal3.add(Calendar.SECOND,30);

intsecond30later=(int)(cal3.getTimeInMillis()/1000);

Calendarcal4=Calendar.getInstance();

cal4.add(Calendar.SECOND,40);

intsecond40later=(int)(cal4.getTimeInMillis()/1000);

Calendarcal5=Calendar.getInstance();

cal5.add(Calendar.SECOND,50);

intsecond50later=(int)(cal5.getTimeInMillis()/1000);

jedis.zadd("

second50later,"

e"

second10later,"

a"

second30later,"

c"

second20later,"

b"

second40later,"

d"

addfinished."

publicvoidgetItem(){

while(true){

Set&

Tuple&

set=jedis.zrangeWithScores("

0,0);

Stringvalue=((Tuple)set.toArray()[0]).getElement();

intscore=(int)((Tuple)set.toArray()[0]).getScore();

intnowSecond=(int)(cal.getTimeInMillis()/1000);

if(nowSecond&

=score){

jedis.zrem("

value);

removedvalue:

+value);

if(jedis.zcard("

)&

=0)

{

zsetempty"

return;

}catch(InterruptedExceptione){

在用作延迟任务的时候,可以在添加数据的时候,使用zadd把score写成未来某个时刻的unix时间戳。

消费者使用zrangeWithScores获取优先级最高的(最早开始的的)任务。

注意,zrangeWithScores并不是取出来,只是看一下并不删除,类似于Queue的peek方法。

程序对最早的这个消息进行验证,是否到达要运行的时间,如果是则执行,然后删除zset中的数据。

如果不是,则继续等待。

由于zrangeWithScores和zrem是先后使用,所以有可能有并发问题,即两个线程或者两个进程都会拿到一样的一样的数据,然后重复执行,最后又都会删除。

如果是单机多线程执行,或者分布式环境下,可以使用Redis事务,也可以使用由Redis实现的分布式锁,或者使用下例中RedisScript。

你可以在Redis官方的Transaction章节找到事务的相关内容。

使用Redis的好处主要是:

1.解耦:

把任务、任务发起者、任务执行者的三者分开,逻辑更加清晰,程序强壮性提升,有利于任务发起者和执行者各自迭代,适合多人协作。

2.异常恢复:

由于使用Redis作为消息通道,消息都存储在Redis中。

如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。

3.分布式:

如果数据量较大,程序执行时间比较长,我们可以针对任务发起者和任务执行者进行分布式部署。

特别注意任务的执行者,也就是Redis的接收方需要考虑分布式锁的问题。

6.Jesque

Jesque是Resque的java实现,Resque是一个基于Redis的Ruby项目,用于后台的定时任务。

Jesque实现延迟任务的方法也是在Redis里面建立一个ZSet,和上例一样的处理方式。

上例提到在使用ZSet作为优先级队列的时候,由于zrangeWithScores和zrem没法保证原子性,所有在分布式环境下会有问题。

在Jesque中,它使用的RedisScript来解决这个问题。

RedisScript可以保证操作的原子性,相比事务也减少了一些网络开销,性能更加出色。

7.RabbitMQTTL和DXL

使用RabbitMQ的TTL和DXL实现延迟队列在这里不做详细的介绍,这篇文章描述的比较详细。

综上所述,解决延迟队列有很多种方法。

选择哪个解决方案也需要根据不同的数据量、实时性要求、已有架构和组件等因素进行判断和取舍。

对于比较简单的系统,可以使用数据库轮训的方式。

数据量稍大,实时性稍高一点的系统可以使用JDK延迟队列(也许需要解决程序挂了,内存中未处理任务丢失的情况)。

如果需要分布式横向扩展的话推荐使用Redis的方案。

但是对于系统中已有RabbitMQ,那RabbitMQ会是一个更好的方案。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 小学教育 > 学科竞赛

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1