为什么"完美架构"毁掉了项目?一个电商系统 7 个决策复盘

全文约 15000 字,以一个电商交易系统从 0 到规模化的完整演进为主线,现场复盘 7 个关键架构决策节点。
不聊概念焦虑,只聊约束、取舍和算账。每个结论都挂在具体决策上,可以直接迁移到你的项目里。


目录

  1. 开篇:一个"完美架构"如何毁掉一个项目
  2. 第1章 第一版架构:单体的克制
  3. 第2章 超卖事故:库存一致性
  4. 第3章 大促:削峰填谷与 MQ 的时机
  5. 第4章 支付对账:状态机、幂等与最终一致
  6. 第5章 拆服务:康威定律与拆的时机
  7. 第6章 大促事故:可用性不是玄学,是算账
  8. 第7章 复盘:回看决策树,为自己的决策付代价
  9. 终章 可迁移的决策资产

开篇:一个"完美架构"如何毁掉一个项目

2019 年,我认识的一个团队拿到了一个企业服务项目的订单。项目不大,但技术负责人是个完美主义者。他花了整整三个月设计架构:微服务拆了八个,消息队列、分布式事务、注册中心、配置中心、链路追踪、灰度发布平台,一应俱全。架构图打印出来贴在墙上,两米长。

结果呢?项目上线推迟了四个月,期间架构改了三版。客户等不及,把项目砍了。团队散伙那天,技术负责人说了句:“架构没设计好。”

不,架构设计得很好。问题恰恰在于它"太好了"——好到不匹配任何真实约束。

三个月里,团队只有 6 个人,没有人写过分布式事务;客户要的是 90 天内上线,不是 5 个 9 的可用性;业务逻辑总共不到 50 张表,却要跨 8 个服务调用。这不是架构设计,这是在真空中画蓝图

为什么很多架构师会犯这个错?因为我们的行业长期把"架构设计"等同于"画出漂亮的架构图"。图越复杂,显得越专业。但真实世界的架构师,工作根本不是画图,而是在约束下做决策:

  • 只有 3 个月,是上微服务还是上单体?
  • 库存不能超卖,是加锁还是预扣?
  • 大促流量是平时的 20 倍,是买机器还是削峰?
  • 支付回调丢了,是改第三方还是自己兜底?

每一个问题,都没有标准答案。答案取决于你的约束:时间、人力、资金、团队能力、业务阶段、可接受的风险。

所以这篇文章,我不想讲任何"架构原则"或"最佳实践"的空话。我想带你完整走一遍一个真实的电商交易系统从 0 到规模化的全过程——7 个关键决策节点,每个节点我都会告诉你:当时面临什么约束、生成了哪些候选方案、为什么这么选、后来被证明是对是错

读完你会带走三样东西:

  1. 一套决策框架:识别约束 → 明确目标 → 生成方案 → 权衡取舍 → 验证反馈
  2. 一组反模式:那些看起来专业、实则害人的做法
  3. 一张决策检查清单:下次做架构决策时逐条打勾

故事开始。


第1章 第一版架构:单体的克制

1.1 现场:5 个人,3 个月,先跑通第一单

2021 年春天,我加入了一家电商创业公司,做生鲜品类。老板老周,一个做过十年供应链的生意人,拍板要自建交易系统——因为他受不了第三方 SaaS 的抽成和定制限制。

团队 5 个人:两个后端(我和阿凯)、一个前端、一个测试、一个产品。技术栈从零开始。

老周给的第一句话是:“三个月,我要看到第一单走通。”

1.2 约束清单

开工前,我列了一张约束清单——不是技术约束,是所有约束:

约束类型 具体内容
时间 3 个月必须上线,没有回旋余地
人力 2 个后端,且其中 1 个是刚毕业一年
业务 生鲜品类,SKU 几千,下单、支付、库存、配送、售后
资金 自筹,服务器预算每月几千块
运维 没有专职运维,出问题我们自己扛
未知 流量完全未知,商业模式还没验证

注意最后一条。这是最容易被忽略、却最致命的约束:我们不知道这个业务能不能成。

1.3 方案生成:三个候选

摆在面前的有三个方向:

方案 A:微服务架构
订单服务、商品服务、库存服务、支付服务、用户服务……按 DDD 划分限界上下文,服务间走 RPC。这是当年技术圈的主流叙事,简历上写出来好看,面试官爱听。

方案 B:模块化单体
一个 Spring Boot 应用,按业务模块分包(订单、商品、库存、支付、用户),内部用 Service 接口解耦,数据库一张 MySQL 实例,分库不拆库。

方案 C:面条式单体
一个应用,一个包,一个 Service,写完拉到,上线再说。

1.4 取舍过程

我先排除了方案 C。面条式单体短期最快,但生鲜业务逻辑复杂(预售、拼团、优惠券、配送范围),结构一旦腐烂,后面每次改动都是地雷。

真正的纠结在 A 和 B 之间。

我当时的内心戏:

“用微服务,现在爽,后面会死。”

微服务的隐性成本,教科书不写:8 个服务意味着 8 套部署、8 套日志、8 套配置、8 套监控。2 个后端,光部署和联调就能吃掉一半时间。更要命的是分布式事务——下单要扣库存、减余额、建订单,跨服务事务怎么保证?要么上分布式事务框架(Seata),要么接受最终一致。这两条路,以我们 5 个人的团队,任何一条都是深渊。

“用单体,现在土,后面有机会翻盘。”

单体唯一的缺点——以后拆不动——其实是可逆的:只要我按模块划清边界、Service 接口解耦、数据库不搞跨模块的强耦合,将来拆服务只是物理搬家,不是逻辑重构。

反过来看微服务的缺点——不可逆:一旦拆分,服务边界、通信协议、数据归属就都定死了,改起来要动手术。

可逆 vs 不可逆,这是架构决策的第一把尺子。 信息越少、越不确定时,越要选可逆的方案——把不可逆的决策推迟到信息最充分的时候。

再算一笔账。3 个月上线,单体单实例单库,最快路径是 1 个月出 MVP,剩下 2 个月给业务和打磨。微服务光搭基础设施(注册中心、网关、链路追踪、CI/CD)就得 2 周以上,还要处理分布式事务这个无底洞。

决策:A + B 的合体——模块化单体,但按微服务的思维设计内部结构。

  • 一个 Spring Boot 应用,按模块分包:orderinventorypaymentuserproduct
  • 模块之间只通过 Service 接口交互,禁止跨模块直接操作对方的 Repository
  • 数据库:一个 MySQL 实例,业务表按模块前缀命名(ord_orderinv_stockpay_payment)
  • 预留:将来每个模块都可以独立成服务,只是现在是"逻辑上拆分、物理上合并"

第一版的模块结构长这样:

仅 Service 接口
禁止跨模块碰 Repository

Spring Boot 单体应用
(一个进程)

order 订单模块

inventory 库存模块

payment 支付模块

user 用户模块

