延遲任務實現方式

背景

​ 請求超時機制如何實現?訂單回滾如何實現?心跳機制如何實現呢?如何實現在一段時間後觸發一些事件呢?問題是有了,如何更好的去實現的?

1.Thread.sleep

​ 當完成某些事之後,需要休眠一段時間再向下執行的時候,可以使用此類。一般在測試的時候會使用此方法,在生產環境中,暫時沒發現哪裏有使用過。

public class SleepTest {
    public static void main(String[] args) {
        System.out.println("第一次輸出");
        try {
            Thread.sleep(3000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第二次輸出");
    }
}

​ 如上,在第一次輸入完成之後,等待3秒後,在進行第二次輸出。此方法簡單,不做過多解釋。

2.Timer

​ 此類爲任務調度工具,任務可以安排爲一次性執行,也可以安排爲定期重複執行。對應於每個計時器對象的是一個單獨的後臺線程,用於按順序執行計時器的所有任務。這個類是線程安全的:多個線程可以共享一個計時器對象,而不需要外部同步。

修飾和類型 方法說明
void **cancel**()Terminates this timer, discarding any currently scheduled tasks.
終止此計時器,放棄任何當前計劃的任務
int **purge**()Removes all cancelled tasks from this timer’s task queue.
從此計時器的任務隊列中刪除所有已取消的任務。
void **schedule**(TimerTask task, Date time)Schedules the specified task for execution at the specified time.
計劃指定的任務以在指定的時間執行。
void **schedule**(TimerTask task, Date firstTime, long period)Schedules the specified task for repeated fixed-delay execution, beginning at the specified time.
從指定的時間開始,調度指定的任務以重複執行固定延遲
void **schedule**(TimerTask task, long delay)Schedules the specified task for execution after the specified delay.
計劃指定的任務以在指定的延遲後執行。
void **schedule**(TimerTask task, long delay, long period)Schedules the specified task for repeated fixed-delay execution, beginning after the specified delay.
調度指定的任務,以在指定的延遲之後開始重複執行固定延遲。
void **scheduleAtFixedRate**(TimerTask task, Date firstTime, long period)Schedules the specified task for repeated fixed-rate execution, beginning at the specified time.
從指定的時間開始計劃指定的任務以重複執行固定速率
void **scheduleAtFixedRate**(TimerTask task, long delay, long period)Schedules the specified task for repeated fixed-rate execution, beginning after the specified delay.
從指定的延遲後開始,調度指定的任務以重複執行固定速率

​ 更詳細說明: https://docs.oracle.com/javase/8/docs/api/java/util/Timer.html

使用示例:

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Created by likuo on 2020/6/6.
 */
public class TimerTaskTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("哈哈");
            }
        }, 0, 1000);  // 每隔1秒執行一次
        //--------------示例--------------------------------
        System.out.println("任務在5秒執行");
        Date date = new Date(new Date().getTime() + 5000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任務已經執行");
            }
        }, date);  // 5秒鐘後執行
        //---------------示例-------------------------------
        System.out.println("任務在5秒後,每隔1秒執行一次");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任務每隔一秒執行一次");
            }
        }, date,1000);  // 5秒鐘後,每隔一秒執行一次
    }
}

​ 由於Timer是單線程的,如果任務太多就會有排隊的情況,導致原本間隔很短的任務,需要等待很長時間才能執行。如下:

public static void main(String[] args) {
        Date date = new Date(new Date().getTime() + 2000);
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println( "我1秒一次,"+LocalTime.now());
            }
        }, date, 1000);  // 每隔1秒執行一次
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    // 休眠5秒
                    Thread.sleep(5000l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println( "我5秒一次,"+LocalTime.now());
            }
        }, date, 1000);  // 每隔1秒執行一次
    }

​ 其中有兩個任務,一個每個的執行間隔都是1秒,但是任務2特別耗費時間,執行時間就5秒。所以就會有如下輸出:

我1秒一次,20:10:24.040
我5秒一次,20:10:29.041
我1秒一次,20:10:29.041

原本任務1是一秒一次,但是由於任務2執行時間太久,導致任務1也需要等很久才能執行。

3.DelayQueue

​ 《java多線程編程實戰指南》中介紹了ArrayBlockingQueue,LinkedBlockingQueue和SynchronousQueue這三種隊列。DelayQueue和這三種類似,這個的特點是延遲隊列。也就是可以實現某些任務在一段時間後執行的。隊列中的元素,需要實現Delayed接口,實現getDelay方法和compareTo方法。getDelay返回任務剩餘時間。 compareTo方法定義了元素排序規則,注意,元素的排序規則影響了元素的獲取順序

