通過(guò)Redis的pub/sub機(jī)制實(shí)現(xiàn)復(fù)雜業(yè)務(wù)的解耦

本文利用Redis的發(fā)布/訂閱機(jī)制,實(shí)現(xiàn)了一種將復(fù)雜的多步邏輯變成若干個(gè)獨(dú)立模塊異步執(zhí)行的方法,并提供了示例。通過(guò)這種方式,可以提升用戶體驗(yàn),避免性能瓶頸。

問(wèn)題

有時(shí)候一個(gè)用戶操作,在后臺(tái)可能需要進(jìn)行很多處理工作,例如:用戶提交一條記錄,需要:保存提交數(shù)據(jù)到數(shù)據(jù)庫(kù)、記錄用戶操作日志、根據(jù)積分規(guī)則生成積分、對(duì)數(shù)據(jù)和積分進(jìn)行匯總記錄、給相關(guān)的用戶發(fā)消息等等。如果這些操作都是按邏輯順序同步執(zhí)行,顯然需要一個(gè)很長(zhǎng)的響應(yīng)時(shí)間,而且難以進(jìn)行性能優(yōu)化,嚴(yán)重影響用戶體驗(yàn)。

站在用戶體驗(yàn)的角度,其實(shí)用戶沒(méi)有必要等待所有的處理都完成,只要保證提交的數(shù)據(jù)沒(méi)問(wèn)題,而且已經(jīng)保存下來(lái),就可以通知用戶操作成功,也就是說(shuō)很多處理都可以在后臺(tái)異步執(zhí)行。

思路

Redis是一個(gè)非常成熟的內(nèi)存數(shù)據(jù)庫(kù),有很好的性能。Redis中支持發(fā)布/訂閱機(jī)制,是一種消息通信模式:發(fā)送者(pub)發(fā)送消息,訂閱者(sub)接收消息。

image.png
image.png

以發(fā)布/訂閱機(jī)制為基礎(chǔ),我們可以把一個(gè)復(fù)雜邏輯分成若干個(gè)獨(dú)立的模塊,模塊與模塊之間利用通道(channel)交換數(shù)據(jù)(而不是直接調(diào)用),模塊之間變成異步調(diào)用,這樣可以盡早給用戶提供響應(yīng),而且每個(gè)模塊都可以獨(dú)立部署,避免性能瓶頸。

示例

假設(shè)我們要實(shí)現(xiàn)一個(gè)進(jìn)行“加減乘除”運(yùn)算的應(yīng)用,但是每個(gè)運(yùn)算的消耗都很大,所以要把每種運(yùn)算獨(dú)立成一個(gè)可單獨(dú)運(yùn)行的模塊,它們用異步方式計(jì)算結(jié)果。例如:輸入1+2*3-1/4得到結(jié)果2。(為了簡(jiǎn)單起見(jiàn),我忽略了運(yùn)算符的優(yōu)先級(jí),按照從左到右的順序執(zhí)行)

建立項(xiàng)目

建立一個(gè)nodejs項(xiàng)目

  • 新建目錄try-pubsub
  • vscode打開(kāi)目錄
  • 在命令行中執(zhí)行npm init,按提示設(shè)置項(xiàng)目信息
  • 如果需要,安裝redis,npm i redis --save
  • 如果需要,啟動(dòng)redis,redis-server &
  • 目錄下創(chuàng)建app.js,dispatcher.js文件;創(chuàng)建子目錄ops,在ops目錄中創(chuàng)建op.js,add.js,sub.js,mul.js,div.js文件。

調(diào)度器(dispatcher.js)

我的目標(biāo)是按順序每一次只執(zhí)行輸入算式中的一個(gè)步驟,調(diào)度器從左到右解析最先碰到的運(yùn)算符,根據(jù)運(yùn)算符將算式放入相應(yīng)的通道。

const redis = require("redis")
const pub = redis.createClient()

const OPNAME = { "+": "加法", "-": "減法", "*": "乘法", "/": "除法" }

function dispatch(formula) {
  return new Promise((resolve, reject) => {
    if (!isNaN(Number(formula))) {
      pub.publish(`通道-完成`, formula, () => {
        console.log(`發(fā)布到:通道-完成`)
        resolve()
      })
    } else {
      let matched = formula.match(/([+\-*/])/)
      if (!matched || !OPNAME[matched[1]]) reject("解析算式失敗")
      let op = OPNAME[matched[1]]
      pub.publish(`通道-${op}`, formula, () => {
        console.log(`發(fā)布到:通道-${op}`)
        resolve()
      })
    }
    pub.quit()
  })
}

module.exports = dispatch