product 商品模块

MySQL 单实例

注意两个关键约束:模块间只通过 Service 接口交互(虚线),数据库只有一个实例(所有模块共享)。这两个约束一个保住了"将来可拆",一个保住了"现在够简单"。

1.5 事后验证

6 个月后回头看,这个决策是全文 7 个决策里最值钱的一个

  • 1 个月出 MVP,第 3 个月如期上线,第一单走通
  • 模块边界后来成了第 5 章拆服务的地基——拆的时候是"搬家"而不是"重构"
  • 而同期的竞争对手,有 3 家上了微服务,2 家死在了分布式事务上,1 家活下来但技术债沉重

1.6 这一章的方法论

第一:识别约束,而且是识别所有约束。 大多数人只列技术约束,漏掉时间、人力、资金、组织、业务阶段。而决定架构方案的,往往是那些非技术约束。5 个人的团队和 50 个人的团队,面对同样业务,答案应该完全不同——如果相同,说明至少有一方做错了。

第二:可逆决策优先。 判断一个决策可逆不可逆,问自己:"如果这个决定错了,撤销的成本是什么?"单体→微服务,可逆;微服务→单体,几乎不可逆。所以信息不足时,选可逆的。把不可逆的决策留到信息最充分、不得不做的时候。

第三:方案 ≥2 个。 只有一个方案就拍板,那不叫决策,叫"没得选"。哪怕你心里已经有答案,也要逼自己写出第二个方案并列出代价——写不出代价的方案,说明你还没真正理解它。


第2章 超卖事故:库存一致性

2.1 现场:端午活动,库存 100,卖了 300

上线第 4 个月,端午活动。一款 99 元的榴莲做限时秒杀,库存 100 份。活动开始 3 分钟,前台显示售罄,后台一看:订单 300 单,库存 100。

超卖了。

更麻烦的是生鲜的赔付:客户收到货之前,我们得先道歉、退款、赔偿。老周的脸绿了:“今天这事,亏了 2 万,还砸了招牌。”

当晚查代码,问题清楚得让人脸红。下单逻辑是这样的:

// 伪代码:先查库存,再扣库存
int stock = stockMapper.selectStock(skuId);   // ① 查
if (stock > 0) {
    orderMapper.insert(order);                 // ② 建订单
    stockMapper.decrease(skuId);               // ③ 扣库存
}

两个请求同时读到 stock = 1,都认为还有货,都进了下单流程。经典的"先查后改"竞态。并发只有几十 QPS 就翻车了——超卖跟高并发没有必然关系,是逻辑本身的洞。

时间线上看,竞态是这样的:

MySQL(inv_stock) 用户B请求 用户A请求 MySQL(inv_stock) 用户B请求 用户A请求 SELECT stock → 读到 1 SELECT stock → 读到 1(同一时刻) INSERT 订单(成功) INSERT 订单(成功) UPDATE stock = 0 UPDATE stock = -1 ← 超卖!

两个请求都先"读到还有 1 件",然后各自建单、各自扣减——检查与扣减之间没有原子性,库存被扣成负数。修复方案见 2.4 节。

2.2 约束清单

约束类型 具体内容
业务 库存是钱:超卖 = 直接亏损 + 口碑崩塌,优先级最高
量级 峰值并发几十 QPS,普通活动,不是大促
团队 还是那 5 个人,没有中间件专家
一致性 必须"绝对不超卖",允许"库存扣了但订单没建成"(可补偿)
体验 秒杀场景下,用户点"立即购买"不能等太久

注意最后两条的微妙之处:"不超卖"是硬目标,"下单体验"是软目标——冲突时,牺牲体验也不能牺牲不超卖。这个优先级顺序,决定了后面所有方案的选择。

2.3 方案生成:三个候选

方案 A:数据库原子扣减(乐观)

UPDATE inv_stock
   SET stock = stock - #{n}
 WHERE sku_id = #{skuId} AND stock >= #{n};
-- 影响行数 = 1 才算成功,否则视为库存不足

把"查 + 改"合并成一条原子 SQL,利用行级锁天然串行化。扣减成功,才在同事务里建订单。

方案 B:悲观锁

SELECT stock FROM inv_stock WHERE sku_id = #{skuId} FOR UPDATE;

先锁行,再查、再改。完全避免竞态,但要持有行锁直到事务结束。

方案 C:Redis 预扣
库存预热进 Redis,用 Lua 脚本原子扣减,扣减成功再异步落库。库存操作从数据库搬到内存,抗高并发,大厂秒杀的标准打法。

2.4 取舍过程

方案 B 先被我划掉。FOR UPDATE 的锁要持有到事务提交,而生鲜下单事务里有支付预创建、优惠计算等耗时操作——锁越久,等待者越多,在高一点并发下会连锁放大成雪崩。悲观锁适合并发低、事务短、必须串行的场景,这里不匹配。

真正的纠结在 A 和 C。

方案 C 是"正确答案"吗?不,它是"高并发场景的正确答案"。而我们的场景是几十 QPS。把 Redis 引进来,等于用一头大象拉一辆自行车:

  • 引入一个新组件:部署、监控、故障处理全要自己扛
  • 引入一个新的不一致问题:Redis 和数据库的库存怎么对账?Redis 挂了怎么办?——为了一个几十 QPS 的场景,引入一个需要分布式锁和双写一致性的系统,这不是解决超卖,这是制造雪崩
  • 唯一收益(抗高并发)在当下用不上

架构决策的第二把尺子:方案没有好坏,只有"匹配当前约束"和"不匹配"。引入任何组件都要有触发条件,而不是因为它"先进"或"迟早要用"。

方案 A 呢?一行 WHERE stock >= n 的原子性,由 MySQL 行锁保证。几十 QPS 下毫无压力,不引入任何新组件,代码量 3 行。它唯一的短板——单库单机的行锁瓶颈——要等 QPS 上到几千才成立,而那时候,我们大概率已经有专职 DBA 和中间件团队了。

决策定了:方案 A,数据库原子扣减。

落地时还有三个细节:

  1. 事务边界:扣库存和建订单必须在同一个数据库事务里。库存扣减成功但订单插入失败 → 整个事务回滚,库存还原,不产生幽灵单。
  2. 失败即拒绝:扣减失败(影响行数 0)直接返回"库存不足",绝不重试抢购——重试会放大无效请求。
  3. 加唯一约束兜底:ord_order 表对 (user_id, sku_id, activity_id) 建唯一索引,防止同一用户在秒杀场景下重复提交导致的超量,这是最后一道保险。

2.5 事后验证

之后的 5 个月,大大小小 7 场活动,没有一次超卖。方案 A 用极低成本完成了任务。

但我知道它是有保质期的。年底的第一次大促,流量是平时的 20 倍,单库单机的行锁会变成新的瓶颈。那个问题,第 3 章和第 6 章会来找我们。

2.6 这一章的方法论