使用示例:

public class DelayQueueTest {

    public static void main(String[] args) {
        // 創建延時隊列
        DelayQueue<Message> queue = new DelayQueue<Message>();
        // 添加延時消息,m1 延時3s
        Message m1 = new Message(1, "world", 3000);
        // 添加延時消息,m2 延時10s
        Message m2 = new Message(2, "hello", 10000);
        //將延時消息放到延時隊列中
        queue.offer(m2);
        queue.offer(m1);
        // 啓動消費線程 消費添加到延時隊列中的消息,前提是任務到了延期時間
        ExecutorService exec = Executors.newFixedThreadPool(1);
        exec.execute(new Consumer(queue));
        exec.shutdown();
    }
    static class Message implements Delayed {
        public long excuteTime;
        public int id;
        public String body;

        public Message(int id, String body, long delayTime) {
            this.excuteTime = TimeUnit.NANOSECONDS.convert(delayTime, TimeUnit.MILLISECONDS) + System.nanoTime();
            this.id = id;
            this.body = body;
        }

        // 返回剩餘時間
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.excuteTime - System.nanoTime(), TimeUnit.NANOSECONDS);
        }

        public int compareTo(Delayed delayed) {
            Message msg = (Message) delayed;
            return Integer.valueOf(this.id) > Integer.valueOf(msg.id) ? 1
                    : (Integer.valueOf(this.id) < Integer.valueOf(msg.id) ? -1 : 0);
        }
    }

   static class Consumer implements Runnable {
        // 延時隊列 ,消費者從其中獲取消息進行消費
        private DelayQueue<Message> queue;

        public Consumer(DelayQueue<Message> queue) {
            this.queue = queue;
        }

        public void run() {
            while (true) {
                try {
                    Message take = queue.take();
                    System.out.println("消費消息id:" + take.id + " 消息體:" + take.body);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.ScheduledExecutorService

​ 線程池的返回值ExecutorService簡介:

​ ExecutorService是Java提供的用於管理線程池的類。該類的兩個作用:控制線程數量和重用線程

  1. Executors.newCacheThreadPool():可緩存線程池,先查看池中有沒有以前建立的線程,如果有,就直接使用。如果沒有,就建一個新的線程加入池中,緩存型池子通常用於執行一些生存期很短的異步型任務
  2. Executors.newFixedThreadPool(int n):創建一個可重用固定個數的線程池,以共享的無界隊列方式來運行這些線程。
  3. Executors.newScheduledThreadPool(int n):創建一個定長線程池,支持定時及週期性任務執行
  4. Executors.newSingleThreadExecutor():創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

先列舉下常用的線程池,前兩個是比較常見的,newSingleThreadExecutor這個用的也不多,曾經有一個面試題說:如何讓線程有順序的執行,可以用這個線程池來實現。

newScheduledThreadPool是一個線程池。由於內部使用的是延遲隊列,所以提供了延遲隊列的功能。

ScheduledExecutorService 可以解決Timer單線程任務排隊的問題。示例:

  public static ScheduledExecutorService mScheduledExecutorService = Executors.newScheduledThreadPool(10);

    public static void main(String[] args) {
        mScheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println( "我1秒一次,"+ LocalTime.now());
            }
        }, 0,1, TimeUnit.SECONDS);

        mScheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(5000l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println( "我5秒一次,"+LocalTime.now());
            }
        }, 0,1, TimeUnit.SECONDS);
    }

​ 同樣是一個任務1秒一次,一個任務5秒一次。輸出:

我1秒一次,20:38:51.571
我1秒一次,20:38:52.462
我1秒一次,20:38:53.464
我1秒一次,20:38:54.464
我1秒一次,20:38:55.462
我1秒一次,20:38:56.463
我5秒一次,20:38:56.463
我1秒一次,20:38:57.464
我1秒一次,20:38:58.462
我1秒一次,20:38:59.462
我1秒一次,20:39:00.464
我1秒一次,20:39:01.463
我5秒一次,20:39:01.464

