延时任务的解决方案
- 1.数据库轮询
- 2. JDK的延迟队列
- 3.netty时间轮算法
- 4.使用消息队列
1.数据库轮询
该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作
代码示例:
select * from '表名' where '时间字段' < '数据库存储的时间戳' and status = '未执行'
while(true){sql;Thread.sleep(1000);
}
1.优点:简单易行,支持集群操作
2.缺点:
- 对服务器内存消耗大
- 存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟
- 假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大
2. JDK的延迟队列
该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的
代码示例:
public class JdkDelayQueue {public static void main(String[] args) throws Exception{DelayQueue<MyDelayed> delayQueue = new DelayQueue();delayQueue.add(new MyDelayed("Andy",5, TimeUnit.SECONDS));delayQueue.add(new MyDelayed("Andy_1",10, TimeUnit.SECONDS));for (;;){// 没有队列就就会阻塞MyDelayed take = delayQueue.take();take.excutue();}}static class MyDelayed implements Delayed {private String name;private long startTime;public MyDelayed(String name, long delayTime, TimeUnit unit) {this.name = name;// 当前时间毫秒值long currentTime = System.currentTimeMillis();// 时间转换为毫秒值long convert = TimeUnit.MILLISECONDS.convert(delayTime, unit);// 任务真正的执行时间this.startTime = currentTime + convert;}@Overridepublic long getDelay(TimeUnit unit) {long currentTime = System.currentTimeMillis();if(startTime <= currentTime){return 0;}long nextTime = startTime - currentTime;return unit.convert(nextTime, TimeUnit.MICROSECONDS);}@Overridepublic int compareTo(Delayed o) {MyDelayed prx = (MyDelayed) o;MyDelayed next = this;if (prx.startTime < next.startTime) {return 1;} else if (prx.startTime > next.startTime){return -1;}return 0;}public void excutue(){System.out.println(name + "任务执行");}}
}
1.优点:效率高,任务触发时间延迟低
2.缺点:
- 服务器重启后,数据全部消失,怕宕机
- 集群扩展相当麻烦
- 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
- 代码复杂度较高
3.netty时间轮算法
时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick
这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈,位置是在2圈之后的5上面(20 % 8 + 1)
1.优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低
2.确定
- 服务器重启后,数据全部消失,怕宕机
- 集群扩展相当麻烦
- 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
4.使用消息队列
我们可以采用RabbitMQ的延时队列,RabbitMQ具有以下两个特性,可以实现延迟队列
- RabbitMQ可以针对Queue和Message设置 x-message-ttl,来控制消息的生存时间,如果超时,则消息变为dead letter
- RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了dead letter,则按照这两个参数重新路由
1.优点:高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
2.缺点:本身的易用度要依赖于RabbitMq的运维,因为要引用RabbitMq,所以复杂度和成本变高