背景
功能描述:會議開始前五分鐘通過消息提醒參會人員。
為什么用redis:其實算是一個前期調研的失誤,之前一直使用的是阿里的RocketMQ,后面公司把RocketMQ做了升級,升級到了5.x版本。但是官方并未提供對應的node版sdk。當然再使用舊版本或者其他消息隊列服務都是不現實的。只能通過一寫其他方案來實現。
預選方案:當時調研了三四種方案,比作了優缺點對比,最終綜合考量選擇了redis key的方式。
方案一:最簡單的利用
node-schedule
庫,去指定一個時間執行任務,該方案任務只存在內存之中,一旦服務重啟,之前的定時任務就會消失。這顯然是不合適的。方案二:把任務存到數據庫中,然后在代碼中輪詢去查庫,查詢到指定時間范圍的執行就去執行。但是該方案會有時間誤差,主要看輪詢頻率。
方案三:通過redis的key過期事件監聽實現定時任務,代碼中提前監聽好事件,當key過期時就會執行監聽函數。該方案既沒有明顯的時間誤差(具體特別小的誤差有沒有并未調研),也不用擔心服務重啟的問題。因為任務相當于是存在redis中的。
代碼實現
接下來我們就用redis key的方式來實現一下定時任務:
首先需要運維同事幫忙設置一下redis的配置項
notify-keyspace-event Ex
,這里設置成Ex即可,E表示鍵事件通知,x表示過期事件(默認情況下收不到通知)。想了解其他值可以自行擴展。-
在項目啟動后,創建redis監聽。
先創建一個工廠函數用于生成redis clientconst redis = require('redis'); async function initRedisClient(){ return await redis.createClient({ url: 'redis://xxxx' }); }
事件監聽:
async function subRedisKey(){ // 創建一個用于監聽事件的client let redisSubClient = initRedisClient() await redisSubClient.connect() // 訂閱數據庫0的key expired事件 redisSubClient.subscribe('__keyevent@0__:expired', (e) => { // e就是過期的key,去做自己的業務邏輯 } }
-
在創建會議的時候通過設置一個key,并設置其過期時間為會議開始前5分鐘即可。需要注意的是key種需要包含需要使用到的數據和唯一性標識,因為過期后就拿不到value中的數據了,所以不能存在value中。同時還要根據key知道對應的會議是哪個。
async function createMeeting(data){ // 創建一個用于創建key的client const redisClient = initRedisClient(); await redisClient.connect() // key自己定義,value可以隨意設置 redisClient.PSETEX(`redis_key_${data.id}__${data}`, 指定時間毫秒值, '') }
當然,如果會議開始時間發生了修改或者會議被刪除,也可以刪除對應的key來清除沒用的任務。
redisClient.DEL(key)
分布式問題解決
通過一頓敲代碼,功能算是實現了,但是還有一個重要的問題。就是在分布式情況下,會出現多次通知的情況。因為分布式是同時啟動了多個服務。然后redis key的過期事件相當于是廣播式的。多個服務監聽了事件,就會多個服務同時觸發事件監聽。就出現了多次通知的問題。
為了解決該問題,一般情況下就會想到利用鎖的性質。接下來嘗試查到的第一個方法:GETSET
。該方法第一次執行,因為之前沒有set過,所以會返回null。一但設置了之后再執行就會返回設置的值。
監聽函數中:
const value = await redisClient.GETSET('key', 1);
// 如果返回值是1就表示某一臺服務器已經執行過,直接退出。反之,執行通知操作。
if(value){
return;;
}
// todo 執行通知
測試之后發現,該方法并不行,兩臺服務GETSET得到的值都是null,依舊都會通知。接下來嘗試第二個方法INCR
。該方法會將key的值增加一,如果key不存在則會先初始化為0再增一。
監聽函數中:
const value = await redisClient.INCR('key');
// 如果返回值大于1就表示某一臺服務器已經執行過,直接退出。反之,執行通知操作。
if(value > 1){
return;;
}
// todo 執行通知
測試結果:該方法可行。任務完成!