通过Redis的pub/sub机制实现复杂业务的解耦

本文利用Redis的发布/订阅机制,实现了一种将复杂的多步逻辑变成若干个独立模块异步执行的方法,并提供了示例。通过这种方式,可以提升用户体验,避免性能瓶颈。

问题

有时候一个用户操作,在后台可能需要进行很多处理工作,例如:用户提交一条记录,需要:保存提交数据到数据库、记录用户操作日志、根据积分规则生成积分、对数据和积分进行汇总记录、给相关的用户发消息等等。如果这些操作都是按逻辑顺序同步执行,显然需要一个很长的响应时间,而且难以进行性能优化,严重影响用户体验。

站在用户体验的角度,其实用户没有必要等待所有的处理都完成,只要保证提交的数据没问题,而且已经保存下来,就可以通知用户操作成功,也就是说很多处理都可以在后台异步执行。

思路

Redis是一个非常成熟的内存数据库,有很好的性能。Redis中支持发布/订阅机制,是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

image.png
image.png

以发布/订阅机制为基础,我们可以把一个复杂逻辑分成若干个独立的模块,模块与模块之间利用通道(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的发布/订阅机制进行代码解藕是一种区别以往的编程模式,它虽然为解决性能瓶颈提供了一种思路,但是也给系统增加了复杂度,包括:部署问题,异常处理问题,数据一致性问题等等。不过,我认为,对于复杂业务这种模式利大于弊,不仅仅是解决性能瓶颈,而是它要求我们必须对每一块独立的逻辑考虑地更全面,实现地更强壮。

你可能感兴趣的:(通过Redis的pub/sub机制实现复杂业务的解耦)