为什么你应该在 MQ 里用多个消费者,而不是一个

有一段时间,产品提新需求,我们的第一反应是:这个回调能不能不加?

不是不想做,是这个 MQ 消费者已经改不动了。每次加一个新 type,都要走一遍:改代码、测试全部 case、发版。战战兢兢,生怕碰坏已有的逻辑。

到后来,我们真的不想在这个 MQ 上再加任何新回调了。

这不应该是正常状态。


两种消费行为的定义方式

方式一:消费行为由消费者决定

生产者只管发消息,消息里带一个 type 字段。消费者收到后,根据 type 判断怎么处理。

typescript 复制代码
// 生产者:只发消息,type 决定"这是什么事件"
rabbitTemplate.convertAndSend("member.exchange", "register", message);
rabbitTemplate.convertAndSend("member.exchange", "upgrade", message);
rabbitTemplate.convertAndSend("member.exchange", "expire", message);

// 消费者:一个消费者处理所有 type
@RabbitListener(queues = "member.queue")
public void handle(String message) {
    Event e = JSON.parseObject(message);
    switch (e.getType()) {
        case "register": handleRegister(e); break;
        case "upgrade": handleUpgrade(e); break;
        case "expire": handleExpire(e); break;
        // ... 更多 case
    }
}

这是最常见的设计。问题也很明显:每次加一个新 type,都要改消费者的代码

方式二:消费行为由生产者决定,通过路由分发

生产者发的每条消息,已经决定了它应该被谁处理。消费者只管自己关心的那类消息。

typescript 复制代码
// 生产者:发消息时已经决定了谁来处理
rabbitTemplate.convertAndSend("member.exchange", "register", message);
rabbitTemplate.convertAndSend("member.exchange", "upgrade", message);
rabbitTemplate.convertAndSend("member.exchange", "expire", message);

// 消费者 A:只处理 register
@RabbitListener(queues = "member.queue.register")
public void handleRegister(String message) {
    // 只处理 register 逻辑
}

// 消费者 B:只处理 upgrade
@RabbitListener(queues = "member.queue.upgrade")
public void handleUpgrade(String message) {
    // 只处理 upgrade 逻辑
}

这种方式下,新增一个 type 不需要动任何老代码,只需要新增一个消费者。


两种方式的本质区别

方式一的问题:消费者的代码会越来越臃肿

当 type 只有两三个的时候,一个消费者写在一起还挺方便。但当 type 变成七个、八个、十来个的时候,消费者就变成了一个巨大的 switch case 集合。

每次加新 type ,开发者都要:

  1. 在消费者的 switch 里加一个 case
  2. 写新的处理方法
  3. 测试所有已有的 case 确保不回归
  4. 发版上线

更糟糕的是,不同类型的消费逻辑相互影响。一个 type 的 bug 可能影响另一个 type 的稳定性。改一个 type 的代码,要承担影响所有 type 的风险。

方式二的核心思想:不同的消息类型,由不同的消费者处理。每个消费者只关心自己该做的事。

这个原则不只适用于 MQ 。任何"一个处理者需要响应多种不同请求"的场景,都可以考虑这种方式。


什么时候选哪种方式

方式一(单一消费者 + type 路由)适合:

  • type 数量少且稳定,预计不会超过 3 个
  • 所有 type 的处理逻辑都很简单,50 行以内
  • 业务逻辑高度相似,只是参数不同
  • 你希望所有消费逻辑集中在一起,方便看全局

方式二(多消费者)适合:

  • type 数量会增长
  • 不同 type 的处理逻辑差异大
  • 稳定性要求高:一个 type 的问题不应该影响其他 type
  • 需要独立扩展:某些类型的消息消费量大,需要单独扩容

没有绝对的好坏。只有适合不适合。


总结一下

回到开头那个需求。重构之后,新增一个回调,我只是加了一个新的消费者类,配了一条路由规则,测完直接上线。

没有回归测试。没有发版风险。没有动一行老代码。

这不是 MQ 的胜利,是设计上的胜利。谁该干什么,让它自己干。

相关推荐
焦糖玛奇朵婷1 小时前
健身房预约小程序开发、设计
java·大数据·服务器·前端·小程序
小新同学^O^1 小时前
简单学习 --> TCP协议
java·网络·tcp
阿星做前端2 小时前
重度 AI 编程用户的一天:我怎么把 Claude Code / Codex 工作流搬进浏览器工作台
前端·javascript·后端
月落归舟2 小时前
深入理解Java适配器模式,彻底搞懂设计思想
java·开发语言·适配器模式
Mr_pyx2 小时前
【LeetHOT100】二叉树的中序遍历——Java多解法详解
java·开发语言·深度优先
代码羊羊2 小时前
Rust 类型转换全方位通俗易懂指南(as、TryInto、强制转换、Transmute)
后端·rust
jay神2 小时前
基于SpringBoot的宠物生命周期信息管理系统
java·数据库·spring boot·后端·web开发·宠物·管理系统
星栈2 小时前
Rust 全栈一个 main.rs 搞定启动:migration + CQRS + 投影监听,部署只需一个二进制
后端·架构
Penge6662 小时前
一文理清 Mac/Linux 终端配置文件(.bash_profile, .bashrc, .zshrc)
后端