本文利用Redis的发布/订阅机制,实现了一种将复杂的多步逻辑变成若干个独立模块异步执行的方法,并提供了示例。通过这种方式,可以提升用户体验,避免性能瓶颈。
问题
有时候一个用户操作,在后台可能需要进行很多处理工作,例如:用户提交一条记录,需要:保存提交数据到数据库、记录用户操作日志、根据积分规则生成积分、对数据和积分进行汇总记录、给相关的用户发消息等等。如果这些操作都是按逻辑顺序同步执行,显然需要一个很长的响应时间,而且难以进行性能优化,严重影响用户体验。
站在用户体验的角度,其实用户没有必要等待所有的处理都完成,只要保证提交的数据没问题,而且已经保存下来,就可以通知用户操作成功,也就是说很多处理都可以在后台异步执行。
思路
Redis是一个非常成熟的内存数据库,有很好的性能。Redis中支持发布/订阅机制,是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
以发布/订阅机制为基础,我们可以把一个复杂逻辑分成若干个独立的模块,模块与模块之间利用通道(channel)交换数据(而不是直接调用),模块之间变成异步调用,这样可以尽早给用户提供响应,而且每个模块都可以独立部署,避免性能瓶颈。
示例
假设我们要实现一个进行“加减乘除”运算的应用,但是每个运算的消耗都很大,所以要把每种运算独立成一个可单独运行的模块,它们用异步方式计算结果。例如:输入1+2*3-1/4得到结果2。(为了简单起见,我忽略了运算符的优先级,按照从左到右的顺序执行)
建立项目
建立一个nodejs项目
- 新建目录try-pubsub
- vscode打开目录
- 在命令行中执行
npm init
,按提示设置项目信息 - 如果需要,安装redis,
npm i redis --save
- 如果需要,启动redis,
redis-server &
- 目录下创建app.js,dispatcher.js文件;创建子目录ops,在ops目录中创建op.js,add.js,sub.js,mul.js,div.js文件。
调度器(dispatcher.js)
我的目标是按顺序每一次只执行输入算式中的一个步骤,调度器从左到右解析最先碰到的运算符,根据运算符将算式放入相应的通道。
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(`发布到:通道-完成`)
resolve()
})
} else {
let matched = formula.match(/([+\-*/])/)
if (!matched || !OPNAME[matched[1]]) reject("解析算式失败")
let op = OPNAME[matched[1]]
pub.publish(`通道-${op}`, formula, () => {
console.log(`发布到:通道-${op}`)
resolve()
})
}
pub.quit()
})
}
module.exports = dispatch
应用(app.js)
应用从命令行接收一个算式,交给调度器去执行,并等待执行结果。
if (process.argv.length < 3) {
console.log("请提供要计算的算式,例如:1+2*3-1/4")
process.exit()
}
/**
* 异步接受执行结果
*/
const redis = require("redis")
const client = redis.createClient()
client.on("message", function(channel, message) {
client.unsubscribe()
client.quit()
console.log(`任务结束:${channel} | ${message}`)
})
client.subscribe("通道-完成")
client.subscribe("通道-中断")
/**
* 启动任务
*/
let dispatch = require("./dispatcher")
let formula = process.argv[2]
dispatch(formula)
.then(() => {
console.log(`开始运算:${formula}`)
})
.catch(err => {
console.log("err", err)
})
在实际的业务中,应用内应该完成最小的业务逻辑,并立刻返回给用户。将剩下的逻辑交给调度器处理,完成后再通过WebSocket等机制通知用户。
运算(op.js)
因为加减乘除这4个运算的基本过程是一样的,所以抽象出公共的运算逻辑。运算是算式的计算步骤,每次完成一步运算,得出结果后,更新算式,然后交给调度器继续处理后续步骤。
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("通道-中断", "数据错误,计算终端")
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+)/)
运行
安装pm2
add.js,sub.js,mul.js,div.js是4个独立运行的应用,需要将它们都启动起来。为了方便管理,我引入了pm2。
npm i pm2 -g
pm2 ecosystem // 生成配置文件
修改生成的ecosystem.config.js文件,在apps:[]中添加4个文件:
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"
}
启动
启动运算模块
pm2 start
启动应用
node app 1+2*3-1/4
输出结果
发布到:通道-加法
开始运算:1+2*3-1/4
任务结束:通道-完成 | 2
总结
通过Redis的发布/订阅机制进行代码解藕是一种区别以往的编程模式,它虽然为解决性能瓶颈提供了一种思路,但是也给系统增加了复杂度,包括:部署问题,异常处理问题,数据一致性问题等等。不过,我认为,对于复杂业务这种模式利大于弊,不仅仅是解决性能瓶颈,而是它要求我们必须对每一块独立的逻辑考虑地更全面,实现地更强壮。