前言
很多时候,业务系统有延时处理任务的需求,当任务量很大时,可能需要维护大量的定时器,或者进行低效的扫描。例如:电商下单成功后60s之后给用户发送短信通知,电商下单后30分钟未支付,则自动取消订单;出行乘客叫单后30秒没有司机接单,重新给周边司机推单等。实现这类需求有一些常见方案。
在讨论方案前我们需要搞清楚,延时任务与定时任务究竟有啥区别?定时任务有明确的执行时间或周期性,比如定时充电,要选开始充电的具体时间点,再比如每10分钟做一次未支付订单的状态检查。而延时任务没有这些特性,它具有不确定性,是在某个事件触发后一段时间内执行。下面以“出行乘客叫单后30秒内没有司机接单,重新给周边司机推单,直到司机接单”为例,讲解每种方案的实现。
方案分析
一、轮询扫描
启动一个Timer,30秒间隔轮询扫描订单表,检查每个未接订单创建时间是否超过30秒的整数倍,如果超过,重新给周边司机推送订单。
优点:简单易行。
缺点:如果订单量过大,延迟会比较高。
适用范围:这种方案一般适用于延时任务量比较少,对于延时精确度要求不高的任务。
二、多Timer触发
为每个订单创建一个Timer,并且设置为30秒的触发时间间隔,事件触发后检查订单状态,如果是初始状态,则继续执行,否则停止并释放Timer。
优点:简单易行,不需要轮询,精确度较高。
缺点:但每个订单要启动一个timer,比较耗资源。
适用范围:同样适用于延时任务量比较少的系统。
三、RabbitMQ死信队列
死信:Dead Letter,是指被拒绝或TTL过期或队列已达到最大长度限制,无法再入队的消息。利用DLX,当消息在一个队列中变成死信之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。
死信队列生产消费模型:
利用死信队列可以实现延时任务,每个订单创建一个消息,消息的TTL被设置为30秒。当消息过期后,通过交换机转发给业务消费队列,消费处理程序订阅业务消费队列,有消息则进行处理,检查订单状态,如果是初始状态,则重新对该订单创建一个消息。
优点:不需要轮询,精确度高。
缺点:引入消息组件,系统复杂度提高。
适用范围:适用于有大量延时任务需求的系统。
四、环形队列
环形队列本质上就是一个数组,收尾相接,形成了一个环。数组中的每个索引位称为槽(Slot),每个槽中放一个集合,用于盛放需要处理的任务。启动一个Timer,从Slot=0处开始,每秒钟向前移动一次,拿到当前Slot中任务数据进行处理,直到数组的最后一个Slot,再从Slot=0开始,循环往复。
环形队列任务处理流程:
实际的业务场景中还要考虑任务不丢失,故障恢复等问题,所以增加了持久化任务队列,将新加入槽中的任务持久化到Redis,和移除任务队列,将处理完的任务从Redis清除,进程故障恢复后初始化时将存在到Redis中的任务还原到环形队列的相应槽位中。
推单任务处理模型:
总结
本文主要讲解了实现延时任务的不同方案,各自有不同的优缺点及适用范围,大家有延时任务的需求时可参考,希望给大家带来帮助。