應(yīng)用(app.js)

應(yīng)用從命令行接收一個(gè)算式,交給調(diào)度器去執(zhí)行,并等待執(zhí)行結(jié)果。

if (process.argv.length < 3) {
  console.log("請(qǐng)?zhí)峁┮?jì)算的算式,例如:1+2*3-1/4")
  process.exit()
}
/**
 * 異步接受執(zhí)行結(jié)果
 */
const redis = require("redis")
const client = redis.createClient()
client.on("message", function(channel, message) {
  client.unsubscribe()
  client.quit()
  console.log(`任務(wù)結(jié)束:${channel} | ${message}`)
})
client.subscribe("通道-完成")
client.subscribe("通道-中斷")
/**
 * 啟動(dòng)任務(wù)
 */
let dispatch = require("./dispatcher")
let formula = process.argv[2]
dispatch(formula)
  .then(() => {
    console.log(`開(kāi)始運(yùn)算:${formula}`)
  })
  .catch(err => {
    console.log("err", err)
  })

在實(shí)際的業(yè)務(wù)中,應(yīng)用內(nèi)應(yīng)該完成最小的業(yè)務(wù)邏輯,并立刻返回給用戶。將剩下的邏輯交給調(diào)度器處理,完成后再通過(guò)WebSocket等機(jī)制通知用戶。

運(yùn)算(op.js)

因?yàn)榧訙p乘除這4個(gè)運(yùn)算的基本過(guò)程是一樣的,所以抽象出公共的運(yùn)算邏輯。運(yùn)算是算式的計(jì)算步驟,每次完成一步運(yùn)算,得出結(jié)果后,更新算式,然后交給調(diào)度器繼續(xù)處理后續(xù)步驟。

op.js

const redis = require("redis")

module.exports = function(name, operation) {
  const client = redis.createClient()
  client.on("subscribe", function(channel, count) {
    console.log(`訂閱通道:${channel}`)
  })

  client.on("message", function(channel, message) {
    console.log(`通道‘${channel}’收到消息 : ${message}`)
    if (message === "exit") {
      client.unsubscribe()
      client.quit()
      return
    }
    let step = message.match(operation)
    if (!step) {
      const pubClient = redis.createClient()
      pubClient.publish("通道-中斷", "數(shù)據(jù)錯(cuò)誤,計(jì)算終端")
      return
    }
    let result = message.replace(step[1], eval(step[1]))
    console.log(`完成${name}:${message} => ${result}`)

    let dispatch = require("../dispatcher")
    dispatch(result)
  })

  client.subscribe(`通道-${name}`)

  return client
}

add.js

require("./op")("加法", /^(\d+\+\d+)/)

sub.js

require("./op")("減法", /^(\d+\-\d+)/)

mul.js

require("./op")("乘法", /^(\d+\*\d+)/)

div.js

require("./op")("除法", /^(\d+\/\d+)/)

運(yùn)行

安裝pm2

add.js,sub.js,mul.js,div.js是4個(gè)獨(dú)立運(yùn)行的應(yīng)用,需要將它們都啟動(dòng)起來(lái)。為了方便管理,我引入了pm2。

npm i pm2 -g
pm2 ecosystem // 生成配置文件

修改生成的ecosystem.config.js文件,在apps:[]中添加4個(gè)文件:

name: "ADD",
script: "ops/add.js",
instances: 1,
autorestart: false,
watch: true,
ignore_watch: ["node_modules"],
max_memory_restart: "1G",
env: {
  NODE_ENV: "development"
},
env_production: {
  NODE_ENV: "production"
}

啟動(dòng)

啟動(dòng)運(yùn)算模塊

pm2 start

啟動(dòng)應(yīng)用

node app 1+2*3-1/4

輸出結(jié)果

發(fā)布到:通道-加法
開(kāi)始運(yùn)算:1+2*3-1/4
任務(wù)結(jié)束:通道-完成 | 2

總結(jié)

通過(guò)Redis的發(fā)布/訂閱機(jī)制進(jìn)行代碼解藕是一種區(qū)別以往的編程模式,它雖然為解決性能瓶頸提供了一種思路,但是也給系統(tǒng)增加了復(fù)雜度,包括:部署問(wèn)題,異常處理問(wèn)題,數(shù)據(jù)一致性問(wèn)題等等。不過(guò),我認(rèn)為,對(duì)于復(fù)雜業(yè)務(wù)這種模式利大于弊,不僅僅是解決性能瓶頸,而是它要求我們必須對(duì)每一塊獨(dú)立的邏輯考慮地更全面,實(shí)現(xiàn)地更強(qiáng)壯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容