1.使用場景
在很多系統中,特別是電商系統常常存在需要執行延遲任務。例如一個待支付訂單,需要在30分鐘後自動關閉。雖然有很多方式可以實現,比如說Job等,這裡主要介紹利用Redis的新特性 keyspace notifications來實現。
2.基礎知識
重點!!! Redis 2.8.0版本開始支援 keyspace notifications。如果你的Redis版本太低,可以洗洗睡了……
如果你還不瞭解Redis的Pub/Sub,強烈建議你先閱讀該篇文章: Redis釋出與訂閱
接下來說說我們的主角:keyspace notifications
keyspace notifications預設是關閉狀態,開啟則需要修改redis.conf檔案或通過CONFIG SET來開啟或關閉該功能。這裡我們使用CONFIG SET來開啟:
$ redis-cli config set notify-keyspace-events Ex複製程式碼
這裡有人會問了, Ex 是什麼意思呢?這是notify-keyspace-events的引數,完整的引數列表看看下面的表格:
字元 | 傳送的通知 |
---|---|
K | 鍵空間通知,所有通知以 __keyspace@<db>__ 為字首 |
E | 鍵事件通知,所有通知以 __keyevent@<db>__ 為字首 |
g | DEL 、 EXPIRE 、 RENAME 等型別無關的通用命令的通知 |
$ | 字串命令的通知 |
l | 列表命令的通知 |
s | 集合命令的通知 |
h | 雜湊命令的通知 |
z | 有序集合命令的通知 |
x | 過期事件:每當有過期鍵被刪除時傳送 |
e | 驅逐(evict)事件:每當有鍵因為 maxmemory 政策而被刪除時傳送 |
A | 引數 g$lshzxe 的別名 |
可以看出,我們只開啟了鍵事件通知和過期事件。因為我們實現延時任務只需要這兩個就足夠了。話不多說,直接看程式碼。
3. 實現方案
一個延遲任務應該具備哪些屬性? 我覺得至少有以下屬性:
- 任務型別。(例如:關閉訂單)
- 任務ID。(例如:訂單ID)
- 任務延遲時間。(例如:30分鐘)
- 任務額外資料。(例如:訂單其他相關資料)
確定好後,我們可以繼續往下走。
3.1 註冊事件處理器
首先在工程啟動後,我們需要根據不同的事件註冊不同的處理器:
const _ = require('lodash')
// 任務處理器map
const handlers = {}
// 事件型別map
const events = {}
const registerEventHandler = (type, handler) => {
if (!type) {
throw new Error('type不能為空')
}
if (!_.isFunction(handler)) {
throw new Error('handler型別非function')
}
handlers[type] = handler
events[type] = true
}複製程式碼
3.2 建立延遲任務
const redis = require('redis')
const client = redis.createClient()
const eventKeyPrefix = 'custom_event_'// 任務列表
const jobs = {}
const addDelayEvent = (type, id, body = {}, delay = 10 * 60) => {
const key = `${eventKeyPrefix}${type}_${id}`
const jobKey = `${type}_${id}`
client.setex(key, delay, 'delay event', (err) => {
if (err) {
return console.log('新增延遲事件失敗:', err);
}
console.log('新增延遲事件成功');
jobs[jobKey] = body
})
}
複製程式碼
這裡比較關鍵的點就是client.setex(key, expired, value)這個方法,我們需要給key新增一個過期時間,那麼當key過期後redis才會發出一個過期事件。
3.3 訂閱過期事件
實現了前兩個步驟後,我們已經可以往redis裡寫入帶有過期時間的key了。接下來關鍵的就是訂閱過期事件並處理。
const redis = require('redis')
const sub = redis.createClient()
sub.on('pmessage', (pattern, channel, message) => {
// match key
const keyMatcher = new RegExp(`^${eventKeyPrefix}(${_.keys(events).join('|')})_(\\S+)$`)
const result = message.match(keyMatcher)
if (result) {
const type = result[1];
const id = result[2];
const handler = handlers[type]
console.log('訂閱訊息:type=%s, id=%s', type, id);
if (_.isFunction(handler)) {
const jobKey = `${type}_${id}`
if (jobs[jobKey]) {
handler(id, jobs[jobKey])
} else {
console.log('未找到延遲事件,type=%s,id=%s', type, id);
}
} else {
console.log('未找到事件處理器。type=%s', type)
}
}
})
// 訂閱頻道
sub.psubscribe('__key*__:expired')
複製程式碼
3.4 編寫Demo
最後我們寫一個Demo來驗證下我們的功能。
const eventManager = require('./utils/eventManager')
eventManager.registerEventHandler('closeorder', (id, body) => {
console.log('關閉訂單 id=%s, body=%o', id, body);
})
eventManager.addDelayEvent('closeorder', 1111, {name: 'test'}, 5)複製程式碼
Done!