
隊(duì)列是一種線性表,內(nèi)部的元素是有序的,具有先進(jìn)先出的特性。
延時(shí)隊(duì)列,顧名思義,它是一個(gè)隊(duì)列,但更重要的是具有延時(shí)的特性,與普通隊(duì)列的先進(jìn)先出不同,延時(shí)隊(duì)列可以指定隊(duì)列中的消息在某個(gè)時(shí)間點(diǎn)被消費(fèi)。
延時(shí)隊(duì)列的使用場(chǎng)景
訂單提交后一定時(shí)間內(nèi)未支付需要自動(dòng)取消。
接口調(diào)用失敗后階梯式的補(bǔ)償調(diào)用。
任務(wù)超時(shí)提醒。
預(yù)定會(huì)議提前十五分鐘通知與會(huì)人員參加會(huì)議。
延時(shí)隊(duì)列常用實(shí)現(xiàn)方式
java DelayQueue延時(shí)隊(duì)列
DelayQueue是無(wú)界的延時(shí)阻塞隊(duì)列,內(nèi)部是使用優(yōu)先級(jí)隊(duì)列PriorityQueue實(shí)現(xiàn)的,其是按時(shí)間來(lái)定優(yōu)先級(jí)的延時(shí)阻塞隊(duì)列,只有在延遲期滿時(shí)才能從隊(duì)列中提取元素,先過(guò)期的元素會(huì)在隊(duì)首,每次從隊(duì)列里取出來(lái)都是最先要過(guò)期的元素,當(dāng)執(zhí)行隊(duì)列take操作元素未過(guò)期時(shí)會(huì)阻塞當(dāng)前線程到元素過(guò)期為止;PriorityQueue是通過(guò)二叉小頂堆實(shí)現(xiàn), 其任意一個(gè)非葉子節(jié)點(diǎn)的權(quán)值,都不大于其左右子節(jié)點(diǎn)的權(quán)值。

