Node利用Redis實現定時任務

背景

功能描述:會議開始前五分鐘通過消息提醒參會人員。

為什么用redis:其實算是一個前期調研的失誤,之前一直使用的是阿里的RocketMQ,后面公司把RocketMQ做了升級,升級到了5.x版本。但是官方并未提供對應的node版sdk。當然再使用舊版本或者其他消息隊列服務都是不現實的。只能通過一寫其他方案來實現。

預選方案:當時調研了三四種方案,比作了優缺點對比,最終綜合考量選擇了redis key的方式。

  1. 方案一:最簡單的利用node-schedule庫,去指定一個時間執行任務,該方案任務只存在內存之中,一旦服務重啟,之前的定時任務就會消失。這顯然是不合適的。

  2. 方案二:把任務存到數據庫中,然后在代碼中輪詢去查庫,查詢到指定時間范圍的執行就去執行。但是該方案會有時間誤差,主要看輪詢頻率。

  3. 方案三:通過redis的key過期事件監聽實現定時任務,代碼中提前監聽好事件,當key過期時就會執行監聽函數。該方案既沒有明顯的時間誤差(具體特別小的誤差有沒有并未調研),也不用擔心服務重啟的問題。因為任務相當于是存在redis中的。

代碼實現

接下來我們就用redis key的方式來實現一下定時任務:

  1. 首先需要運維同事幫忙設置一下redis的配置項notify-keyspace-event Ex,這里設置成Ex即可,E表示鍵事件通知,x表示過期事件(默認情況下收不到通知)。想了解其他值可以自行擴展。

  2. 在項目啟動后,創建redis監聽。
    先創建一個工廠函數用于生成redis client

    const 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,去做自己的業務邏輯
        }
    }
    
  3. 在創建會議的時候通過設置一個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 執行通知

測試結果:該方法可行。任務完成!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容