延時任務怎麼搞

前言

開發過程當中,每每有一些延遲任務的需求,好比:數據庫

  • 訂單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的運維,複雜度有必定的增高。
相關文章
相關標籤/搜索