本文利用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)接收消息。


以發(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)壯。