mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1988 words
5 minutes
MQ 从 0 到 1(一):为什么需要消息队列
2025-08-03

MQ 从 0 到 1(一):为什么需要消息队列#

MQ,全称 Message Queue,也就是消息队列。

它不是某一种具体技术,而是一类中间件思想:

生产者把消息发到队列里,消费者从队列里取消息处理。

听起来很简单,但它解决的是后端系统里非常常见的一组问题:

  • 一个请求里要做的事情太多,接口响应越来越慢;
  • 一个业务动作会牵扯多个系统,代码耦合越来越重;
  • 高峰期流量突然打进来,数据库和下游服务顶不住;
  • 跨系统操作不可能都放在同一个事务里,需要最终一致性;
  • 某些任务失败后需要重试,而不是直接丢掉。

这篇先不急着写 RabbitMQ 代码,先把 MQ 出现的原因讲清楚。

不用 MQ 的同步调用#

假设一个电商系统里,用户下单成功之后要做这些事:

1. 创建订单
2. 扣减库存
3. 生成支付单
4. 发短信通知
5. 发站内信
6. 增加用户积分
7. 推送数据到搜索系统
8. 记录运营分析数据

如果所有事情都写在一个接口里,大概会变成这样:

public void createOrder(CreateOrderCommand command) {
Order order = orderService.create(command);
stockService.deduct(order);
paymentService.createPayment(order);
smsService.sendOrderCreatedMessage(order);
messageService.sendSiteMessage(order);
pointService.addPoint(order);
searchService.syncOrder(order);
analysisService.record(order);
}

代码一眼能看懂,但问题也很明显。

问题一:接口响应慢#

用户真正关心的是:

我这个订单有没有创建成功?

但同步调用会让用户等所有步骤都完成。

假设每个步骤耗时如下:

操作耗时
创建订单50ms
扣减库存40ms
生成支付单30ms
发短信300ms
发站内信80ms
增加积分50ms
同步搜索系统200ms
记录分析数据60ms

总耗时可能接近 800ms。

如果短信服务偶尔慢一点,接口就会更慢。用户看到的就是一直转圈。

但短信、积分、搜索同步这些事情,并不一定要阻塞下单接口。

这时候就可以改成:

下单接口只做核心链路:
创建订单 -> 扣库存 -> 生成支付单
其他事情发消息:
发送 OrderCreatedEvent

消费者异步处理:

短信消费者:发送短信
积分消费者:增加积分
搜索消费者:同步搜索数据
分析消费者:记录运营数据

接口响应会变快,因为非核心动作被移到了异步链路。

问题二:系统耦合太重#

同步调用还有一个问题:订单服务知道得太多。

订单服务本来只应该关心订单,结果它还要知道:

  • 短信服务怎么调用;
  • 积分服务怎么调用;
  • 搜索服务怎么调用;
  • 运营分析服务怎么调用;
  • 新增一个优惠券服务时,还要改订单服务。

这会让订单服务越来越胖。

用了 MQ 之后,订单服务只需要做一件事:

订单创建成功后,发布一条订单创建消息。

至于谁关心这条消息,由消费者自己订阅。

OrderCreatedEvent
-> 短信服务消费
-> 积分服务消费
-> 搜索服务消费
-> 分析服务消费
-> 优惠券服务消费

订单服务不需要知道后面有多少消费者。

这就是解耦。

问题三:高峰流量打垮下游#

同步调用是“来一个请求,立刻处理一个请求”。

如果一场秒杀活动瞬间来了 10 万个请求,后端会直接把压力传给数据库和下游服务。

大量请求 -> 订单服务 -> 库存服务 -> 数据库

数据库顶不住时,系统就会雪崩。

MQ 可以作为缓冲层。

大量请求 -> 订单服务 -> MQ -> 消费者慢慢处理 -> 数据库

上游先把任务放进队列,下游按自己的处理能力慢慢消费。

这叫削峰填谷。

注意,MQ 不是让系统凭空变强,它只是把瞬时压力摊平。真正的处理能力还是取决于消费者数量、数据库能力、业务幂等和限流策略。

问题四:跨系统事务不好处理#

本地事务只能保证一个数据库里的操作一致。

比如下单时:

订单库:创建订单
库存库:扣库存
积分库:加积分
搜索系统:同步文档

这些操作不在一个数据库里,不能简单用一个 MySQL 事务包起来。

