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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容

  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 17,050评论 1 51
  • 本文精心整理了书籍、博客以及本人面试中遇到的基础知识点,方便大家快速回顾知识。 参考: https://githu...
    蛮三刀酱阅读 887评论 0 4
  • 使用缓存是系统性能优化的第一黄金法则。 缓存的设计和使用对一个系统的性能至关重要,平时接触到项目无论多少也都会在某...
    刀刃丿阅读 1,341评论 0 6
  • 安全性 设置客户端连接后进行任何其他指令前需要使用的密码。 警告:因为redis 速度相当快,所以在一台比较好的服...
    OzanShareing阅读 1,720评论 1 7
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,182评论 0 3