延时队列&时间轮
延时队列
1、什么是延时队列
队列是存储消息的载体,延时队列存储的对象是延时消息。
所谓的延时消息,是指消息被发送以后,并不想让消费者立刻获取,而是等待特定的时间后,消费者才能获取这个消息进行消费。
和定时任务的区别:
1)定时任务有明确的触发时间,延时任务没有
2)定时任务有执行周期,而延时任务在某时间触发一段时间内执行,没有执行周期
3)定时任务一般执行的批处理操作是多个任务,而延时任务一般是单个任务
2、延时队列使用场景
1)在订单系统中,订单如果30分钟之内没有支付成功,那么这个订单将被关闭;生成订单60s之后给用户发通知短信;这时就可以使用延时队列来处理这些订单。
2)订单完成1小时后通知用户进行评价
3)用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时就可以将用户指令发送到延时队列,当指令的时间到了之后再将它推送到智能设备
4)订单7天自动确认收货
在我们签收商品后,物流系统会在7天后延时发送一个消息给支付系统,通知支付系统将款打给商家
3、延时队列的实现
1)Redis key过期通知
https://www.cnblogs.com/yangyongjie/p/14399707.html
2)Redis zset
Redis的zset(有序集合)也能实现延时队列。主要利用它的score属性,redis通过score来为集合中的成员进行从小到大的排序
我们将消息序列化成一个字符串作为zset的value,这个消息的到期处理时间作为score,然后用多个线程轮询zset获取到期的任务进行处理(多个线程是为了保证可用性,万一挂了一个线程还有其他线程可以继续处理。同时需要考虑多个线程的并发问题,确保任务不会被多次执行)。
步骤:
①:将延时消息的score 设置为到期的时间戳,消息内容序列化为value,调用zdd命令将此条延迟消息保存在key为delayqueue 的zset中
②:另起线程,循环从 delayqueue 中获取score小于等于当前时间戳的消息元素(zrangebyscore命令)
③:zrem删除获取到的元素
3)Kafka实现延时队列
原生的Kafka并不具备延时队列的功能,不过可以改造来实现延时队列(不建议使用Kafka来实现的延时队列)。
方案1:(延时精度较低)
①:在发送延时消息时,先将消息投递到延时队列(delay_topic)中(headers中设置延时时间,timestamp存消息发送初始发送时间戳)
②:定义一个服务去消费延时队列中的消息,将满足条件的消息再投递到目标队列(target_topic)中。
延时一般以秒来计,若要支持2小时(2*60*60=7200)之内的延时时间的消息,显然不能按照延时时间来创建7200个 delay_topic,一般是按照延时等级来划分 delay_topic,如设定5s,10s,30s,1min,5min,30min,1h,2h这些递增的延时等级,
延时消息只支持这些等级内的延时,然后延时的消息按照延时时间投递到不同等级的topic中(RocketMQ的延时实现与此类似)
4)RockerMQ延时消息
5)RabbitMQ死信队列
6)时间轮
时间轮
什么是时间轮?
简单来说,时间轮是一种高效利用线程资源进行批量化调度的一种调度模型。
通过把大批量的调度任务全部绑定到同一个调度器上,使用这一个调度器来进行所有任务的管理、触发、以及运行。
所以时间轮的模型能够高效管理各种延时任务、周期任务、通知任务。
时间轮是以时间作为刻度组成的一个环形队列,所以叫做时间轮。
1、Kafka时间轮实现
Kafka的延时操作,并没有使用JDK自带的Timer或DelayQueue来实现延时的功能,而是基于时间轮的概念自定义实现了一个用于延时功能的定时器(SystemTimer),基于时间轮的插入和删除操作的事件复杂度都是O(1)。
时间轮结构:
Kafka中的时间轮(TimingWheel) 是一个存储定时任务的环形队列,底层采用数组实现,数组内的每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务(TimerTask)。
时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格的个数(wheelSize)是固定的,整个时间轮的时间跨=跨度(interval=wheelSize*tickMS)。
时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,表盘指针可以将整个时间轮划分为到期部分和未到期部分,表盘指针当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的TimerTaskList中的所有任务。
若时间轮的 tickMs 为 1ms且 wheelSize 等于20,那么可以计算得出总体时间跨度 interval 为 20ms。
初始情况下表盘指针 currentTime 指向时间格0,此时有一个延时为2ms的任务插进来会存放到时间格为2的TimerTaskList中。随着时间不断推移,指针currentTime不断向前推进,过了2ms之后,当到达时间格2时,就需要将时间格2对应的TimerTaskList中的任务进行相应的到期操作。此时若又有一个延时为8ms的任务插进来,则会存放到时间格为10中。
若延时的时长大于时间轮的总体时间跨度20ms,那该怎么办,不能无限扩容wheelSize的大小,kafka为此引入了时间轮的概念,当任务的到期时间超过了当前时间轮所表示的时间范围时,就会尝试添加到上层时间轮中。
2、Netty的HashedWheelTimer时间轮实现:
https://www.cnblogs.com/yangyongjie/p/15839713.html
END.