第一:先明确目标,再谈方案。 超卖事故里,如果目标被写成"提升下单性能",方案 C 就会胜出;把目标写清楚——“绝对不超卖,体验其次”——方案 A 才是答案。目标错了,方案全错。 很多架构争论,吵到最后发现是双方目标不一致。

第二:引入任何技术,都要有触发条件。 Redis、MQ、微服务、分布式事务……它们不是武器库里的勋章,是有使用成本的负债。什么时候该引入?写清楚触发阈值:“当单库 QPS 超过 X”、“当团队有 Y 个人专门维护”。阈值没到,再先进也忍住。

第三:事故是最好的验证,但更好的是把验证前置。 这次超卖靠线上事故发现,代价 2 万块。如果当时用 20 行脚本压测一下并发下单,这个洞 10 分钟就能发现。架构决策的质量,要靠验证,不能靠运气。


第3章 大促:削峰填谷与 MQ 的时机

3.1 现场:双十一来了,老板说"不能挂"

10 月,老周把我们叫进办公室:“今年双十一,目标 200 万单。我们第一次参加大促,别到时候挂了。”

200 万单是什么概念?平时日均 2 万单,这是 100 倍。一个 5 个人从零写出来的系统,要面对 100 倍流量——我当时后背就凉了。

但慌之前,先做一件事:量化。 没有数字的"大促预案"都是拍脑袋。

3.2 目标量化:先算账,再谈方案

峰值下单 QPS 的估算方法,就三个数乘起来:

峰值 QPS = (大促总单量 ÷ 集中售卖时长) × 高峰系数
  • 总单量:200 万
  • 集中售卖时长:大促活动 24 小时,但 90% 的单量集中在开场 8 小时
  • 高峰系数:开场瞬间的流量通常是均值的 3-4 倍
200 万 ÷ (8 × 3600 秒) × 4 ≈ 278 单/秒

这是下单入口的 QPS。但一个下单请求,后端要干一串活:查商品、算优惠、校验库存、扣库存、插订单、扣优惠券、写流水——大约 6 次数据库操作。所以数据库承受的真实压力是:

278 单/秒 × 6 次 = 约 1700 DB QPS(下单链路)

单库 MySQL 带索引的简单查询能扛 2000-3000 QPS。看似够,但别急——真正的杀手是热点行锁

双十一最大的爆品 iPhone,假设 200 万单里 30 万单打在同一个 SKU 上。第 2 章我们用的 UPDATE inv_stock ... WHERE stock >= n,靠的是这一行的行锁。30 万单串行抢同一行锁,每单持锁 5ms,光排队就是 1500 秒。单库扣减在这个场景下,物理上就不成立。

所以结论不是"要不要削峰",而是"必须削峰,且必须解决热点库存"。

3.3 约束清单

约束类型 具体内容
业务 大促期间系统不能挂,挂了 = 事故 + 退款潮
量级 峰值下单约 280 QPS,DB 链路约 1700 QPS,热点 SKU 单行 30 万单
一致性 不能超卖(第 2 章的底线不变);允许下单短暂"排队中"
团队 5 个人,没玩过 Kafka/RocketMQ,没专职运维
成本 服务器预算有限,不能无限加机器
时间 距离双十一 5 周

3.4 方案生成

方案 A:硬扛——加机器 + 读写分离
主库扛写,挂两个只读从库分担读流量,应用层水平扩容。不动架构。

方案 B:Redis 预扣 + 限流,不动主链路
只把第 2 章忍住的 Redis 拿出来,库存进 Redis 用 Lua 扣减,解决热点行锁;入口加限流,超阈值直接拒绝。下单链路还是同步的。

方案 C:MQ 削峰,主链路异步化
下单请求先过限流,然后直接发消息队列,立刻返回"订单创建中";消费端慢慢扣库存、建订单。把"洪峰"变成"排队",用时间换吞吐。

方案 D:A + B + C 全上
加机器 + Redis 预扣 + MQ 削峰 + 限流降级,一套组合拳。

3.5 取舍过程

方案 A 先淘汰。读写分离解决读瓶颈,但我们的瓶颈在(扣库存的行锁),加从库没用。加机器能缓解 CPU,缓解不了行锁排队。先分清瓶颈是读还是写,是 CPU 还是锁,再决定怎么扩。

方案 B 和 C 的核心分歧是:下单主链路能不能异步?

当时我算了一笔"异步的代价账":

  • 同步链路,用户 200ms 内看到"下单成功",逻辑简单,排查容易
  • 异步链路,用户先看到"订单创建中",得靠前端轮询;消息可能丢失(要处理可靠性)、可能重复消费(要幂等)、消费慢了会堆积(要监控);出了问题,链路跨了 HTTP、MQ、消费端三个环节,排查难度翻倍

异步化是用"复杂度 + 最终一致"换"吞吐 + 平滑"——这笔交易,值不值,取决于峰值是否真的打爆同步链路。

答案在第 3.2 节的账里:280 下单 QPS 同步扛得住,但热点 SKU 的 30 万单行锁,同步方案物理上无解。所以 Redis 预扣(B)不是可选项,是必选项;而 MQ 削峰©,280 QPS 其实用不上——真正要削的不是吞吐,是行锁热点

于是决策收敛为:B(必选)+ C(选择性地用)+ 限流:

  1. 热点库存 Redis 预扣(方案 B):库存按"是否爆品"分流。爆品 SKU 预热进 Redis,Lua 脚本原子扣减,扣减成功异步落库;普通 SKU 继续走数据库原子扣减。只对热点用 Redis,不为全体引入复杂度。
  2. MQ 只用在真正需要排队的地方:支付回调异步处理(第 4 章会讲到)、订单超时关单(延迟消息)、以及大促高峰期的下单排队降级——平时走同步,一旦 QPS 超过阈值(比如 300),自动切换为"先落 MQ 再异步建单"的排队模式。平时不用的复杂路径,大促才激活。
  3. 入口限流:令牌桶,应用层前置。超阈值直接返回"当前人数过多,请稍后再试",不让流量打到数据库。

MQ 选型上,团队没人用过 Kafka,但 RocketMQ 对国内开发者友好、自带事务消息和延迟消息、中文文档齐全,我选了 RocketMQ——选型标准是:团队上手成本 + 功能匹配度,不是"谁吞吐最高"。 Kafka 吞吐更高,但我们用不到,而它的运维复杂度我们承受不起。

大促期间的完整下单链路长这样:

QPS ≤ 阈值
走同步快速通道

QPS > 阈值
自动切换排队模式

爆品 SKU

普通 SKU

异步落库

用户点击下单
(峰值 ~300 QPS)

入口限流
(令牌桶,阈值可配)

同步建单
扣库存 → 建订单 → 返回

RocketMQ
下单消息队列

消费端
异步建单(限流消费)

库存扣减

Redis 预扣
Lua 原子扣减

MySQL 原子扣减
UPDATE ... WHERE stock >= n

