背景
請求超時機制如何實現?訂單回滾如何實現?心跳機制如何實現呢?如何實現在一段時間後觸發一些事件呢?問題是有了,如何更好的去實現的?
文章目錄
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提供的用於管理線程池的類。該類的兩個作用:控制線程數量和重用線程
- Executors.newCacheThreadPool():可緩存線程池,先查看池中有沒有以前建立的線程,如果有,就直接使用。如果沒有,就建一個新的線程加入池中,緩存型池子通常用於執行一些生存期很短的異步型任務
- Executors.newFixedThreadPool(int n):創建一個可重用固定個數的線程池,以共享的無界隊列方式來運行這些線程。
- Executors.newScheduledThreadPool(int n):創建一個定長線程池,支持定時及週期性任務執行
- 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