示例
隊(duì)列中的元素必須實(shí)現(xiàn)Delayed接口
public class MeetingNotice implements Delayed {
private long noticeTime;
private long meetingId;
public MeetingNotice(long meetingId, long noticeTime, TimeUnit unit) {
this.name = name;
this.noticeTime = System.currentTimeMillis() + (noticeTime > 0 ? unit.toMillis(noticeTime) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
return noticeTime - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
MeetingNotice meetingNotice = (MeetingNotice) o;
long diff = this.time - meetingNotice.time;
if (diff <= 0) {
return -1;
} else {
return 1;
}
}
}
DelayQueue 僅適用于單機(jī)部署的應(yīng)用,對(duì)于分布式場(chǎng)景無(wú)法適用,同時(shí)也不適用于隊(duì)列元素量很大的場(chǎng)景,不支持持久化。
Redis key過(guò)期回調(diào)
redis key的過(guò)期事件是通過(guò)redis 2.8.0之后版本提供的訂閱發(fā)布功能(pub/sub)下發(fā)的,當(dāng)key過(guò)期后系統(tǒng)自動(dòng)Pub,應(yīng)用程序只需訂閱(sub)該事件即可。
實(shí)現(xiàn)步驟
-
修改redis.conf文件配置如下參數(shù)
notify-keyspace-events Ex -
客戶(hù)端訂閱
redis key過(guò)期后系統(tǒng)會(huì)publish 頻道(channel)
__keyevent@0__:expired其中__keyevent為固定前綴,@0表示db0,訂閱是可根據(jù)自己的dbindex進(jìn)行調(diào)整,:expired表示過(guò)期事件??蛻?hù)端可通過(guò)SUBSCRIBE或PSUBSCRIBE訂閱,如SUBSCRIBE __keyevent@0__:expired監(jiān)聽(tīng)db0的key過(guò)期事件。
示例
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
public RedisKeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String redisKey = message.toString();
log.info("監(jiān)聽(tīng)到key: {} 過(guò)期" , redisKey);
}
@Configuration
static class RedisKeyExpiredConfig {
/************注冊(cè)redis監(jiān)聽(tīng)bean************/
@Bean
RedisMessageListenerContainer listenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory);
return listenerContainer;
}
@Bean
KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
return new RedisKeyExpiredListener(listenerContainer);
}
}
}
存在的問(wèn)題
key的失效通知無(wú)法保證時(shí)效性。redis過(guò)期策略有一下三種:
| 策略 | 說(shuō)明 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|---|
| 定時(shí)刪除 | 在設(shè)置 Key 過(guò)期時(shí)間的同時(shí)創(chuàng)建定時(shí)器,讓定時(shí)器在 Key 過(guò)期時(shí)執(zhí)行刪除操作 | 保證過(guò)期數(shù)據(jù)能被及時(shí)刪除 | 耗 CPU,尤其當(dāng)存在大量非永久 Key 時(shí),對(duì) CPU 影響更嚴(yán)重 |
| 惰性刪除 | Key 過(guò)期時(shí)不主動(dòng)刪除,獲取數(shù)據(jù)時(shí)判斷該 Key 是否過(guò)期,如果過(guò)期直接刪除 | 對(duì) CPU 消耗小 | 耗內(nèi)存,如果數(shù)據(jù)過(guò)期但又沒(méi)有任何操作來(lái)獲取該數(shù)據(jù),哪怕數(shù)據(jù)已經(jīng)過(guò)期了,但該數(shù)據(jù)任會(huì)一直存在 |
| 定期刪除 | 每隔一段時(shí)間執(zhí)行一次刪除操作 | 不如定時(shí)刪除那么消耗 CPU,也不如惰性刪除那么占內(nèi)存 | 比定時(shí)刪除更消耗內(nèi)存,必惰性刪除更消耗 CPU |
默認(rèn)情況下,Redis 使用的是惰性刪除 + 定期刪除的策略;每隔一段時(shí)間(可通過(guò)hz參數(shù)設(shè)置每秒執(zhí)行的次數(shù)),Redis 會(huì)分別從各個(gè)庫(kù)隨機(jī)選取部分測(cè)試設(shè)置了過(guò)期時(shí)間的 Key,判斷它們是否過(guò)期,過(guò)期則刪除;如果 key 已過(guò)期,但沒(méi)有被定期刪除,由于惰性刪除策略,在下次請(qǐng)求獲取該數(shù)據(jù)時(shí)會(huì)將該數(shù)據(jù)刪除。
可通過(guò)如下方式提高時(shí)效性
- 將緩存數(shù)據(jù)與監(jiān)聽(tīng)過(guò)期key數(shù)據(jù)分離,例如把緩存數(shù)據(jù)存在 database0,把監(jiān)聽(tīng)數(shù)據(jù)存在 database1;讓進(jìn)行監(jiān)聽(tīng)的庫(kù)中 key 盡量少,如果不同業(yè)務(wù)的監(jiān)聽(tīng)超時(shí)時(shí)間差異較大,則考慮將不同業(yè)務(wù)的超時(shí)監(jiān)聽(tīng)數(shù)據(jù)存放到不同的數(shù)據(jù)庫(kù);
- 調(diào)整過(guò)期策略為定時(shí)刪除策略,但這樣CPU定時(shí)器的開(kāi)銷(xiāo)會(huì)增大。
Redis zset
redis zset 結(jié)構(gòu)是一個(gè)有序集合,每個(gè)元素都會(huì)關(guān)聯(lián)一個(gè) double 類(lèi)型的分?jǐn)?shù),通過(guò)分?jǐn)?shù)來(lái)為集合中的成員進(jìn)行從小到大的排序;有序集合的成員是唯一的,但分?jǐn)?shù)(score)卻可以重復(fù)。
實(shí)現(xiàn)思路
將任務(wù)id作為member,到期時(shí)間作為score存入到zset中,然后不斷輪詢(xún)獲取第一個(gè)元素,判斷其是否過(guò)期,過(guò)期后刪除并執(zhí)行任務(wù)即可。
@Slf4j
@Component
public class TestZsetDelayTask {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String REDIS_DELAY_TASK_KEY="test_delay_task";
@PostConstruct
public void consumerRedisDelayTask() throws InterruptedException {
ZSetOperations<String,String> zSetOperations = redisTemplate.opsForZSet();
while(true){
//獲取當(dāng)前時(shí)間內(nèi)的第一個(gè)任務(wù)
Long score = System.currentTimeMillis();
Set<String> tasks = zSetOperations.rangeByScore(REDIS_DELAY_TASK_KEY,0,score);
if(CollectionUtils.isEmpty(tasks)){
Thread.sleep(200);
}else{
//移除該任務(wù)
String task = (String) tasks.iterator().next();
if(zSetOperations.remove(REDIS_DELAY_TASK_KEY,task)>0){
log.info("任務(wù): {} 準(zhǔn)備執(zhí)行" , task);
}
}
}
}
}
也可以通過(guò)lua腳本將zrangebyscore和zrem操作變成原子操作,避免了多線程時(shí)同一個(gè)me mber多次zrem。
String luaScript = "local resultArray = redis.call('zrangebyscore', KEYS[1], 0, ARGV[1], 'limit' , 0, 1)\n" +
"if #resultArray > 0 then\n" +
" if redis.call('zrem', KEYS[1], resultArray[1]) > 0 then\n" +
" return resultArray[1]\n" +
" else\n" +
" return ''\n" +
" end\n" +
"else\n" +
" return ''\n" +
"end";
存在的問(wèn)題
- 存在大量的空輪詢(xún)不但占用了客戶(hù)端的 CPU,同時(shí)也占用了redis的資源,空輪詢(xún)的客戶(hù)端有過(guò)多,redis的慢查詢(xún)可能會(huì)顯著增多。設(shè)置sleep的時(shí)間過(guò)大也會(huì)出現(xiàn)時(shí)效性不及時(shí)問(wèn)題。
- 沒(méi)有重試和ack機(jī)制,客戶(hù)端異常時(shí),這條任務(wù)可能會(huì)丟失。
可以使用Redission的RDelayedQueue數(shù)據(jù)結(jié)構(gòu),其api類(lèi)似于java queue使用簡(jiǎn)單,可更方便的實(shí)現(xiàn)基于redis的延時(shí)隊(duì)列。感興趣的可自行了解,這里不再展開(kāi)。
RabbitMQ延時(shí)隊(duì)列
RabbitMQ本身沒(méi)有直接支持延遲隊(duì)列功能,但是可以通過(guò)ttl及dlx(Dead Letter Exchanges)特性模擬出延遲隊(duì)列的功能。
死信隊(duì)列
綁定在死信交換機(jī)上的隊(duì)列。RabbitMQ的Queue(隊(duì)列)可以配置兩個(gè)參數(shù)x-dead-letter-exchange(死信交換機(jī))和x-dead-letter-routing-key(指定routing-key發(fā)送,可選),當(dāng)消息在一個(gè)隊(duì)列中變成死信 (dead message) 之后,按照這兩個(gè)參數(shù)可以將消息重新路由到另一個(gè)DLX Exchange(死信交換機(jī)),讓消息重新被消費(fèi)。
隊(duì)列出現(xiàn)Dead Letter的情況有:
消息或者隊(duì)列的TTL過(guò)期
隊(duì)列達(dá)到最大長(zhǎng)度
消息被消費(fèi)端拒絕(basic.reject / basic.nack),并且requeue = false
RabbitMQ ttl
RabbitMQ可以對(duì)消息和隊(duì)列設(shè)置TTL,為隊(duì)列設(shè)置時(shí),隊(duì)列中所有消息都有相同的過(guò)期時(shí)間;對(duì)消息進(jìn)行單獨(dú)設(shè)置,每條消息過(guò)期時(shí)間可以不同;如果同時(shí)設(shè)置了隊(duì)列的ttl和消息的ttl以?xún)烧咧gTTL較小的那個(gè)數(shù)值為準(zhǔn)。消息超過(guò)設(shè)置的ttl值未被消費(fèi),將會(huì)變?yōu)樗佬?,消費(fèi)者將無(wú)法再收到該消息。
x-message-ttl 為隊(duì)列設(shè)置過(guò)期時(shí)間。
expiration 為消息設(shè)置過(guò)期時(shí)間。
RabbitMQ延時(shí)隊(duì)列示例
@Configuration
@Slf4j
public class RabbitMqDelayQueue {
/**
* 普通交換機(jī)
*/
private static final String NORMAL_EXECHANGE = "test-exchange";
/**
* 死信交換機(jī)名稱(chēng)
*/
private static final String DLX_EXCHANGE ="test-dlx-exchange";
/**
* 普通隊(duì)列名稱(chēng)
*/
private static final String NORMAL_QUEUE_NAME = "test-queue";
/**
* 死信隊(duì)列名稱(chēng)
*/
private static final String DLX_QUEUE_NAME ="test-dlx-queue";
private static final String ROUTING_KEY="test";
@Autowired
private RabbitTemplate rabbitTemplate;
/*************普通交換機(jī)隊(duì)列的聲明及綁定*************/
@Bean
public DirectExchange normalExchange(){
return new DirectExchange(NORMAL_EXECHANGE, true,false);
}
@Bean
public Queue normalQueue(){
//設(shè)置隊(duì)列過(guò)期時(shí)間
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl",30000);
//設(shè)置死信后重新路由的交換機(jī)
args.put("x-dead-letter-exchange", DLX_EXCHANGE);
args.put("x-dead-letter-routing-key",ROUTING_KEY);
return new Queue(NORMAL_QUEUE_NAME,true,false,false,args);
}
@Bean
public Binding normalBinding(){
return BindingBuilder.bind(normalQueue()).to(normalExchange()).with(ROUTING_KEY);
}
/*************死信交換機(jī)隊(duì)列的聲明及綁定*************/
@Bean
public DirectExchange dlxExchange(){
return new DirectExchange(DLX_EXCHANGE, true,false);
}
@Bean
public Queue dlxQueue(){
return new Queue(DLX_QUEUE_NAME,true);
}
@Bean
public Binding dlxBinding(){
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(ROUTING_KEY);
}
@RabbitListener(queues = DLX_QUEUE_NAME)
public void consumerDlxQueue(@Payload String message) {
log.info("消費(fèi)到死信消息:{}",message);
}
@PostConstruct
public void sendMessage(){
Message message = MessageBuilder.withBody("hello rabbitmq".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
//設(shè)置消息的過(guò)期時(shí)間(毫秒)
.setExpiration("20000")
.build();
rabbitTemplate.send(NORMAL_EXECHANGE,ROUTING_KEY,message);
log.info("發(fā)送mq消息成功");
}
}
存在的問(wèn)題
ttl消息按照入發(fā)送順序排列在隊(duì)列中,且rabbitMQ只會(huì)判斷隊(duì)列頭消息是否失效,失效后才會(huì)加入到死信隊(duì)列中,如果發(fā)送多個(gè)過(guò)期時(shí)間不一致的消息,有可能后面的消息已經(jīng)過(guò)期了,但隊(duì)列頭消息沒(méi)有過(guò)期,導(dǎo)致其他消息不能及時(shí)加入到死信隊(duì)列被消費(fèi)。
rabbitmq_delayed_message_exchange插件
針對(duì)上述的問(wèn)題,可以使用rabbitmq_delayed_message_exchang插件來(lái)解決。
安裝該插件后會(huì)生成新的Exchange類(lèi)型x-delayed-message,該類(lèi)型消息支持延遲投遞機(jī)制,接收到消息后并未立即將消息投遞至目標(biāo)隊(duì)列中,而是存儲(chǔ)在mnesia(一個(gè)分布式數(shù)據(jù)系統(tǒng))表中,檢測(cè)消息延遲時(shí)間(通過(guò)消息頭的x-delay指定),如達(dá)到可投遞時(shí)間時(shí)并將其通過(guò)x-delayed-type類(lèi)型標(biāo)記的交換機(jī)類(lèi)型投遞至目標(biāo)隊(duì)列。
插件的安裝
- 進(jìn)入https://www.rabbitmq.com/community-plugins.html 頁(yè)面找到rabbitmq_delayed_message_exchang并下載。
- 將下載的插件復(fù)制到rabbitmq的plugins目錄
- 執(zhí)行
rabbitmq-plugins enable rabbitmq_delayed_message_exchange啟用該插件。
使用示例
/**
* 延遲消息交換機(jī)
*/
public final static String DELAY_EXCHANGE = "test-delay-exchange";
/**
* 隊(duì)列
*/
public final static String DELAY_QUEUE = "test-delay-queue";
/**
* 路由Key
*/
public final static String DELAY_ROUTING_KEY = "test-delay-routingKey";
@Bean
public CustomExchange delayMessageExchange() {
//自定義交換機(jī),type必須為x-delayed-message,添加參數(shù)x-delayed-type=direct
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", false, false, args);
}
@Bean
public Queue delayMessageQueue() {
return new Queue(DELAY_QUEUE, true);
}
@Bean
public Binding bindingDelayExchangeAndQueue() {
return BindingBuilder.bind(delayMessageQueue()).to(delayMessageExchange()).with(DELAY_ROUTING_KEY).noargs();
}
@PostConstruct
public void sendDelayMessages(){
Message message1 = MessageBuilder.withBody("delay message1".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
//設(shè)置消息過(guò)期時(shí)間
message1.getMessageProperties().setDelay(20000);
rabbitTemplate.send(DELAY_EXCHANGE,DELAY_ROUTING_KEY,message1);
Message message2 = MessageBuilder.withBody("delay message2".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
//設(shè)置消息過(guò)期時(shí)間
message2.getMessageProperties().setDelay(15000);
rabbitTemplate.send(DELAY_EXCHANGE,DELAY_ROUTING_KEY,message2);
log.info("發(fā)送mq delay 消息成功");
}
@RabbitListener(queues = DELAY_QUEUE)
public void consumerDelayQueue(@Payload String message) {
log.info("消費(fèi)到延時(shí)消息:{}",message);
}
插件的局限
- 插件極限時(shí)間是 8byte 長(zhǎng)度 ms,大概 49天,如果你的延時(shí)時(shí)間很長(zhǎng),超過(guò)49天那么該消息將會(huì)立刻被投遞到隊(duì)列中,不會(huì)延時(shí)。
- 該插件的當(dāng)前設(shè)計(jì)并不真正適合包含大量延遲消息(例如數(shù)十萬(wàn)或數(shù)百萬(wàn))的場(chǎng)景。有關(guān)詳情,請(qǐng)參見(jiàn)#72
- 如果該插件被禁用那么插件上的延時(shí)消息將丟失(還未投遞到目標(biāo)隊(duì)列的)。
時(shí)間輪
時(shí)間輪的應(yīng)用廣泛,包括linux內(nèi)核的調(diào)度、zookeeper、netty、kafka、xxl-job、quartz等均有使用時(shí)間輪。
原理

圖中的圓盤(pán)可以看作是鐘表的刻度。比如一圈round長(zhǎng)度為24秒,刻度數(shù)為8,那么每一個(gè)刻度表示3秒。那么時(shí)間精度就是3秒。每個(gè)刻度為一個(gè)bucket(實(shí)際上就是TimerTaskList),TimerTaskList是環(huán)形雙向鏈表,在其中鏈表項(xiàng)TimeTaskEntry封裝了真正的定時(shí)任務(wù)TimerTask。TimerTaskList使用expiration字段記錄了整個(gè)TimerTaskList的超時(shí)時(shí)間。TimeTaskEntry中的expirationMs字段記錄了超時(shí)時(shí)間戳,timerTask字段指向了對(duì)應(yīng)的TimerTask任務(wù);根據(jù)每個(gè)TimerTaskEntry的過(guò)期時(shí)間和當(dāng)前時(shí)間輪的時(shí)間,選擇一個(gè)合適的bucket,把這個(gè)TimerTaskEntry對(duì)象放進(jìn)去;對(duì)于延遲超過(guò)時(shí)間輪所能表示的范圍有兩種處理方式,一是通過(guò)增加一個(gè)字段-輪數(shù),Netty 就是這樣實(shí)現(xiàn)的;二是多層時(shí)間輪,Kakfa 是這樣實(shí)現(xiàn)的。
下面介紹下kafka的多層時(shí)間輪,層數(shù)越高時(shí)間跨度越大。

每個(gè)使用到的TimerTaskList都會(huì)加入到DelayQueue中,DelayQueue會(huì)根據(jù)TimerTaskList對(duì)應(yīng)的超時(shí)時(shí)間expiration來(lái)排序,最短expiration的TimerTaskList會(huì)被排在DelayQueue的隊(duì)頭,通過(guò)一個(gè)線程獲取到DelayQueue中的超時(shí)的任務(wù)列表TimerTaskList之后,既可以根據(jù)TimerTaskList的expiration來(lái)推進(jìn)時(shí)間輪的時(shí)間,也可以就獲取到的TimerTaskList執(zhí)行相應(yīng)的操作,TimerTaskEntry該執(zhí)行過(guò)期操作的就執(zhí)行過(guò)期操作,該降級(jí)時(shí)間輪的就降級(jí)時(shí)間輪。
舉個(gè)例子
假設(shè)現(xiàn)在有一個(gè)任務(wù)在445ms后執(zhí)行,默認(rèn)情況下,各個(gè)層級(jí)的時(shí)間輪的時(shí)間格個(gè)數(shù)為20,第一層時(shí)間輪每一個(gè)時(shí)間格跨度為1ms,整個(gè)時(shí)間輪跨度為20ms,跨度不夠。第二層時(shí)間輪每一個(gè)時(shí)間格跨度為20ms,整個(gè)時(shí)間輪跨度為400ms,跨度依然不夠,第三層時(shí)間輪每一個(gè)時(shí)間格跨度為400ms,整個(gè)時(shí)間輪跨度為8000ms,現(xiàn)在跨度夠了,此任務(wù)就放在第三層時(shí)間輪的第一個(gè)時(shí)間格對(duì)應(yīng)的TimerTaskList,等待被執(zhí)行,此TimerTaskList到期時(shí)間是400ms,隨著時(shí)間的流逝,當(dāng)此TimerTaskList到期時(shí),距離該任務(wù)到期時(shí)間還有45ms,不能執(zhí)行該任務(wù),將重新提交到時(shí)間輪,此時(shí)第一層時(shí)間輪跨度依然不夠,不能執(zhí)行任務(wù),第二層時(shí)間輪時(shí)間格跨度為20,整個(gè)世間輪跨度為400,跨度足夠,放在第三個(gè)時(shí)間格等待執(zhí)行,如此往復(fù)幾次,高層時(shí)間輪最終會(huì)慢慢移動(dòng)到低層時(shí)間輪上,最終任務(wù)到期執(zhí)行。
與kafka時(shí)間輪相比,netty采用的是輪次來(lái)解決超過(guò)時(shí)間輪所能表示的范圍,通過(guò)固定的時(shí)間間隔tickDuration掃描,時(shí)候未到就等待來(lái)進(jìn)行時(shí)間輪的推動(dòng),會(huì)有空推進(jìn)的情況,例如 TickDuration 為1秒,此時(shí)就一個(gè)延遲350秒的任務(wù),那就是有349次無(wú)用的操作。
Netty時(shí)間輪使用示例
public class TimeWheelDealyQueue {
public static void main(String[] args) {
/**
* 參數(shù)依次為
* 1.ThreadFactory 自定義線程工廠,用于創(chuàng)建線程執(zhí)行TimerTask
* 2.tickDuration 間隔多久走到下一槽(相當(dāng)于時(shí)鐘走一格),值越小,時(shí)間輪精度越高
* 3.unit 定義tickDuration的時(shí)間單位
* 4.ticksPerWheel 一圈有多個(gè)槽
* 5.leakDetection 是否開(kāi)啟內(nèi)存泄漏檢測(cè)。
* 6. maxPendingTimeouts 最多待執(zhí)行的任務(wù)個(gè)數(shù)。0或負(fù)數(shù)表示無(wú)限制。
*/
Timer timer = new HashedWheelTimer(Executors.defaultThreadFactory(), 10, TimeUnit.MILLISECONDS, 10);
System.out.println("開(kāi)始添加任務(wù):" + System.currentTimeMillis());
//延遲任務(wù),5秒后執(zhí)行
timer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
System.out.println("任務(wù)開(kāi)始執(zhí)行:"+System.currentTimeMillis());
}
}, 5, TimeUnit.SECONDS);
}
}