三个关键点:限流器是"闸门",MQ 是"缓冲池",库存扣减按热点分流。平时只走上半条路(同步),只有峰值超过阈值才切到排队模式——复杂路径平时不生效,大促才激活。

3.6 事后验证

双十一当天,峰值下单 310 QPS,数据库 QPS 稳定在 1800 左右,没有一次超卖,系统没挂。削峰生效了。

但新问题也来了,而且都是异步化的"利息":

  • 消息堆积告警:某 SKU 的消费端处理慢,积压了 5 万条消息,前台大量用户卡在"订单创建中"
  • 重复消费:网络抖动导致消费端重试,同一个用户订单被创建了两次(幂等没做好)——这是第 4 章对账时才发现的
  • Redis 与 DB 库存不一致:某个普通 SKU 的库存被误判为"热点"预热进 Redis,活动结束后发现 Redis 剩 0、DB 还有 12,两边对不上

这三个问题,没有一个是可以"上线后再说"的。它们直接把第 4 章的题目推到了我面前。

3.7 这一章的方法论

第一:决策前先量化。 "大促不能挂"是口号,"峰值 280 QPS、热点行锁 30 万单"才是决策输入。架构讨论里,凡是拿不出数字的争论,基本都是情绪。QPS、延迟、成本、团队人力——先把数字摆上桌,方案自然浮现。

第二:触发条件兑现了,就果断上。 第 2 章我说"Redis 是过度设计,触发条件没到";这一章,热点行锁把触发条件坐实了,我就毫不犹豫地用。架构决策不是一劳永逸,是跟着约束滚动的。 忍住和果断,用的是同一把尺子,不是双重标准。

第三:异步化有显式账单。 消息可靠性、消费幂等、堆积监控、排错难度、最终一致——这些是异步化的"利息",决策时必须写进代价里,而不是上线后才发现。第 3 章我们付了这笔利息,第 4 章要把账算清楚。


第4章 支付对账:状态机、幂等与最终一致

4.1 现场:用户付了钱,订单卡在"待支付"

双十一过去一个月,财务张姐拿着一沓 Excel 找到我:“小李,这个月有 47 单,用户支付成功了,订单还挂在’待支付’。客户投诉都到我这来了。”

我心里咯噔一下。47 单,平均每天 1.5 单,不算多——但每单都是"用户付了钱,系统说没收到"的真金白银。

问题出在支付回调上。下单支付后,订单状态的推进完全依赖微信/支付宝的回调:

用户支付 → 支付平台异步回调 → 我们的服务更新订单状态为"已支付"

回调是异步网络消息:可能延迟(用户付完钱 5 分钟才回调)、可能重复(平台重试)、可能丢(我们服务重启/网络抖动,回调就没了)。丢了,订单就永远卡在"待支付"——没有兜底。

同一个月,对账还发现了另一类问题:双十一期间,有 23 个用户被重复创建了订单。同一个用户、同一款商品,订单表里躺着两条。原因就是第 3 章埋的雷:MQ 消费端网络抖动重试,消息被消费了两次,而我们的消费逻辑没有幂等。

两个问题,一个对外(用户付钱拿不到货),一个对内(库存、财务全乱)。都得治。

4.2 约束清单

约束类型 具体内容
业务 钱的事:不能多扣用户、不能少收自己、不能重复建单
一致性 支付状态与订单状态最终必须一致;中间允许短暂不一致
可靠性 回调会丢、会重、会乱序——不能假设外部系统可靠
合规 财务要求:每日对账,差异要能解释
团队 还是小团队,方案必须"短平快",不能上重型中间件

4.3 方案生成

方案 A:只靠回调,回调丢了自己手动补
现状。回调是唯一驱动,丢了靠张姐的人工 Excel 对账发现,再手动改状态。

方案 B:回调 + 主动查询兜底
保留回调,同时加一个定时任务:轮询"待支付"超时(如 15 分钟)的订单,主动调支付平台的订单查询接口,确认是否真的支付成功。查到了,就补更新状态。

方案 C:回调 + 主动查询 + 自动对账
在 B 的基础上,再加一个对账任务:每天拉取支付平台的对账单(文件/接口),与本地支付流水逐笔核对,差异自动告警并触发补偿。

外加一个贯穿三者的基础改造:订单状态机重构全链路幂等

4.4 取舍过程

方案 A 肯定不行——把可靠性建立在"不丢消息"上,而消息一定会丢。系统设计的第一原则:永远假设外部系统不可靠,然后设计兜底。

方案 B 解决了"实时性"问题:用户付了钱,最多 15 分钟,订单状态就能被主动查询修正过来。但它依赖一个假设:查询接口可靠、对账单和我方流水总能对上。现实是,两边数据口径、时区、退款场景、部分退款场景,人工都容易看花眼——对账不能靠人。

方案 C 是完整答案。对账任务不是"补回调",而是独立于业务链路的第三方裁判:支付平台说"这笔钱收了",本地说"这笔订单是待支付",差异就摆在那,跑不掉。它把"靠运气发现"变成"每天自动发现"。

于是方案定了:C + 状态机 + 幂等,三件事一起做。

第一件事:订单状态机,把隐式逻辑显式化。

重构前,订单状态是散落在代码里的 if/else,想改成什么状态改什么状态——这就是 47 单卡死的温床。重构后,状态迁移被画成一张图:

支付回调 / 主动查单

用户取消

超时未支付(30分钟)

发货

用户申请退款

签收

退货退款

待支付

已支付

已取消

已关闭

已发货

退款中

已完成

已退款

每一条箭头都是一个"合法迁移",箭头之外的状态跳转一律拒绝——状态机把"想改就改"变成"按图迁移"。

用状态机约束每一次迁移:

// 非法迁移直接拒绝,不靠人肉保证
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of(
    PENDING_PAYMENT, Set.of(PAID, CANCELLED, CLOSED),
    PAID,            Set.of(SHIPPED, REFUNDING),
    SHIPPED,         Set.of(COMPLETED, REFUNDING),
    ...
);

void transit(Order order, OrderStatus target) {
    if (!TRANSITIONS.get(order.getStatus()).contains(target)) {
        throw new IllegalStateException("非法状态迁移: " + order.getStatus() + " -> " + target);
    }
    order.setStatus(target);
}

状态机是"契约"——一旦发布,对外(客服、财务、下游系统)就依赖它。这是典型的不可逆决策:定义的时候要格外谨慎,把每种迁移的触发条件、失败路径、补偿路径都列清楚。但定义好了,它就是长期资产,后面所有逻辑都在这个骨架上长。

第二件事:全链路幂等。

回调处理必须幂等:同一个支付回调,处理 100 次和 1 次,结果一样。

-- 支付流水表:pay_payment 对第三方支付单号建唯一索引
CREATE UNIQUE INDEX uk_txn_id ON pay_payment (txn_id);

