消息队列的核心作用是:异步、解耦、削峰。
这三个词看起来很简单,但如果只是记住定义,其实很难真正理解消息队列到底解决了什么问题。
本文不从 RabbitMQ、Kafka 或 RocketMQ 的具体 API 开始,而是先从一个普通的业务场景出发,看看为什么系统中需要消息队列。
1. 不使用消息队列会怎么样?
假设现在有一个商城系统,用户下单以后,需要执行下面几个操作:
创建订单
↓
扣减库存
↓
增加积分
↓
发送短信
↓
发送优惠券
如果全部采用同步调用:
def create_order():
save_order()
reduce_stock()
add_points()
send_sms()
send_coupon()
用户点击“提交订单”以后,必须等所有操作执行完成,才能得到响应。
假设每个操作耗时:
创建订单:100ms
扣减库存:100ms
增加积分:200ms
发送短信:500ms
发送优惠券:300ms
那么整个请求大约需要:
100 + 100 + 200 + 500 + 300
= 1200ms
但实际上,用户下单时真正关心的可能只有:
订单有没有创建成功?
库存有没有扣减成功?
至于积分、短信、优惠券,并不一定需要立即完成。
这时候就可以引入消息队列。
2. 异步:不用等所有事情做完
加入 MQ 后,可以把非核心业务发送到消息队列:
用户下单
↓
订单服务
├── 创建订单
├── 扣减库存
│
└── 发送消息 → MQ
↓
┌───────┼────────┐
↓ ↓ ↓
积分服务 短信服务 优惠券服务
订单服务只需要完成核心操作,然后向 MQ 发送一条消息:
order_created
之后就可以直接告诉用户:
下单成功
积分、短信、优惠券则由其他服务慢慢处理。
原本:
订单 → 库存 → 积分 → 短信 → 优惠券
现在变成:
订单 → 库存 → MQ → 返回结果
这就是消息队列的第一个作用:
异步
所谓异步,可以简单理解成:
这件事情我先通知你,你什么时候处理,不需要我一直等着。
因此 MQ 可以明显降低主流程的响应时间。
3. 解耦:订单服务不需要认识所有系统
继续看刚才的例子。
如果没有 MQ,订单服务可能需要直接调用:
积分服务
短信服务
优惠券服务
物流服务
推荐系统
数据分析系统
也就是说,订单服务需要知道:
积分服务在哪?
短信接口是什么?
优惠券接口是什么?
调用失败怎么办?
如果以后又增加一个“用户成长值系统”,订单服务还需要继续修改代码:
add_growth_value()
随着业务越来越多,订单服务和其他系统之间的依赖也会越来越复杂。
引入 MQ 后,订单服务只负责一件事:
告诉 MQ:订单创建成功了
例如发送:
{
"order_id": 10001,
"user_id": 9527,
"event": "ORDER_CREATED"
}
至于谁需要这个消息,订单服务并不关心。
订单服务
↓
MQ
↓
┌─┼───────────────┐
↓ ↓ ↓
积分服务 短信服务
↓
优惠券服务
以后新增一个数据分析系统,只需要让它订阅这个消息即可:
订单服务
↓
MQ
↓
数据分析系统
订单服务本身甚至不需要修改。
这就是:
解耦
也就是:
消息发送方只负责发送消息,不需要关心消息最终由谁处理。
这样不同系统之间的依赖就会减少。
4. 削峰:MQ 为什么能扛住突然的大量请求?
相比异步和解耦,削峰通常更容易让人困惑。
假设订单系统最多只能稳定处理:
1000 个请求 / 秒
平时访问量只有:
300 个请求 / 秒
完全没有问题。
但是到了秒杀活动开始的一瞬间:
10000 个请求 / 秒
突然全部进入订单系统。
就可能出现:
CPU 飙升
数据库连接池耗尽
大量请求超时
甚至服务直接崩溃
整个流量类似:
正常情况:
请求
■■■
■■■
■■■
秒杀开始:
■■■■■■■■■■■■■■■■■■■■
■■■■■■■■■■■■■■■■■■■■
■■■■■■■■■■■■■■■■■■■■
这种突然出现的巨大流量,就是所谓的:
流量峰值
5. MQ 是怎么把峰削掉的?
加入消息队列以后,请求不再全部直接进入订单系统。
而是:
大量请求
↓
MQ
↓
订单服务慢慢消费
假设瞬间来了:
10000 个请求
MQ 可以先把这些消息存下来。
订单服务的处理能力只有:
1000 条 / 秒
那么它就按照自己的能力消费:
第 1 秒:处理 1000
第 2 秒:处理 1000
第 3 秒:处理 1000
…
也就是说:
原来:
10000 请求
↓
订单服务
↓
瞬间压垮
变成:
10000 请求
↓
MQ
↓
1000/s
↓
订单服务
MQ 就像在请求和服务之间放了一个巨大的“蓄水池”。
6. 用水库理解削峰
其实可以把 MQ 想象成一个水库。
突然下暴雨:
水流量:10000 L/s
但下游河道只能承受:
1000 L/s
如果所有水直接进入下游:
暴雨
↓↓↓↓↓↓↓↓↓
河道
很容易发生洪水。
如果中间修一个水库:
暴雨
↓↓↓↓↓↓↓↓↓
██████████
水库
██████████
↓
1000 L/s
↓
下游
水库先把大量水暂时存起来,然后按照下游能够承受的速度慢慢放水。
MQ 的作用非常类似:
大量请求 = 暴雨
MQ = 水库
业务系统 = 下游河道
因此所谓削峰,并不是:
MQ 让业务系统突然变快了。
而是:
MQ 把瞬间的大量请求暂时存下来,让后端按照自己的处理能力逐渐消费。
这也是理解削峰最关键的一点。
7. 削峰其实是用时间换压力
比如:
10000 条消息
业务系统一秒只能处理:
1000 条
那么使用 MQ 后,大约需要:
10000 ÷ 1000 = 10 秒
才能全部处理完成。
MQ 并没有减少业务量。
原本要处理:
10000 个任务
使用 MQ 后依然需要处理:
10000 个任务
区别只是原来:
1 秒全部压过来
现在变成:
分 10 秒慢慢处理
因此,削峰本质上可以理解为:
把空间上的压力转换成时间上的等待。
8. 异步、解耦、削峰到底有什么区别?
最后再把三者放在一起看。
异步
解决:
主流程等待时间过长
核心思想:
先发送消息
不等待处理完成
例如:
下单成功
↓
MQ
↓
后台发送短信
解耦
解决:
系统之间依赖太强
原来:
订单服务 → 短信服务
订单服务 → 积分服务
订单服务 → 优惠券服务
现在:
订单服务
↓
MQ
↓
各个业务系统自己订阅
发送者和消费者不再直接依赖。
削峰
解决:
瞬时流量过大
核心思想:
大量请求
↓
MQ 暂存
↓
业务系统按照自己的速度处理
MQ 相当于流量缓冲区。
9. MQ 也不是没有代价
既然消息队列这么好,是不是所有业务都应该加 MQ?
当然不是。
引入 MQ 后,系统也会变得更加复杂。
例如你需要考虑:
消息发送失败怎么办?
消息丢失怎么办?
消息重复消费怎么办?
消费者处理失败怎么办?
消息堆积怎么办?
如何保证消息顺序?
原来的系统可能只是:
A → B
加入消息队列以后变成:
A → MQ → B
虽然解决了异步、解耦和削峰的问题,但同时也引入了新的问题。
因此 MQ 更适合:
非核心业务异步处理
多个系统订阅同一个业务事件
瞬时流量较大的场景
允许一定时间延迟的业务
而对于一些简单、强实时的小型业务,直接同步调用反而更加简单。
