延时任务怎么搞

前言

开发过程当中,每每有一些延迟任务的需求,好比:数据库

  • 订单15分钟未支付,自动取消订单。
  • 用户支付成功后,给用户发短信。

这种状况咱们通常采用延迟任务来实现。服务器

延迟任务和定时任务什么区别呢?运维

  • 定时任务是在一个指定时间执行,延迟任务通常是在上一次任务执行完成后在同一个延迟间隔执行。

方案

数据库轮询分布式

能够经过一个线程定时扫描数据库,根据时间执行update或insert操做。 能够采用quartz实现:性能

<dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.2.2</version>
 </dependency>

Demo:ui

public class MyJob implements Job {
    public void execute(JobExecutionContext context)
            throws JobExecutionException {
        System.out.println("要去数据库扫描啦。。。");
    }

    public static void main(String[] args) throws Exception {
        // 建立任务
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("job1", "group1").build();
        // 建立触发器 每3秒钟执行一次
        Trigger trigger = TriggerBuilder
                .newTrigger()
                .withIdentity("trigger1", "group3")
                .withSchedule(
                        SimpleScheduleBuilder.simpleSchedule()
                                .withIntervalInSeconds(3).repeatForever())
                .build();
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        // 将任务及其触发器放入调度器
        scheduler.scheduleJob(jobDetail, trigger);
        // 调度器开始调度任务
        scheduler.start();
    }
}

这样程序会每隔3秒,输出:要去数据库扫描啦。。。this

优势:.net

  • 简单,支持集群操做。

缺点:线程

  • 消耗内存。
  • 若是数据量大,每扫描一次db性能存在损失。

JDK延迟队列指针

采用JDK自带的DelayQueue实现,是一个无界阻塞队列,只有在延迟时间知足时才在队列获取元素,流程以下:

producer->生产一个任务->放入delayedQueue->经过poll()/take()方法获取一个任务->交给消费者

  • poll:获取并移除队列的超时元素,没有则返回空;
  • take:获取并移除队列的超时元素,若是没有,线程阻塞,直到知足条件返回结果;

Demo:

public class OrderDelay implements Delayed {
    
    private String orderId;
    private long timeout;

    OrderDelay(String orderId, long timeout) {
        this.orderId = orderId;
        this.timeout = timeout + System.nanoTime();
    }

    public int compareTo(Delayed other) {
        if (other == this)
            return 0;
        OrderDelay t = (OrderDelay) other;
        long d = (getDelay(TimeUnit.NANOSECONDS) - t
                .getDelay(TimeUnit.NANOSECONDS));
        return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
    }

    // 返回距离你自定义的超时时间还有多少
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
    }

    void print() {
        System.out.println(orderId+"编号的订单要删除啦。。。。");
    }
}

public class DelayQueueDemo {
     public static void main(String[] args) {  
            // TODO Auto-generated method stub  
            List<String> list = new ArrayList<String>();  
            list.add("00000001");  
            list.add("00000002");  
            list.add("00000003");  
            list.add("00000004");  
            list.add("00000005");  
            DelayQueue<OrderDelay> queue = new DelayQueue<OrderDelay>();  
            long start = System.currentTimeMillis();  
            for(int i = 0;i<5;i++){  
                //延迟三秒取出
                queue.put(new OrderDelay(list.get(i),  
                        TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));  
                    try {  
                         queue.take().print();  
                         System.out.println("After " +   
                                 (System.currentTimeMillis()-start) + " MilliSeconds");  
                } catch (InterruptedException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
            }  
        }  
    
}

输出:

00000001编号的订单要删除啦。。。。
After 3003 MilliSeconds
00000002编号的订单要删除啦。。。。
After 6006 MilliSeconds
00000003编号的订单要删除啦。。。。
After 9006 MilliSeconds
00000004编号的订单要删除啦。。。。
After 12008 MilliSeconds
00000005编号的订单要删除啦。。。。
After 15009 MilliSeconds

优缺点:

  • 效率高,低延迟
  • 服务器重启,数据丢失,集群扩展受限于单机/内存,易出现OOM异常,代码复杂度高

时间片轮训

原理:

相似于时钟顺时针执行,每一次移动算一个tick。时间片主要有三个属性:

  • ticksPerWheel:一圈的tick数
  • tickDuration:一个tick持续的时间
  • timeUnit:时间单位

好比现实中的时钟就是:ticksPerWheel = 60,tickDurateion = 1,timeUnit = s。

若是当前时针在1上,下一个任务要4秒后执行,这这个任务的线程回调放在5上。 若是要在20秒后执行,因为一圈有8个位置,若是20秒,指针须要转两圈的5上面。

实现: 利用Netty的HashedWheelTimer:

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.24.Final</version>
        </dependency>

Demo:

public class HashedWheelTimerTest {
    static class MyTimerTask implements TimerTask{
        boolean flag;
        public MyTimerTask(boolean flag){
            this.flag = flag;
        }
        public void run(Timeout timeout) throws Exception {
            // TODO Auto-generated method stub
             System.out.println("要去数据库删除订单了。。。。");
             this.flag =false;
        }
    }
    public static void main(String[] argv) {
        MyTimerTask timerTask = new MyTimerTask(true);
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
        int i = 1;
        while(timerTask.flag){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(i+"秒过去了");
            i++;
        }
    }
}

输出:

1秒过去了
2秒过去了
3秒过去了
4秒过去了
5秒过去了
要去数据库删除订单了。。。。
6秒过去了

优缺点:

  • 效率高,延迟低,代码复杂度比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的运维,复杂度有必定的增高。
相关文章
相关标签/搜索