-- 处理回调时:先查重,再插入
INSERT INTO pay_payment (txn_id, order_id, amount, status)
VALUES (#{txnId}, #{orderId}, #{amount}, 'SUCCESS')
ON DUPLICATE KEY UPDATE status = 'SUCCESS';  -- 重复回调:幂等更新,不重复入账

MQ 消费端同样幂等:消费前查"消息处理记录表",处理过就跳过。

// 消费订单创建消息
if (msgProcessedDAO.exists(msgId)) {
    return; // 已处理,直接确认
}
msgProcessedDAO.mark(msgId);        // 先标记(唯一索引防并发)
createOrder(...);                   // 再处理业务

幂等的本质:给每个操作一个全局唯一的"业务 ID",存储层用唯一约束兜底。 没有幂等,异步化、重试、回调,每一样都会变成重复数据制造机。

第三件事:对账任务。

每日 02:00,任务拉取支付平台前一日对账单(文件),与本地 pay_payment 逐笔核对:

  • 对账单有、本地没有 → 单边账(钱收了,单没建/没更新)→ 告警 + 自动按单号补单
  • 本地有、对账单没有 → 支付失败/未回调,标记异常人工核查
  • 金额不一致 → 高优先级告警

三件事合起来,就是"最终一致"的正确姿势——三层兜底,各管一摊:

第三层:对账 · 管真相

第二层:主动查询 · 管延迟

第一层:事件驱动 · 管常态

发现差异

支付平台回调
(幂等处理,秒级生效)

定时查单任务
(每15分钟扫'待支付'超时单)
主动调支付平台查询接口

每日对账任务
(对账单 vs 本地流水)
差异告警 + 自动补偿

订单状态 → 已支付

三层各司其职,任何一层挂了,另外两层还能兜住。单靠回调一层,就是把自己吊在悬崖上。

4.5 事后验证

三个月后,再没有一单"用户付了钱订单卡死"。主动查单把修正延迟从"张姐发现"缩短到 15 分钟内;对账任务上线第一个月,自动发现并修复了 3 类历史差异(退款未同步、渠道单边账、汇率差异)。

幂等改造的效果立竿见影:重复订单从每月 23 单降到 0。张姐从"天天对 Excel"变成"每周看一眼对账报告"。

4.6 这一章的方法论

第一:永远假设外部系统不可靠。 支付回调会丢、会重、会乱序;第三方接口会挂;对账单会迟到。架构里凡依赖外部的地方,必须有兜底,而且兜底不能靠人。

第二:最终一致的正确姿势是"三层兜底"——事件驱动(回调)管常态、主动查询管延迟、对账管真相。 三层各司其职,任何一层挂了,另外两层还能兜住。单靠一层,就是把自己吊在悬崖上。

第三:幂等是分布式系统的必修课。 消息、回调、重试、定时任务——凡是可能重复执行的操作,都要有"业务唯一 ID + 存储唯一约束"。判断一个系统是否成熟,看它有没有把幂等当基础设施,而不是当补丁。

第四:契约类决策要格外谨慎。 状态机、对外接口、数据字典,这些一旦发布就是不可逆决策,定义时花十倍时间都不为过;但反过来,它们也是越早定义越值钱的资产。


第5章 拆服务:康威定律与拆的时机

5.1 现场:一次发布,把交易链路带崩 40 分钟

2022 年初,公司从 5 个人涨到 20 人,分成了三个组:交易组(我们)、增长组(秒杀、团购、营销)、供应链组(商品、采购、履约)。业务线齐了,问题也来了。

最痛的是发布。还是那个单体工程,三个组 20 个人在一个仓库里提交代码。一周发 5 次版,每次都有冲突,每次都要拉群对代码。最惨的一次:增长组上线秒杀活动,一条 SQL 没走索引,把单体的数据库 CPU 打到 100%,交易链路跟着挂了 40 分钟——当天订单量掉了 30%。

老周拍桌子:"以后谁再动,必须过全组评审!"但所有人都知道,评审解决不了结构问题:一个应用里塞三个组的业务,任何一个组的失误,都会炸掉所有人的链路。 这就是没有故障隔离。

同时,增长组天天喊"秒杀要独立扩容,大促时不想被交易拖累";供应链组喊"我们要自己的发布节奏,不能等你们排期"。

拆服务这件事,从"要不要拆"变成了"怎么拆"。

5.2 约束清单

约束类型 具体内容
组织 3 个组,各自 KPI、各自发布节奏、各自排期——这是最强的拆分驱动
技术 单体:发布互相踩、无故障隔离、无法独立扩容
风险 拆服务必须拆数据库;拆库后没有跨库事务,一致性方案要重设计
团队 后端 12 人,没有专职中间件,拆太细没人维护
时间 业务还在快速迭代,不能停下来重构太久

5.3 方案生成

方案 A:不拆,继续单体
靠代码评审、分支规范、更细的模块边界来缓解。改动最小,但不解决故障隔离和独立发布。

方案 B:拆 3 个服务,对齐 3 个组织
交易服务(订单+支付)、增长服务(营销+秒杀+团购)、供应链服务(商品+库存+履约)。每个组拥有自己服务的"写权限",通过接口协作。

方案 C:按 DDD 拆 8 个服务
订单、支付、库存、商品、营销、用户、履约、结算,每个领域一个微服务。教科书级划分,架构图好看。

5.4 取舍过程

方案 A 不是选项——组织已经从 5 人变成 3 组 20 人,单体物理上承载不了这种协作结构。这就是康威定律:系统结构会复制组织沟通结构。 组织变了,系统必须跟着变;反过来,组织没变就拆服务,拆出来的系统会在组织里找不到对应物,协调成本反而更高。先看组织,再看架构。

方案 C 呢?8 个服务,12 个后端,平均 1.5 人维护一个服务——这意味着每个人要同时维护 2-3 个服务,每个服务都是"没人真正懂"的状态。DDD 的限界上下文是分析工具,不是拆分粒度。拆分的粒度不是"领域概念",是"独立发布单元":最小可独立发布、独立伸缩、独立故障的单位。 3 个组 = 3 个发布单元 = 3 个服务,这就是 B。

方案 B 胜出,但真正的难点不是拆服务,是拆数据库。这里我做了整个项目里最谨慎的一个决策,拆库的三个原则:

原则一:一个数据,只有一个"写主"。 每个表归属一个服务,其他服务只能通过接口读写,禁止跨服务直连数据库。比如订单表归交易服务,营销活动表归增长服务,库存表归供应链服务——第 4 章的支付流水表也在交易服务。

原则二:跨服务事务,用最终一致,不用分布式事务。 拆库后,下单扣库存 + 建订单不再是一个本地事务。两条路:分布式事务框架(Seata,复杂、侵入强、性能损耗),或本地消息表 + MQ + 幂等——先写本地事务(建订单 + 落一条"待发消息"),MQ 把消息发给库存服务,库存服务幂等扣减(第 4 章的幂等基础设施,此时直接复用)。我选后者。分布式事务是把复杂度外包给框架,本地消息表是把复杂度留在自己手里但每一步可控——小团队,选可控。

原则三:拆的顺序,先拆最独立的,别一步到位。 第一步只拆供应链服务(商品+库存+履约):它的边界最清晰(第 1 章的模块边界,此时兑现了)、痛点最明显(秒杀要独立扩容)、依赖最少。跑通一个月,再拆增长服务,最后拆交易服务。每拆一个,灰度验证、随时可回滚;三个一起拆,出事你都不知道是哪个环节的问题。

演进前后的架构对比:

拆分后 · 2022年中

RPC / MQ

RPC / MQ

交易服务
(订单+支付)

交易库

增长服务
(营销+秒杀+团购)

增长库

供应链服务
(商品+库存+履约)

供应链库

拆分前 · 2022年初

单体应用
(3个组的代码混在一起)

MySQL 单实例
全库共享

对比要点:拆分前"一个库、一个进程、三组人抢";拆分后"三个服务、三个库、各管各的"——跨服务协作只走接口和 MQ,谁也不能碰别人的库。

5.5 事后验证

拆完三个月,效果清晰:

  • 发布冲突消失:三个组各自发布,互不阻塞,发布频率从每周 5 次变成每个组自己说了算
  • 故障隔离生效:三个月后增长组又出过一次事故(缓存击穿),这次只挂了营销活动,交易链路纹丝不动
  • 独立扩容兑现:5 月周年庆大促,增长服务单独扩了 3 台,交易服务原样不动,省了预算

但代价也真实存在,写在这里供你参考:

  • 联调成本上升:本地开发要起 3 个服务,跨服务联调靠 Mock 和测试环境,排错要顺着调用链追(链路追踪工具直到第 7 章复盘时才补上,这半年排错靠日志,苦)
  • 延迟增加:原来一次本地事务 20ms,现在跨服务 RPC + MQ,下单链路变成 60-80ms,用户可感知
  • 数据一致性心智负担:所有跨服务写操作都要想"最终一致对不对、补偿怎么做"

这些代价,拆之前我预判到了 80%。剩下 20%,是拆完才懂的。

5.6 这一章的方法论

第一:康威定律是拆分的第一触发器。 组织没变,别拆;组织变了(多团队并行、独立发布节奏、独立 KPI),不拆也得拆。架构对齐组织,而不是对齐理论。

第二:拆分粒度 = 独立发布单元,不是领域概念。 8 个服务的 DDD 图很美,但 12 个人养不起。宁可拆粗一点,让每个服务"有人真正懂",也别拆细了让每个服务都"没人负责"。

第三:拆服务是普通决策,拆数据库才是重决策。 服务边界可调,数据归属定了就极难改。拆库三原则:单一写主、最终一致(不用分布式事务)、分批拆(先拆最独立的)。

第四:好的架构决策是"攒出来的"。 第 1 章的模块边界、第 4 章的幂等基建,当时看起来是"多花了一点功夫",拆服务时全部兑现成"搬家式拆分"。架构不是某一天设计出来的,是每一次决策都在为未来铺路或挖坑。


第6章 大促事故:可用性不是玄学,是算账

6.1 现场:618 当晚 20:14,下单 P99 飙到 3 秒

2022 年 618 大促,拆完服务后的第一次大考。

20:14,监控告警:交易服务下单接口 P99 从 80ms 飙到 3s,5xx 率从 0.1% 冲到 12%。我盯着大屏,手在抖。

20:20,排查定位:不是交易服务自己的问题。供应链服务的"商品详情聚合接口"在某个爆款 SKU 上发生缓存穿透——促销价 1999 的电视,详情页请求量是预估的 8 倍,缓存全被这个 SKU 打穿,慢 SQL 把供应链数据库 CPU 打满,接口响应从 20ms 退化到 2s。

真正的灾难在交易服务这边:交易的下单链路同步调用这个详情接口,200 个线程全部堵在等待上,线程池耗尽,新请求进不来,全部排队——然后用户超时重试,请求量再放大一倍。 经典雪崩:一个下游变慢,通过同步依赖,把整个上游拖死。

20:40,供应链组紧急重启了数据库,恢复。

当晚损失:订单量掉 25%,退款率上升,老周的微信语音打了 17 个。而这一切的根源,不是那个缓存穿透——是我们没有给"下游变慢"设计任何防线。

雪崩是怎么一步步传导的:

供应链服务 (数据库被打满) 交易服务 (线程池 200) 用户请求 供应链服务 (数据库被打满) 交易服务 (线程池 200) 用户请求 缓存穿透,DB CPU 100% 接口 20ms → 2s 线程全被"等供应链"占满 新请求全部排队 线程池耗尽 新请求直接拒绝/超时 重试流量放大 → 雪崩 下单请求(第1个) 同步调用商品详情接口 2s 后才返回 下单请求(第100个) 下单请求(第200个) 用户超时 → 疯狂重试

一个下游变慢 → 上游线程被占满 → 排队 → 超时重试 → 流量放大 → 全链路雪崩。 这就是 6.4 节要解决的核心问题。

6.2 约束清单

约束类型 具体内容
业务 大促窗口(4 小时)内不能挂;挂了就是事故
目标 老板拍板"可用性要 99.9%",但他不知道这意味着什么——需要算给他看
技术 同步依赖多(交易→供应链、交易→增长),任何一个变慢都可能雪崩
成本 99.9% 到 99.99% 的成本是数量级差异,预算不支持
时间 距离双十一还有 5 个月,来得及系统性治理

6.3 先算账:SLA 不是口号,是预算

"可用性 99.9%"到底意味着什么?先算账:

可用性 全年宕机预算 换算
99.9% 8.76 小时/年 相当于每年允许挂 8 个多小时
99.99% 52.6 分钟/年 每年只能挂不到 1 小时
99.999% 5.26 分钟/年 金融级,基本不能挂

再算一次事故的代价:618 当晚挂了 40 分钟,订单损失 25%,按日均 2 万单、客单价 80 元粗算:

40 分钟损失 ≈ 2万 × 25% × (40/1440) × 80 ≈ 11 万订单额 + 退款 + 口碑

99.99% 意味着大促窗口内连一次 40 分钟的事故都不能容忍——那需要多活架构、异地容灾、全链路演练,投入是 99.9% 方案的 5-10 倍。而我们的业务,大促之外 99.9% 完全够用。

我拿着这张表找老周:"99.9%,我保证大促窗口零事故、平时最多每年 8 小时不可用;99.99%,预算要翻 8 倍,多招一个 SRE 团队。你要哪个?"老周选了 99.9%。架构谈判的本质,是把抽象的质量属性换算成老板听得懂的钱和时间。

6.4 方案生成:防雪崩三件套

目标定了,方案围绕"防止同步依赖雪崩"展开:

方案 A:扩容 + 监控
出事了加机器,提前做好监控告警。治标:机器加得再快,也快不过雪崩的速度;监控只能发现,不能阻止。

方案 B:超时治理 + 熔断 + 降级(三件套)

  • 超时:所有下游调用设"连接超时 + 读超时"(如 300ms + 1s),快速失败,不无限等待
  • 熔断:下游错误率超过阈值(如 50%),熔断器打开,直接快速失败,不再发请求,给下游恢复时间
  • 降级:熔断后走降级路径——库存查不到就不查(用缓存兜底),详情页挂了返回简化数据,实在不行返回"稍后再试"
  • 配套:全链路压测前置验证 + 大促预案(限流阈值、降级开关、值班表)

熔断器本身是"三态循环",不是简单的开/关:

错误率 > 50%(超过最小请求数20)

熔断10s后尝试放行少量请求

试探请求仍失败

试探请求成功 → 关闭熔断

CLOSED

OPEN

HALF_OPEN

闭合(正常放行)→ 打开(全部快速失败)→ 半开(试探恢复)→ 回到闭合。 半开状态是关键:不给下游"满血冲击",只放行少量流量试水温,成功才恢复全量。

方案 C:异步化解耦
把交易对供应链的同步调用全部改成 MQ 异步。彻底隔离,但改动巨大,5 个月内做不完,且引入新的复杂度(第 3、4 章的教训)。

6.5 取舍过程

方案 C 先排除——"用异步解决所有同步问题"是另一个极端,改造成本和风险都不可控,而且异步化自己的问题(堆积、幂等、排错)我们刚吃过亏。能同步解决的,别异步化;异步化是最后的武器,不是第一选择。

方案 A 太弱。方案 B 是标准答案,关键在落地细节:

选型:Sentinel vs Resilience4j。 团队是 Java 技术栈,已经在用 RocketMQ(阿里生态),Sentinel 支持熔断、限流、降级、热点防护,和 Nacos 配置中心配合好,控制台可视化,国内文档多。Resilience4j 更轻量、纯代码配置,但功能少、无控制台。选 Sentinel——选型标准还是那两条:团队上手成本 + 功能匹配度。 大促高峰,我们在控制台一键打开降级规则,这是 Resilience4j 做不到的。

降级要提前设计"降级后长什么样",不能临时拍脑袋:

依赖 正常 降级后
商品详情 供应链接口返回完整信息 返回本地缓存的简化信息(价格、主图、库存)
库存查询 实时库存 读 Redis 预扣库存,过期数据可接受(下单时再强校验)
营销计算 实时算优惠 关闭复杂优惠,只保留基础折扣
支付回调 正常处理 进 MQ 排队,消费端限流(第 4 章的兜底正好接住)

熔断参数要基于压测数据,不靠拍脑袋: 错误率阈值 50%、最小请求数 20、熔断时长 10s——这些数字来自 6.6 节的压测。

6.6 事后验证:压测把事故拦在线上之前

双十一前 3 周,我们做了第一次全链路压测(用线上真实流量模型回放),结果触目惊心,3 个隐患在压测里现形:

  1. 交易服务数据库连接池配的是默认 10,压测 300 QPS 就告警——改成 50 并加了等待队列上限
  2. 商品详情接口没有缓存,压测直接打穿——补了本地缓存 + Redis 二级缓存
  3. 日志框架在大流量下疯狂刷屏,IO 打满——调了日志级别和采样

双十一当晚,峰值流量比预估还高 30%,但这次稳住了:中间有 3 次触发熔断降级,都按预案平滑降级,用户无感,大促窗口零事故。

压测是最便宜的保险。 618 那次事故的根因,压测里 100% 能复现;而压测发现问题的成本,是线上事故成本的百分之一。

6.7 这一章的方法论

第一:可用性不是玄学,是算账。 SLA 的每一个 9,都是真金白银。把"99.9% vs 99.99%"换算成年宕机小时数、换算成事故损失、换算成预算投入,让老板做选择题,而不是让他提一个他不懂的要求。

第二:雪崩的根因是同步依赖,防线是"超时 + 熔断 + 降级"三件套。 没有防线的同步调用,等于把全系统吊在一个下游的稳定性上。超时管"快速失败",熔断管"不再打下游",降级管"失败后用户看到什么"——三层缺一不可。

第三:验证前置,压测常态化。 架构决策靠验证,验证里最便宜的是压测。每次大促前压测,每次大促后复盘,把"事故驱动改进"变成"预演驱动改进"。


第7章 复盘:回看决策树,为自己的决策付代价

7.1 现场:新人问"这两个状态有什么区别",没人答得上来

2023 年初,团队来了个新人,接手订单模块。第三天,他拿着代码问我:“订单表里 statuspay_status 两个字段,为什么这么设计?什么时候该用哪个?”

我愣了一下。这个设计是双十一前定下来的,当时讨论得很充分——status 管订单生命周期,pay_status 管支付生命周期,两者解耦,因为退款时订单可能还在"已发货"而支付已经"已退款"。但现在让我把这个决策的完整上下文讲清楚,我发现自己只能讲出七成。

更糟的是,这个设计的原始讨论,只存在于当时几个人的聊天记录里,拉群记录已过期,文档没写。

决策没有记录,就变成玄学。 团队里的人来来去去,决策的理由跟着人走,后来者只能靠猜——猜错了,就重复踩坑,或者"优化"掉一个精心设计的边界。

7.2 复盘:7 个决策,逐条过堂

年终复盘,我把过去两年的关键决策拉了一张表,每一行都问三个问题:当时为什么这么选?结果如何?如果重来,还会这么选吗?

# 决策 当时的判断 结果 复盘结论
1 模块化单体 可逆决策优先,信息不足不上微服务 成了第 5 章拆服务的地基 赌对,且是全文最值钱的一笔
2 数据库原子扣减 几十 QPS 用不上 Redis 7 场活动零超卖 赌对,触发条件管理有效
3 大促 Redis 预扣 + MQ 只对热点用,平时走同步 大促稳,但埋了不一致的雷 半对:方案对,热点判定拍脑袋
4 状态机 + 幂等 + 对账 契约谨慎定义,三层兜底 重复订单清零,对账自动化 赌对,基建红利持续兑现
5 拆服务对齐组织 康威定律,3 组 3 服务 发布解耦、故障隔离 赌对,但链路追踪补晚了
6 99.9% + 熔断降级 可用性是算账,不是玄学 双十一零事故 赌对,压测前置是功臣
7 决策没写 ADR —— 新人看不懂设计,重复提问 做错,欠债要还

注意第 7 行:它不是某个技术方案的对错,而是整个决策体系的漏洞——前 6 个决策都对,但没有记录,它们的"对"就没法传承,等于每次决策都要重来一次。

7.3 两个被复盘揪出来的具体问题

问题一:Redis 与 DB 库存不一致的根源——"热点判定"是拍脑袋。

第 3 章,我们判定"哪些 SKU 是热点、要预热进 Redis"靠的是运营拍脑袋。结果普通 SKU 被误判进 Redis,活动结束后 Redis 剩 0、DB 还有 12,两边对不上,最后靠人工修正。

复盘后的解法:热点判定改成数据驱动——按历史销量分位数自动圈定热点 SKU,Redis 库存和 DB 库存加每日对账任务(第 4 章的对账基建直接复用),差异自动修正。

问题二:链路追踪补得太晚。

第 5 章拆完服务,排错要顺着调用链手动翻日志,618 事故排查花了 40 分钟。复盘后补了 SkyWalking,第二次大促的熔断排查 5 分钟定位。这笔账很清晰:拆分之前就该把链路追踪当基础设施铺好,而不是等排错痛了再补。

7.4 决策记录:ADR,一行都不多的模板

复盘后,我立了一条规矩:每个架构决策,必须写一份 ADR(Architecture Decision Record)。 模板就四段,不许写废话:

# ADR-001:库存扣减采用数据库原子操作

## 背景(当时发生了什么)
端午活动超卖 300 单,根因是"先查后改"竞态。

## 约束(当时不能动的条件)
不能超卖(钱);峰值几十 QPS;团队无中间件专家。

## 决策与理由
用 UPDATE ... WHERE stock >= n 原子扣减。
理由:行锁保证原子性,零新组件;Redis 方案触发条件未到。

## 后果(后来发生了什么)
7 场活动零超卖;大促前触发条件满足,迁移到 Redis 预扣(见 ADR-003)。

为什么要写 ADR?三个理由:

  1. 让后来者知道"为什么",而不是只看到"是什么"——新人问 statuspay_status,一份 ADR 就解释清楚
  2. 逼自己把决策讲清楚——写不出来的决策,说明你还没想明白
  3. 让复盘有据可依——没有记录,复盘就变成记忆竞赛,谁嗓门大谁有理

7.5 这一章的方法论

第一:复盘要区分"决策质量"和"结果运气"。 决策对、结果差(比如压测没做、出事故),要认;决策对、结果好,要复现;决策错、结果碰巧好,更要警惕——那是最危险的侥幸。复盘的对象是决策过程,不是结果本身。

第二:决策记录是架构师留给团队的遗产。 架构师真正的产出,不只是系统,还有决策的可传承性。ADR 让每个决策的理由活过人员流动。判断一个架构师的水平,看他团队的 ADR 写得怎么样。

第三:没有记录就没有复盘,没有复盘就没有进化。 复盘不是秋后算账,是把"每次决策都从零开始"变成"站在上一次决策的肩膀上"。这就是决策框架的最后一环——验证反馈,让下一轮决策比上一轮更好。


终章 可迁移的决策资产

故事讲完了。回到开篇那个问题:为什么"完美架构"会毁掉一个项目?因为它不是决策,是表演。

真正的架构设计,是 7 个决策节点的每一次"现场→约束→方案→取舍→决策→验证"。先回头看这两年,架构是怎么一步步长出来的:

第1章

第2章

第3章

第4章

第5章

第6章

2021春
单体上线
(模块化结构)

2021夏
超卖事故
→ 原子扣减

2021冬
大促
→ Redis预扣 + MQ削峰

2022初
支付对账
→ 状态机+幂等+对账

2022中
拆服务
→ 3服务3库

2022秋
可用性治理
→ 熔断+降级+压测

2023初
复盘
→ ADR 决策记录

注意每一条箭头:没有一步是"计划好的完美蓝图",全是"业务/事故逼到面前 → 在约束下决策"。 架构不是设计出来的,是决策攒出来的。

现在,把散落在七章里的方法论收束成三样可以直接带走的东西。

9.1 完整决策框架:五步闭环

约束变了,重新识别

① 识别约束
时间/人力/资金/组织/业务
硬约束 vs 软约束

② 明确目标
第一目标写下来
冲突时定优先级

③ 生成方案
≥2 个候选,各列代价
评估可逆性

④ 权衡取舍
对照目标打分
触发条件到没到?

⑤ 验证反馈
压测/演练/灰度
写 ADR、定复盘

每一步的自检问题:

① 识别约束

  • 时间、人力、资金、组织、业务阶段,我列全了吗?
  • 哪些是硬约束(不能动的),哪些是软约束(可以谈的)?
  • 团队能力是不是约束?没人会的东西,方案再先进也是负债。

② 明确目标

  • 这个决策的"第一目标"是什么?写下来。
  • 目标之间冲突时,谁优先?(不超卖 > 体验;不挂 > 省机器)
  • 目标能量化吗?QPS、延迟、成本、可用性,数字呢?

③ 生成方案

  • 至少 2 个方案,每个都列出代价了吗?
  • 有没有第三方案?"不做什么"算不算一个方案?
  • 每个方案的可逆性如何?撤销成本是多少?

④ 权衡取舍

  • 我的取舍标准是什么?和"明确目标"对得上吗?
  • 引入的每个组件,触发条件是什么?现在到没到?
  • 这个决策是契约/不可逆的吗?是的话,要不要再想想?

⑤ 验证反馈

  • 上线前能压测/演练吗?别等事故当验证。
  • 决策记录(ADR)写了吗?
  • 什么时候复盘?谁来复盘?

9.2 反模式清单:看起来专业,实则害人

反模式 表现 为什么害人
蓝图式设计 画两米长的架构图,不匹配任何真实约束 在真空中做决策,第一课就是空中楼阁
只有一个方案 拍板时只列了一个方案 没得选 ≠ 决策,没有对比就没有取舍
技术崇拜 “Redis/MQ/微服务迟早要用,先上” 触发条件没到就负债,复杂度先于业务
把可逆做成不可逆 信息不足时拆了微服务/定了数据归属 撤销成本极高,一步错步步错
赌运气当验证 压测不跑、预案不写,靠上线赌 事故是最贵的验证,而且常常验证的是别人的坑
决策不留痕 决策只存在于聊天记录和脑海里 人走决策亡,后来者重复踩坑

9.3 决策检查清单:下次做架构决策时逐条打勾

□ 约束清单:时间/人力/资金/组织/业务,列全了
□ 硬约束和软约束分开了
□ 第一目标写下来了,且能量化
□ 至少 2 个候选方案,代价都列了
□ 每个方案的可逆性评估了
□ 引入组件的触发条件写清楚了
□ 这个决策是不是契约/不可逆(格外谨慎)
□ 验证方式定了(压测/演练/灰度),不是靠上线赌
□ ADR 写完了
□ 复盘时间定了

最后说一句

两年,7 个决策节点,一个系统从 5 个人的单体长成 3 个服务的架构。回头看,没有一个决策是"标准答案"——每个都是当时约束下的最优解,有些后来被证明错了,但错了也是决策的一部分

架构设计的本质,就是在约束下做决策,并为自己的决策付代价、做复盘、留记录。

你下一次面对架构选择的时候,记住这句话就够了:

好的架构不是设计出来的,是一个个决策攒出来的。


全文完。欢迎留言讨论你的架构决策故事。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