Files
file/演讲稿/260313.md
T
2026-03-12 00:01:10 +08:00

259 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 演讲稿:从集中式到分布式,为什么消息中间件成为关键基础设施
建议时长:约 1216 分钟。
使用方式:可以按页讲,也可以根据时间压缩第 4–6 页与第 10 页的展开部分。
---
## 封面页
大家好,今天我想汇报的主题是:**从集中式到分布式,为什么消息中间件会成为关键基础设施**。
我这次不会把重点放在某一个具体产品的接口细节上,比如 Kafka、RabbitMQ 或 RocketMQ 的命令怎么写。今天我更想回答三个问题。
第一,集中式系统为什么在早期很好用,但后面又会遇到瓶颈。
第二,系统拆成分布式之后,为什么会自然出现解耦、异步、削峰填谷、最终一致性、重试、幂等、顺序性和可靠投递这些问题。
第三,消息中间件为什么会在这个阶段被引入,它到底解决了什么,又把复杂度转移到了哪里。
---
## 第 1 页:集中式系统——单体架构与数据流
我们先看最传统的集中式系统,也就是单体系统。
它的典型结构是:用户请求先进入统一入口层,然后进入一个单体应用。这个单体应用内部虽然可能也划分了用户、商品、订单、库存、支付、通知这些模块,但它们本质上还是运行在同一个进程里。最后,这些模块再统一访问同一个数据库,处理结束后同步把结果返回给用户。
这种架构在早期通常非常高效。原因也很直接。
第一,开发和部署成本低。大家维护的是一个工程、一个进程、一个数据库,协作路径很短。
第二,调用发生在进程内,延迟低,定位问题也比较直接。
第三,数据集中在一个库里,本地事务和强一致性都更容易实现。
所以单体并不是“错误架构”。相反,在业务规模还不大、边界也还比较稳定的时候,它往往是最经济、最务实的起点。
---
## 第 2 页:集中式系统的缺点——为什么要转向分布式
但随着业务增长,集中式系统的优势会慢慢变成它的约束。
首先,**发布开始耦合**。哪怕你只是改一个很小的库存逻辑,也可能需要重新构建和发布整个系统。
其次,**扩容开始粗糙**。如果热点其实只在订单和支付链路上,你还是只能把整个单体一起扩容。
第三,**故障容易放大**。一个局部模块出现问题,可能把整个进程拖慢,最后影响全站。
第四,**数据库压力会集中**。当订单、库存、支付都共享一个核心库时,单库就会逐渐成为吞吐和可用性的瓶颈。
所以系统转向分布式,并不是因为单体“落后”,而是因为随着业务、流量和团队规模的上升,单体的边际成本越来越高。
换句话说,分布式不是为了“更先进”,而是为了获得三种能力:**按业务能力拆分、按热点独立扩容、按故障域做隔离**。
---
## 第 3 页:拆成分布式后——问题不是消失了,而是迁移了
当系统拆成分布式以后,我们确实会得到一些很明显的好处,比如独立部署、按需扩容、团队边界更清楚。
但是,这个时候复杂度并没有消失,它只是从“代码内部”转移到了“服务之间”。
在单体里,一个模块调用另一个模块,本质上是函数调用。
可是在分布式里,一个服务调用另一个服务,已经变成了跨网络的 RPC。
同样,在单体里,订单、支付、库存如果都在一个数据库里,本地事务就能把它们一起包住。
可是在分布式里,这些数据往往分散在不同服务、不同数据库里,于是跨服务协同就不再能靠一个本地事务解决。
这就是后面所有问题的来源。
也就是说,**解耦、异步、削峰填谷、最终一致性、重试、幂等、顺序性、可靠投递,并不是消息中间件发明出来的问题,而是分布式系统天然会遇到的问题。**
---
## 第 4 页:问题来源之一——解耦、异步、削峰填谷
先看前三个问题:解耦、异步和削峰填谷。
### 1. 解耦从哪里来?
如果支付成功以后,订单服务还要直接去调用积分服务、短信服务、物流服务,那么每增加一个下游,主服务就多一条同步依赖。
这样一来,订单或支付服务不仅要关心自己的业务,还要感知每个下游的接口、可用性和上线节奏。这就是强耦合。
消息中间件的做法是,把“直接调用”改成“发布事件”。
比如支付成功后,只发布一条 `PaymentSucceeded` 事件,下游谁需要谁订阅。这样发布者只负责表达业务事实,不再关心具体有几个消费者。
### 2. 异步从哪里来?
很多动作其实并不要求用户同步等待。比如发短信、发优惠券、写埋点、做分析,这些都不是主链路必须立刻完成的结果。
如果我们把这些动作都塞在同步请求里,用户的等待时间就会越来越长。
所以更合理的方式是:主链路只做关键事务,旁路任务通过 MQ 异步处理。这样用户更快拿到响应,系统也更轻。
### 3. 削峰填谷从哪里来?
大促、秒杀这类场景里,上游写入速度往往会在瞬间暴涨,而下游数据库和服务的处理能力是相对稳定的。
如果没有缓冲层,流量就会直接冲到下游,轻则超时,重则把下游打垮。
消息中间件在这里承担的是“缓冲区”的角色。上游先把消息写入队列,下游按照自己能承受的速率平滑消费。再配合限流、扩容、重试和死信治理,就能把高峰流量整形成可处理的流量。
---
## 第 5 页:问题来源之二——最终一致性、重试、幂等
接下来再看另外三个经常同时出现的问题:最终一致性、重试和幂等。
### 1. 最终一致性从哪里来?
在单体里,订单、支付、库存可以共享一个事务。
但拆成分布式以后,这些状态往往分别保存在不同服务的数据库里,就很难再依赖一个本地事务同时提交。
这个时候,很多系统会采用事件驱动的一致性方式。也就是说,先让一个核心服务把自己的本地事务做成功,再通过事件把变更传播给其他服务,逐步把整个系统收敛到一致状态。
这就是我们说的“最终一致性”。如果某个下游暂时失败,也可以通过补偿、重试或者 Saga 机制继续推进,而不是要求所有服务在同一时刻一步到位。
这里补充解释一下 Outbox。
在分布式系统里,一个很典型的问题叫做双写不一致。比如支付服务处理“支付成功”时,一方面要把自己的数据库状态改掉,另一方面又要把“支付成功”这个事件发到消息队列里。问题在于,这两个动作通常不在同一个原子事务里。
如果数据库已经提交成功了,但消息还没来得及发出去,服务就宕机了,那么下游系统就永远不知道这次支付成功,库存、通知、积分这些动作也就不会发生。
反过来,如果消息先发出去了,但数据库事务最后回滚了,下游又会收到一个其实不应该存在的事件。
Outbox 模式就是为了解决这个问题。它的核心思路是:不直接把消息发到 MQ,而是先把“业务数据变更”和“待发送事件”一起写进同一个本地数据库事务里。
也就是说,支付状态更新的同时,再往一张 Outbox 表里插入一条事件记录。只要本地事务提交成功,这两件事就一定同时成功;如果事务失败,这两件事就一起失败。
后面再由一个独立的 relay 程序,或者后台任务,去扫描 Outbox 表,把里面还没发送的事件可靠地投递到 MQ。等收到 Broker 的确认之后,再把这条 Outbox 记录标记为已发送。
所以 Outbox 的本质,就是把“数据库更新 + 发消息”这个难以保证原子性的过程,改造成“先本地原子落库,再异步可靠投递”。
它解决的是事务与消息之间的衔接问题,是很多事件驱动架构里非常关键的一层。
不过也要注意,Outbox 解决的是“不丢消息、不漏事件”的问题,并不自动解决重复发送。所以即使用了 Outbox,下游消费者依然要做幂等。
### 2. 重试从哪里来?
只要是分布式调用,就必须接受失败是常态。网络会抖动,Broker 会重启,消费者会超时,ACK 也可能丢失。
一旦某次处理“看起来失败”,系统通常就会重试或者重投。
所以重试不是一个偶发现象,而是分布式系统必须显式设计的机制。一个成熟的设计通常至少要有:重试次数上限、退避策略、重试队列和死信队列。
### 3. 幂等从哪里来?
只要存在重试,就必须接受“同一条消息可能被处理多次”。
这意味着,我们不能假设消息只会来一次。工程上更可靠的思路是:默认它可能重复,然后通过 messageId、业务唯一键、去重表、唯一约束或者 upsert 让重复处理的结果保持不变。
所以可以把这三者连起来理解:**因为服务拆分,才需要最终一致性;因为最终一致性里一定会失败,才需要重试;因为有重试,才必须做幂等。**
---
## 第 6 页:问题来源之三——顺序性、可靠投递
再往下看两个更偏系统语义的问题:顺序性和可靠投递。
### 1. 顺序性从哪里来?
很多业务对象天然有顺序要求。比如同一个订单,应该先“创建订单”,再“支付成功”,最后才“发货”。
但分布式系统里,消息会分区、消费者会并发、失败后还会重试,这些都会打乱原本看似自然的顺序。
所以顺序不是默认就存在的,而是必须被明确定义出来。
工程上通常不会追求全局顺序,而是追求**局部顺序**。比如按订单 ID、账户 ID 这样的业务 key 来定义顺序范围,然后让相同 key 的消息进入同一分区、同一队列,或者由单线程、Single Active Consumer 这类方式串行消费。
### 2. 可靠投递从哪里来?
可靠投递也不能只看 Broker 本身。因为消息可能在四个地方出问题:
发送到 Broker 之前,Broker 持久化之前,Broker 投递给消费者之后,以及消费者处理完成但还没确认之前。
因此真正的可靠投递是一个端到端问题。
它通常包含几段机制:生产端要有 confirm 或 acks,Broker 侧要有持久化和副本,消费端要在本地事务完成后再 ACK,跨数据库与发消息之间还要通过 Outbox 或类似模式弥合空档。
所以工程上更常见的目标,不是抽象地追求“绝对恰好一次”,而是**至少一次 + 幂等 + 有边界的顺序**。
---
## 第 7 页:引入消息中间件后的纵览架构与数据流
理解了这些问题以后,我们再看为什么消息中间件会被引入。
引入 MQ 之后,整个系统一般会形成两条链路。
第一条是**同步核心链路**。也就是用户请求进入网关,再进入订单、支付这类必须同步完成的核心服务。
第二条是**异步事件链路**。也就是核心服务本地事务成功后,把业务事件送入 MQ,再由库存、优惠券、通知、分析等下游独立消费。
这时候的典型数据流是:
第一,用户请求进入订单或支付服务。
第二,核心服务把本地业务数据和 Outbox 一起提交。
第三,事件被 relay 或 producer 投递到 MQ。
第四,下游服务按需扇出消费。
第五,各个消费者本地提交成功后再 ACK。
第六,如果失败,就进入 Retry 或 DLQ,后续再补偿或人工介入。
这就是为什么消息中间件会成为关键基础设施。它把服务之间原来那种直接、同步、脆弱的依赖,转成了带缓冲、可恢复、可扩展的事件协作。
---
## 第 8 页:为什么要引入 MQ——它到底解决了什么
如果要用一句更直接的话来总结 MQ 的作用,我会说:
**它不是消灭问题,而是把问题从隐性的同步耦合,变成显性的系统机制。**
没有 MQ 时,系统常常是点对点同步调用。新增一个下游,就新增一条依赖;主链路越拉越长,用户等待时间也越来越长;流量和故障会沿调用链不断传导。
引入 MQ 之后,系统获得了四种关键能力:
第一,发布订阅,解决了解耦问题。
第二,异步处理,让非关键任务离开主链路。
第三,队列缓冲,把瞬时高峰转换成平滑消费。
第四,配套 Outbox、ACK、重试和幂等之后,最终一致性和故障恢复开始变得可治理。
所以 MQ 的价值,并不只是“多了一个队列”,而是**重新定义了服务之间的协作方式**。
---
## 第 9 页:一个完整业务例子——支付成功后的事件流
这页我用一个支付成功的例子,把前面的概念串起来。
当支付服务确认支付成功时,它不会直接同步调用所有下游,而是先在本地事务里完成两件事:更新支付状态,同时写一条 Outbox 记录。
接着,relay 把这条 `PaymentSucceeded` 事件投递到 MQ。
随后,库存服务会消费这条事件,去扣减库存;通知、优惠券、分析服务也可以并行地消费同一条事件。
如果库存服务处理成功,它就在本地提交完成之后再 ACK。
如果库存服务失败,这条消息就进入 Retry 或 DLQ,等待后续重试或补偿。
如果因为网络原因,这条消息又被投递了一次,那么库存服务也不应该被重复扣减,这就需要通过 `orderId + eventId` 这样的幂等键来兜底。
所以在这一条业务链路里,解耦、异步、最终一致性、重试、幂等、顺序和可靠投递,其实都会同时出现。
这也是为什么消息中间件不是一个“点状技术”,而是整条业务链路的协作基础设施。
---
## 第 10 页:真正落地时,先想清楚的 4 件事
最后我想强调,落地时真正需要设计的,不只是 MQ 的集群怎么搭,而是下面这四件事。
第一,先划清边界。哪些动作必须同步完成,哪些动作可以异步收敛。
第二,先定义事件。事件应该表达业务事实,而不是把 RPC 简单换个名字包成消息。
第三,默认至少一次。也就是说,在设计上先接受重复,再用幂等和重试去消化它。
第四,把事件流运营起来。持续监控 backlog、lag、重试率、死信队列和超时情况。
很多系统并不是死在 Broker 上,而是死在三个误区上:
要么把 MQ 当成强一致事务的替代品;
要么无限重试,没有上限和告警;
要么只搭了消息中间件,却没有配套的监控、补偿和事件治理。
所以真正成熟的设计,不是“用了 MQ”,而是知道**哪些问题应该让 MQ 解决,哪些问题不应该强行交给 MQ**。
---
## 第 11 页:总结
最后我用一句话收束今天的汇报:**消息中间件不是为了让系统看起来更高级,而是为了把分布式系统里原本隐性的协作复杂度,变成一套可以设计、治理和监控的机制。**
我的汇报就到这里,谢谢大家,欢迎交流。
---