分布式理论01:主要变迁
从单体应用走向分布式系统后,调用、事务、数据、时间和故障边界都会变化。本文作为分布式理论模块的开篇,先把这些变化讲清楚。
主要变迁是什么
分布式理论这个模块很容易被写成一篇”大杂烩”:CAP、BASE、分布式事务、分布式锁、幂等、共识协议、消息可靠性全都塞进来。这样看起来信息很多,但读完往往只剩一堆名词。
所以这篇只写一个知识点:主要变迁。
它要回答的问题是:一个 Java 后端系统从单体应用走向分布式架构时,到底发生了哪些根本变化?这些变化为什么会让我们不得不继续学习 CAP / BASE、分布式事务、幂等、分布式锁这些后续主题?
我的理解是,主要变迁不是机器变多,而是边界变了。以前很多问题被 JVM、本地方法调用、单库事务和同一个部署单元顺手兜住;服务拆开以后,这些默认保护消失了,系统开始依赖网络、协议、补偿、观测和工程约束。
五个核心边界变化
| 边界 | 单体时代 | 分布式时代 | 带来的新问题 |
|---|---|---|---|
| 调用边界 | 本地方法调用 | 远程协作 | 超时、重试、幂等、状态查询 |
| 事务边界 | 单库事务兜底 | 多个提交点 | 分布式事务、补偿、对账 |
| 数据边界 | 单库读写一致 | 多副本多延迟 | 最终一致、读写路径设计 |
| 时间边界 | 天然有先后顺序 | 时钟偏差、消息乱序 | 状态机、乐观锁、幂等键 |
| 故障边界 | 失败范围集中 | 故障沿链路传播 | 熔断、隔离、降级、链路追踪 |
下面逐个展开。
调用边界:方法调用变成远程协作
单体应用里,订单逻辑调用库存逻辑,大多就是一次本地方法调用:
orderService.createOrder();
inventoryService.lockStock();
失败时,异常在同一个进程里抛出来,调用栈也还在。你可以通过事务回滚、异常捕获和日志快速判断发生了什么。
拆成订单服务和库存服务后,这件事就变了。调用方看到的不再只是”业务成功或失败”,还可能是:
- 请求根本没发出去
- 请求发出去了,但响应超时
- 下游处理成功了,但响应在网络里丢了
- 网关、客户端或消息队列帮你重试了一次
这意味着调用结果不再天然确定。一个”超时”不等于失败,一个”失败响应”也不一定代表下游没有改状态。
在项目里,这个变化会逼着我们给远程调用补上四件事:超时、重试、幂等、状态查询。比如锁库存接口如果超时,调用方不能简单地认为库存没锁住,也不能无脑重试;它需要能通过业务单号查询结果,或者让库存服务保证重复请求不会重复扣减。
这就是后续要单独写”幂等性设计”的原因。
事务边界:一个提交点变成多个提交点
单体加单库时,很多业务靠数据库事务兜底:
@Transactional
public void submitOrder() {
saveOrder();
lockStock();
useCoupon();
}
只要这些操作都在同一个数据库连接里,要么一起提交,要么一起回滚。开发者真正要关心的是事务传播、隔离级别和锁冲突。
拆成多个服务后,订单库、库存库、优惠券库各自提交。订单服务里的 @Transactional 只能保护订单库,保护不了库存服务和优惠券服务。于是一个新问题出现了:如果订单保存成功、库存锁定成功、优惠券使用失败,系统应该怎么办?
这不是简单的技术选型题,而是业务后果题。
有些链路必须强约束,比如支付、余额、核心库存;有些链路可以事后补偿,比如积分、通知、搜索索引。前者可能需要更严肃的事务方案和对账机制,后者可以通过消息重试、补偿任务和状态修复来收敛。
这就是后续要单独写”分布式事务”的原因。它不是为了背 2PC、TCC、Saga 的定义,而是为了判断不同业务链路能接受哪种提交失败后的处理方式。
数据边界:一致性不再是默认值
单体应用里,很多一致性来自单库读写。你刚写入一条订单,马上从主库查询,通常能读到最新结果。
分布式系统里,数据会被拆到多个地方:
- 数据库有主从复制,读库可能延迟
- Redis 缓存和数据库可能短暂不一致
- 搜索索引通过异步任务构建,天然落后于主数据
- 消息消费者处理失败时,下游状态可能晚几分钟才追上
这时”一致”不再是一个默认结果,而是一个需要明确选择的目标。
项目里更实际的做法,是先给数据分层:
| 数据类型 | 一致性要求 | 延迟容忍度 | 兜底方式 |
|---|---|---|---|
| 支付状态、核心库存 | 强一致 | 不接受 | 对账 + 状态机 |
| 订单列表 | 读己之写 | 秒级 | 短期读主 |
| 积分、消息通知 | 最终一致 | 分钟级 | 重试 + 补偿 |
| 搜索索引、统计报表 | 最终一致 | 分钟到小时 | 重建索引、批处理校验 |
这些判断会影响缓存策略、读写路径、消息补偿和告警方式。后续再学 CAP / BASE 时,就能把它放回这个问题里看:当网络不稳定、节点不可达、数据复制延迟时,系统到底优先保证什么,又允许什么暂时不一致。
时间边界:顺序不再天然可靠
单机程序里,很多代码默认”先发生的事先被处理”。分布式系统里,这个假设经常不成立。
两台机器的时钟可能有偏差;两条消息可能因为分区、重试和消费者并发而乱序;同一个用户请求可能因为重复点击、网关重试或 MQ 重投被处理多次。
一个常见例子是支付回调。系统先收到”支付成功”,随后又收到一条延迟到达的”支付处理中”。如果代码只是按回调内容覆盖订单状态,就可能把已支付订单改回处理中。
更稳的方式是给状态流转加规则:
INIT -> PAYING -> PAID -> CLOSED
状态只能往合法方向流转,不能随便回退。重复消息来了,要能识别已经处理过;旧状态来了,要能拒绝覆盖新状态。
这就是分布式系统里”时间”和”顺序”带来的变化。后续写分布式锁、全局 ID、消息顺序、共识协议时,本质上都绕不开这个问题:没有一个天然可靠的全局顺序时,系统靠什么规则保持结果正确。
故障边界:失败会沿链路扩散
单体应用也会失败,但失败范围往往更集中。分布式系统里,一个下游慢,可能拖住一串上游。
比如下单链路里,订单服务同步调用库存、优惠券和支付预创建。某天支付服务依赖的第三方接口从 200ms 抖到 5s。订单服务线程被占住,连接池开始排队,网关还在继续放流量。最后用户看到的是”下单不可用”,而不是”支付服务变慢”。
这就是分布式架构里的典型变化:故障不再只停在发生点,它会沿调用链传播。
所以排查分布式问题时,不能只看单个服务有没有异常,还要看调用链:
- 哪一跳的 P95 / P99 先变坏
- 超时和重试有没有放大流量
- 线程池、连接池、队列是否已经饱和
- 熔断、限流、降级有没有保护核心链路
- traceId 能不能把一次请求串起来
这也是服务治理、链路追踪、限流熔断为什么会和分布式理论紧密相连。理论不是停在白板上,它会直接影响线上故障怎么止损。
常见误区
误区一:把分布式理解成”把服务拆小”
服务拆小只是形态变化,真正的成本是调用边界、事务边界、数据边界、时间边界和故障边界一起变化。
误区二:上了中间件就解决了理论问题
注册中心、配置中心、MQ、Redis、Seata 都只是工具。工具能帮你落地方案,但不能替你决定哪些数据必须一致、哪些操作需要幂等、哪些失败可以补偿。
误区三:把最终一致当成”不用管”
最终一致不是放任不一致,而是要有重试、补偿、对账、告警和人工处理入口。没有收敛机制的最终一致,本质上就是随机不一致。
项目里先问这几件事
当一个系统准备拆服务、接 MQ、做异步化或改造事务时,可以先用这几个问题过一遍:
- 这次改造改变了哪些边界:调用、事务、数据、时间还是故障?
- 远程调用超时后,调用方怎么确认最终状态?
- 同一个业务请求重复到达时,下游是否能幂等处理?
- 数据短暂不一致时,用户能不能接受,系统多久必须收敛?
- 下游变慢时,会不会拖垮上游核心线程池?
- 出现不一致后,靠什么重试、补偿、对账和告警发现?
这些问题比”用哪个框架”更早。框架是实现手段,边界才是设计对象。