有一段时间,产品提新需求,我们的第一反应是:这个回调能不能不加?
不是不想做,是这个 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 ,开发者都要:
- 在消费者的 switch 里加一个 case
- 写新的处理方法
- 测试所有已有的 case 确保不回归
- 发版上线
更糟糕的是,不同类型的消费逻辑相互影响。一个 type 的 bug 可能影响另一个 type 的稳定性。改一个 type 的代码,要承担影响所有 type 的风险。
方式二的核心思想:不同的消息类型,由不同的消费者处理。每个消费者只关心自己该做的事。
这个原则不只适用于 MQ 。任何"一个处理者需要响应多种不同请求"的场景,都可以考虑这种方式。
什么时候选哪种方式
方式一(单一消费者 + type 路由)适合:
- type 数量少且稳定,预计不会超过 3 个
- 所有 type 的处理逻辑都很简单,50 行以内
- 业务逻辑高度相似,只是参数不同
- 你希望所有消费逻辑集中在一起,方便看全局
方式二(多消费者)适合:
- type 数量会增长
- 不同 type 的处理逻辑差异大
- 稳定性要求高:一个 type 的问题不应该影响其他 type
- 需要独立扩展:某些类型的消息消费量大,需要单独扩容
没有绝对的好坏。只有适合不适合。
总结一下
回到开头那个需求。重构之后,新增一个回调,我只是加了一个新的消费者类,配了一条路由规则,测完直接上线。
没有回归测试。没有发版风险。没有动一行老代码。
这不是 MQ 的胜利,是设计上的胜利。谁该干什么,让它自己干。