在真实项目里,更常见的做法是:

核心事务先成功。
然后发送业务事件。
消费者异步处理自己的数据。
如果失败,就重试或补偿。

这就是最终一致性的思路。

用户下单成功后,积分晚几秒到账,通常可以接受。

但订单创建成功、支付单没生成,这种核心链路就不能随便异步,要谨慎设计。

MQ 适合放什么#

适合放进 MQ 的事情,一般有几个特点:

  • 不需要立即给用户返回结果;
  • 失败后可以重试;
  • 能接受短暂延迟;
  • 可以设计幂等;
  • 适合被多个系统订阅。

比如:

场景是否适合 MQ原因
下单后发短信适合短暂延迟可接受
下单后加积分适合可以异步,失败可补偿
下单后同步 ES适合搜索数据可以最终一致
支付成功后修改订单状态谨慎核心状态,必须保证可靠
用户登录校验密码不适合必须立即返回结果
查询商品详情不适合是读请求,不是异步任务

MQ 不是万能药,不是所有操作都应该丢到队列里。

MQ 的核心角色#

不管是 RabbitMQ、RocketMQ 还是 Kafka,最基本的角色都差不多。

生产者 Producer
发送消息
消息中间件 Broker
保存消息、路由消息、投递消息
消费者 Consumer
接收消息并处理业务

用订单场景表示就是:

订单服务
发送 OrderCreatedEvent
|
v
MQ
|
+--> 短信消费者
+--> 积分消费者
+--> 搜索消费者
+--> 分析消费者

生产者只负责把消息发出去。

消费者只负责处理自己关心的消息。

Broker 负责在中间保存和分发消息。

消息一般长什么样#

消息本质上就是一份数据。

比如订单创建消息:

{
"eventId": "202607091200000001",
"eventType": "ORDER_CREATED",
"orderId": 10001,
"userId": 90001,
"occurredAt": "2026-07-09 12:00:00"
}

业务里不要只传一个字符串,也不要把整个大对象无脑塞进去。

建议消息里包含:

  • 全局唯一的消息 ID;
  • 事件类型;
  • 业务主键,比如 orderId
  • 必要的业务字段;
  • 事件发生时间;
  • 版本号,方便后面兼容升级。

很多时候,消息里只放 orderId 就够了,消费者再根据 orderId 查询自己需要的数据。

MQ 会带来什么新问题#

MQ 解决了一些问题,也会引入新的复杂度。

最常见的是:

  • 消息发送失败怎么办;
  • 消息发出去了但数据库事务回滚怎么办;
  • 消费者处理失败怎么办;
  • 同一条消息被消费多次怎么办;
  • 消息顺序是否需要保证;
  • 消息堆积了怎么办;
  • MQ 服务挂了怎么办;
  • 消费太慢怎么扩容;
  • 如何监控和告警。

所以 MQ 不是“加了就高级”,而是要配合可靠性、幂等、重试、死信队列、监控一起设计。

后面的文章会围绕这些问题展开。

MQ 和 @Async 的区别#

前面写过 @Async

@Async 和 MQ 都能做异步,但定位不一样。

对比@AsyncMQ
是否跨服务通常不跨服务天然适合跨服务
任务是否持久化默认不持久化通常可以持久化
失败重试需要自己做中间件和业务一起做
削峰能力
系统复杂度更高
适合场景本服务内部轻量异步跨系统事件、可靠异步、削峰

简单说:

本服务内部,轻量异步,可以先考虑 @Async。
跨服务、不能丢、要削峰,优先考虑 MQ。

小结#

这一篇先记住几个点:

  • MQ 的核心思想是生产者发消息,消费者异步消费;
  • MQ 常用来做异步、解耦、削峰和最终一致性;
  • MQ 适合处理可延迟、可重试、可幂等的任务;
  • MQ 会引入消息可靠性、重复消费、堆积、监控等新问题;
  • 不要为了用 MQ 而用 MQ,要先判断业务是否真的需要。

下一篇开始进入 RabbitMQ,看它是怎么用交换机、队列和绑定关系完成消息路由的。

下一篇:MQ 从 0 到 1(二):RabbitMQ 基础模型

Share

If this article helped you, please share it with others!

MQ 从 0 到 1(一):为什么需要消息队列
https://mizuki.mysqil.com/posts/message-queue-01-intro/
Author
梦幻晨风
Published at
2025-08-03
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents