延时任务的解决方案

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;
        }

        @Override
        public long getDelay(TimeUnit unit) {
    
    
            long currentTime = System.currentTimeMillis();
            if(startTime <= currentTime){
    
    
                return 0;
            }
            long nextTime = startTime - currentTime;
            return unit.convert(nextTime, TimeUnit.MICROSECONDS);
        }

        @Override
        public 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,所以复杂度和成本变高

猜你喜欢

转载自blog.csdn.net/weixin_44702984/article/details/131631580
今日推荐