基於redis實現定時任務

雨茗良記發表於2019-08-25

業務中碰到的需求(抽象描述一下):針對不同的使用者能夠實現不同時間的間隔迴圈任務。比如在使用者註冊成功24小時後給使用者推送相關簡訊等類似需求。

使用crontab?太重,且基本不現實,不可能給每一個使用者在伺服器上生成一個定時任務。

定時輪詢?IO頻繁且效率太低

想到經常的使用的redis可以設定快取時間,應該會有過期的事件通知吧,查了一下文件,果然有相關配置,叫做“鍵空間事件通知”。具體說明可參考官方文件

技術棧

redis / nodeJs / koa

技術重難點

  • 開啟redis的鍵空間通知功能(2.8.0及以上的版本才有此功能)
  • 儘量使用單獨的redis db來實現
  • 使用基於redis的分散式鎖來實現相關事件不會被重複消費
  • 需要二次使用的資訊需要體現在redis快取的key中
  • redis cache key使用業務字首,避免重名覆蓋
  • 防止業務服務重啟導致nodejs層面的監聽失效

"talk is cheap, show me the code ?"

核心程式碼
const { saveClient, subClient } = require('./db/redis') // 儲存例項和訂閱例項需要為兩個不同的例項
const processor = require('./service/task')
const config = require('./config/index')
const innerDistributedLockKey = '&&__&&' // 內部使用的分散式鎖的key的特徵值
const innerDistributedLockKeyReg = new RegExp(`^${innerDistributedLockKey}`)

saveClient.on('ready', async () => {
  saveClient.config('SET', 'notify-keyspace-events', 'Ex') // 儲存例項設定為推送鍵過期事件
  console.log('redis init success')
})

subClient.on('ready', () => { // 服務重啟後依舊可以初始化所有processor
  subClient.subscribe(`__keyevent@${config.redis.sub.db}__:expired`) // 訂閱例項負責訂閱訊息
  subClient.on('message', async (cahnnel, expiredKey) => {
    // 分散式鎖的key不做監聽處理
    if (expiredKey.match(innerDistributedLockKeyReg)) return
    // 簡易分散式鎖,拿到鎖的例項消費event
    const cackeKey = `${innerDistributedLockKey}-${expiredKey}`
    const lock = await saveClient.set(cackeKey, 2, 'ex', 5, 'nx') // 這裡的用法可以實現簡易的分散式鎖
    if (lock === 'OK') {
      await saveClient.del(cackeKey)
      for (let key in processor) {
        processor[key](expiredKey) // processor對應的是接收到相關鍵過期通知後執行的業務邏輯,比如推送簡訊,然後在相關processor中再次set一個定時過期的key
      }
    }
  })
  console.log('subClient init success')
})
複製程式碼
servide/task (processor)
exports.sendMessage = async function sendMessage(expiredKey, subClient) {
  // 只處理相關業務的過期事件
  if (expiredKey.match(/^send_message/)) {
    const [prefix, userId, type] = expiredKey.split('-')
    let user = getUser(userId)
    if (user.phone) {
      push(message) // 虛擬碼
      resetRedisKey(expiredKey, ttl) // 重新把key設定為一段時間後過期,過期後會再次觸發本邏輯
    }
  }
}
複製程式碼

總結

  • 此功能利用了redis的鍵空間通知功能實現了簡單了基於使用者或者基於不同業務場景的定時任務功能。由於鍵空間事件通知功能是一個較消耗CPU的操作,所以建議使用單獨的DB來處理。
  • 這裡展示出來的是基本用法,未考慮定時任務的持久化功能,如果使用過程中redis故障重啟,則會導致所有定時任務丟失。如果在redis釋出鍵失效通知時,訂閱服務出故障未線上,或者網路問題沒有被消費方收到,也會導致此次事件丟失
  • redis的expired事件並不是在key過期的時候觸發,而是在key被刪除的時候觸發。redis會定期清理過期的key,或者當訪問key的時候檢查是否過期,只有這時過期的key才會觸發刪除操作,因此會有一些小的時間差距(個人的實際使用中並沒有影響使用者體驗)。

因此需要權衡使用redis的過期機制實現的定時任務的使用場景。

感謝閱讀,轉載請註明出處。 喜歡的朋友可以關注我的公眾號:雨茗良記,每週會定期更新文章哦,包括但不限於技術。 我是雨茗良記,一個愛做飯的程式猿?

雨茗良記

相關文章