​ 此方法在進行服務之間的心跳檢測的時候極爲常見。比如nacos在進行續約時就有。在com.alibaba.nacos.client.naming.beat.BeatReactor可見。

	//創建任務線程池
 private ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setDaemon(true);
            thread.setName("com.alibaba.nacos.naming.beat.sender");
            return thread;
        }
    });
    //心跳時間
    private long clientBeatInterval = 5000L;
    private NamingProxy serverProxy;
    public final Map<String, BeatInfo> dom2Beat = new ConcurrentHashMap();

    public BeatReactor(NamingProxy serverProxy) {
        this.serverProxy = serverProxy;
        // 執行任務,進行心跳,告訴nacos,當前服務還是存活狀態
        this.executorService.scheduleAtFixedRate(new BeatReactor.BeatProcessor(), 0L, this.clientBeatInterval, TimeUnit.MILLISECONDS);
    }

5.quartz

​ quartz框架是用的較多的任務調度框架。應該都見過類似0/5 * * ? * *這樣的配置。比如:每隔一週需要做一些事情,每天凌晨做一些事情等等,即可在項目中進行配置。簡單使用示例:

   public static void main(String[] args) throws SchedulerException, InterruptedException {
		// 1、創建調度器Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 2、創建JobDetail實例,並與PrintWordsJob類綁定(Job執行內容)
        JobDetail jobDetail = JobBuilder.newJob(TaskDemo.class)
                .withIdentity("job1", "group1").build();
        // 3、構建Trigger實例,每隔1s執行一次
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
                .startNow()//立即生效
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)//每隔1s執行一次
                        .repeatForever()).build();//一直執行

        //4、執行
        scheduler.scheduleJob(jobDetail, trigger);
        System.out.println("--------scheduler start ! ------------");
        scheduler.start();

        //睡眠
        TimeUnit.MINUTES.sleep(1);
        scheduler.shutdown();
        System.out.println("--------scheduler shutdown ! ------------");
    }
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
 
public class TaskDemo implements Job {
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("執行定時任務");
    }
}

6.HashedWheelTimer

​ 如果簡單的服務之間的心跳機制,用上面的那些都可以的,但是如果dubbo服務之間調用的時候超時機制呢?當dubbo消費端調用服務端時,如果3秒還沒有返回,則進行超時異常處理。這種情況用jdk自帶的任務調度器可能不能更好的實現了。HashedWheelTimer是netty框架的一個工具類,主要是用來高效處理大量的定時任務的。這裏不做過多的解釋。

​ 詳細:https://www.cnblogs.com/zemliu/p/3928285.html

https://www.jianshu.com/p/db138d40c3c5

7.redis失效監聽

​ 場景:訂單30分鐘如果還沒有支付,則進行庫存回滾操作。

​ 在現在大部分互聯網公司都是會進行多機部署,那麼如果使用jvm自帶的定時器進行處理的話,一旦服務宕機,就會出現所有的定時器也就失效了。想想如果大量的訂單因爲服務宕機導致不能回滾,是多麼的可怕。

​ redis在2.8之後支持對Key過期通知。也就是說,如果key消失的時候會有一個推送。那麼就可以利用這個機制,設置key的value爲訂單id,超時時間爲30分鐘,在30分鐘的時候,會有一個推送。

7.1redis失效機制測試

​ 1.設置redis.conf或者redis.windows.conf的notify-keyspace-events值

​ 2.修改爲`notify-keyspace-events Ex

​ 3.啓動一個客戶端,執行 psubscribe __keyevent@0__:expired 在這裏插入圖片描述
​ 4再啓動一個客戶端,設置一個key,並且設置過期時間

在這裏插入圖片描述

​ 5.3秒鐘後會有通知到第一個客戶端。

在這裏插入圖片描述

7.2redis失效機制項目中使用

引入jar

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>2.2.13</version>
        </dependency>
@Configuration
public class RedisListenerConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}
@Service
public class RedisTest extends KeyExpirationEventMessageListener {
    @Autowired
    private RedisTemplate redisTemplate;


    public RedisTest(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        System.out.println(new String(message.getChannel())+":"+message.toString());
    }
}
@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

如上,三個類即可實現redis失效機制的使用。

8.rocketmq延遲消息

​ 雖然redis可以解決服務器宕機後jdk自帶的延遲任務失效問題。但是如果是redis宕機了呢?或者redis失效的監聽機制沒有收到呢?畢竟redis的消息消失了,就什麼都沒有了,日誌都沒有。訂單這種敏感的數據,如果出現問題,沒日誌查,那太難受了。

​ rocketmq提供了延遲消息,可以更好的來實現訂單超時回滾。

​ 阿里雲rocketMq定時消息和延遲消息說明文檔:https://help.aliyun.com/document_detail/43349.html?spm=a2c4g.11186623.6.552.533e30300FOeTX

https://help.aliyun.com/document_detail/29549.html?spm=a2c4g.11186623.6.599.909378b4KEfHZ3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章