前言
开发过程中,往往有一些延迟任务的需求,比如:
订单15分钟未支付,自动取消订单。
用户支付成功后,给用户发短信。
这种情况我们一般采用延迟任务来实现。
延迟任务和定时任务什么区别呢?
定时任务是在一个指定时间执行,延迟任务一般是在上一次任务执行完成后在同一个延迟间隔执行。
方案
数据库轮询
可以通过一个线程定时扫描数据库,根据时间执行update或insert操作。 可以采用quartz实现:
Demo:
这样程序会每隔3秒,输出:要去数据库扫描啦。。。
优点:
简单,支持集群操作。
缺点:
消耗内存。
如果数据量大,每扫描一次db性能存在损失。
JDK延迟队列
采用JDK自带的DelayQueue实现,是一个无界阻塞队列,只有在延迟时间满足时才在队列获取元素,流程如下:
producer->生产一个任务->放入delayedQueue->通过poll()/take()方法获取一个任务->交给消费者
poll:获取并移除队列的超时元素,没有则返回空;
take:获取并移除队列的超时元素,如果没有,线程阻塞,直到满足条件返回结果;
Demo:
输出:
优缺点:
效率高,低延迟
服务器重启,数据丢失,集群扩展受限于单机/内存,易出现OOM异常,代码复杂度高
时间片轮训
原理:
类似于时钟顺时针执行,每一次移动算一个tick。时间片主要有三个属性:
ticksPerWheel:一圈的tick数
tickDuration:一个tick持续的时间
timeUnit:时间单位
比如现实中的时钟就是:ticksPerWheel = 60,tickDurateion = 1,timeUnit = s。
如果当前时针在1上,下一个任务要4秒后执行,这这个任务的线程回调放在5上。 如果要在20秒后执行,由于一圈有8个位置,如果20秒,指针需要转两圈的5上面。
实现: 利用Netty的HashedWheelTimer:
Demo:
输出:
优缺点:
效率高,延迟低,代码复杂度比delayQueue低。
受限于单机内存,易出现OOM,机器重启数据丢失,集群难扩展。
消息队列
可以采用消息队列简单的实现延迟队列,比如:
RabbitMQ对Queue和Message设置x-message-tt来控制消息的生存时间,如果超时消息变为dead leter。
RabbitMQ的Queue设置x-dead-leter-exchange和x-dead-letter-routing-key用来控制队列内出现dead letter后进行重新路由。
优缺点:
高效的利用MQ本身的分布式特点,增加了扩展性和可靠性。
但是强依赖于MQ的运维,复杂度有一定的增高。
领取专属 10元无门槛券
私享最新 技术干货