为什么“完美架构“毁掉了项目?一个电商系统 7 个决策复盘
为什么"完美架构"毁掉了项目?一个电商系统 7 个决策复盘
全文约 15000 字,以一个电商交易系统从 0 到规模化的完整演进为主线,现场复盘 7 个关键架构决策节点。
不聊概念焦虑,只聊约束、取舍和算账。每个结论都挂在具体决策上,可以直接迁移到你的项目里。
目录
- 开篇:一个"完美架构"如何毁掉一个项目
- 第1章 第一版架构:单体的克制
- 第2章 超卖事故:库存一致性
- 第3章 大促:削峰填谷与 MQ 的时机
- 第4章 支付对账:状态机、幂等与最终一致
- 第5章 拆服务:康威定律与拆的时机
- 第6章 大促事故:可用性不是玄学,是算账
- 第7章 复盘:回看决策树,为自己的决策付代价
- 终章 可迁移的决策资产
开篇:一个"完美架构"如何毁掉一个项目
2019 年,我认识的一个团队拿到了一个企业服务项目的订单。项目不大,但技术负责人是个完美主义者。他花了整整三个月设计架构:微服务拆了八个,消息队列、分布式事务、注册中心、配置中心、链路追踪、灰度发布平台,一应俱全。架构图打印出来贴在墙上,两米长。
结果呢?项目上线推迟了四个月,期间架构改了三版。客户等不及,把项目砍了。团队散伙那天,技术负责人说了句:“架构没设计好。”
不,架构设计得很好。问题恰恰在于它"太好了"——好到不匹配任何真实约束。
三个月里,团队只有 6 个人,没有人写过分布式事务;客户要的是 90 天内上线,不是 5 个 9 的可用性;业务逻辑总共不到 50 张表,却要跨 8 个服务调用。这不是架构设计,这是在真空中画蓝图。
为什么很多架构师会犯这个错?因为我们的行业长期把"架构设计"等同于"画出漂亮的架构图"。图越复杂,显得越专业。但真实世界的架构师,工作根本不是画图,而是在约束下做决策:
- 只有 3 个月,是上微服务还是上单体?
- 库存不能超卖,是加锁还是预扣?
- 大促流量是平时的 20 倍,是买机器还是削峰?
- 支付回调丢了,是改第三方还是自己兜底?
每一个问题,都没有标准答案。答案取决于你的约束:时间、人力、资金、团队能力、业务阶段、可接受的风险。
所以这篇文章,我不想讲任何"架构原则"或"最佳实践"的空话。我想带你完整走一遍一个真实的电商交易系统从 0 到规模化的全过程——7 个关键决策节点,每个节点我都会告诉你:当时面临什么约束、生成了哪些候选方案、为什么这么选、后来被证明是对是错。
读完你会带走三样东西:
- 一套决策框架:识别约束 → 明确目标 → 生成方案 → 权衡取舍 → 验证反馈
- 一组反模式:那些看起来专业、实则害人的做法
- 一张决策检查清单:下次做架构决策时逐条打勾
故事开始。
第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 应用,按模块分包:
order、inventory、payment、user、product - 模块之间只通过 Service 接口交互,禁止跨模块直接操作对方的 Repository
- 数据库:一个 MySQL 实例,业务表按模块前缀命名(
ord_order、inv_stock、pay_payment) - 预留:将来每个模块都可以独立成服务,只是现在是"逻辑上拆分、物理上合并"
第一版的模块结构长这样:
注意两个关键约束:模块间只通过 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 就翻车了——超卖跟高并发没有必然关系,是逻辑本身的洞。
时间线上看,竞态是这样的:
两个请求都先"读到还有 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,数据库原子扣减。
落地时还有三个细节:
- 事务边界:扣库存和建订单必须在同一个数据库事务里。库存扣减成功但订单插入失败 → 整个事务回滚,库存还原,不产生幽灵单。
- 失败即拒绝:扣减失败(影响行数 0)直接返回"库存不足",绝不重试抢购——重试会放大无效请求。
- 加唯一约束兜底:
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(选择性地用)+ 限流:
- 热点库存 Redis 预扣(方案 B):库存按"是否爆品"分流。爆品 SKU 预热进 Redis,Lua 脚本原子扣减,扣减成功异步落库;普通 SKU 继续走数据库原子扣减。只对热点用 Redis,不为全体引入复杂度。
- MQ 只用在真正需要排队的地方:支付回调异步处理(第 4 章会讲到)、订单超时关单(延迟消息)、以及大促高峰期的下单排队降级——平时走同步,一旦 QPS 超过阈值(比如 300),自动切换为"先落 MQ 再异步建单"的排队模式。平时不用的复杂路径,大促才激活。
- 入口限流:令牌桶,应用层前置。超阈值直接返回"当前人数过多,请稍后再试",不让流量打到数据库。
MQ 选型上,团队没人用过 Kafka,但 RocketMQ 对国内开发者友好、自带事务消息和延迟消息、中文文档齐全,我选了 RocketMQ——选型标准是:团队上手成本 + 功能匹配度,不是"谁吞吐最高"。 Kafka 吞吐更高,但我们用不到,而它的运维复杂度我们承受不起。
大促期间的完整下单链路长这样:
三个关键点:限流器是"闸门",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 单卡死的温床。重构后,状态迁移被画成一张图:
每一条箭头都是一个"合法迁移",箭头之外的状态跳转一律拒绝——状态机把"想改就改"变成"按图迁移"。
用状态机约束每一次迁移:
// 非法迁移直接拒绝,不靠人肉保证
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 逐笔核对:
- 对账单有、本地没有 → 单边账(钱收了,单没建/没更新)→ 告警 + 自动按单号补单
- 本地有、对账单没有 → 支付失败/未回调,标记异常人工核查
- 金额不一致 → 高优先级告警
三件事合起来,就是"最终一致"的正确姿势——三层兜底,各管一摊:
三层各司其职,任何一层挂了,另外两层还能兜住。单靠回调一层,就是把自己吊在悬崖上。
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 章的模块边界,此时兑现了)、痛点最明显(秒杀要独立扩容)、依赖最少。跑通一个月,再拆增长服务,最后拆交易服务。每拆一个,灰度验证、随时可回滚;三个一起拆,出事你都不知道是哪个环节的问题。
演进前后的架构对比:
对比要点:拆分前"一个库、一个进程、三组人抢";拆分后"三个服务、三个库、各管各的"——跨服务协作只走接口和 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 个。而这一切的根源,不是那个缓存穿透——是我们没有给"下游变慢"设计任何防线。
雪崩是怎么一步步传导的:
一个下游变慢 → 上游线程被占满 → 排队 → 超时重试 → 流量放大 → 全链路雪崩。 这就是 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%),熔断器打开,直接快速失败,不再发请求,给下游恢复时间
- 降级:熔断后走降级路径——库存查不到就不查(用缓存兜底),详情页挂了返回简化数据,实在不行返回"稍后再试"
- 配套:全链路压测前置验证 + 大促预案(限流阈值、降级开关、值班表)
熔断器本身是"三态循环",不是简单的开/关:
闭合(正常放行)→ 打开(全部快速失败)→ 半开(试探恢复)→ 回到闭合。 半开状态是关键:不给下游"满血冲击",只放行少量流量试水温,成功才恢复全量。
方案 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 个隐患在压测里现形:
- 交易服务数据库连接池配的是默认 10,压测 300 QPS 就告警——改成 50 并加了等待队列上限
- 商品详情接口没有缓存,压测直接打穿——补了本地缓存 + Redis 二级缓存
- 日志框架在大流量下疯狂刷屏,IO 打满——调了日志级别和采样
双十一当晚,峰值流量比预估还高 30%,但这次稳住了:中间有 3 次触发熔断降级,都按预案平滑降级,用户无感,大促窗口零事故。
压测是最便宜的保险。 618 那次事故的根因,压测里 100% 能复现;而压测发现问题的成本,是线上事故成本的百分之一。
6.7 这一章的方法论
第一:可用性不是玄学,是算账。 SLA 的每一个 9,都是真金白银。把"99.9% vs 99.99%"换算成年宕机小时数、换算成事故损失、换算成预算投入,让老板做选择题,而不是让他提一个他不懂的要求。
第二:雪崩的根因是同步依赖,防线是"超时 + 熔断 + 降级"三件套。 没有防线的同步调用,等于把全系统吊在一个下游的稳定性上。超时管"快速失败",熔断管"不再打下游",降级管"失败后用户看到什么"——三层缺一不可。
第三:验证前置,压测常态化。 架构决策靠验证,验证里最便宜的是压测。每次大促前压测,每次大促后复盘,把"事故驱动改进"变成"预演驱动改进"。
第7章 复盘:回看决策树,为自己的决策付代价
7.1 现场:新人问"这两个状态有什么区别",没人答得上来
2023 年初,团队来了个新人,接手订单模块。第三天,他拿着代码问我:“订单表里 status 和 pay_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?三个理由:
- 让后来者知道"为什么",而不是只看到"是什么"——新人问
status和pay_status,一份 ADR 就解释清楚 - 逼自己把决策讲清楚——写不出来的决策,说明你还没想明白
- 让复盘有据可依——没有记录,复盘就变成记忆竞赛,谁嗓门大谁有理
7.5 这一章的方法论
第一:复盘要区分"决策质量"和"结果运气"。 决策对、结果差(比如压测没做、出事故),要认;决策对、结果好,要复现;决策错、结果碰巧好,更要警惕——那是最危险的侥幸。复盘的对象是决策过程,不是结果本身。
第二:决策记录是架构师留给团队的遗产。 架构师真正的产出,不只是系统,还有决策的可传承性。ADR 让每个决策的理由活过人员流动。判断一个架构师的水平,看他团队的 ADR 写得怎么样。
第三:没有记录就没有复盘,没有复盘就没有进化。 复盘不是秋后算账,是把"每次决策都从零开始"变成"站在上一次决策的肩膀上"。这就是决策框架的最后一环——验证反馈,让下一轮决策比上一轮更好。
终章 可迁移的决策资产
故事讲完了。回到开篇那个问题:为什么"完美架构"会毁掉一个项目?因为它不是决策,是表演。
真正的架构设计,是 7 个决策节点的每一次"现场→约束→方案→取舍→决策→验证"。先回头看这两年,架构是怎么一步步长出来的:
注意每一条箭头:没有一步是"计划好的完美蓝图",全是"业务/事故逼到面前 → 在约束下决策"。 架构不是设计出来的,是决策攒出来的。
现在,把散落在七章里的方法论收束成三样可以直接带走的东西。
9.1 完整决策框架:五步闭环
每一步的自检问题:
① 识别约束
- 时间、人力、资金、组织、业务阶段,我列全了吗?
- 哪些是硬约束(不能动的),哪些是软约束(可以谈的)?
- 团队能力是不是约束?没人会的东西,方案再先进也是负债。
② 明确目标
- 这个决策的"第一目标"是什么?写下来。
- 目标之间冲突时,谁优先?(不超卖 > 体验;不挂 > 省机器)
- 目标能量化吗?QPS、延迟、成本、可用性,数字呢?
③ 生成方案
- 至少 2 个方案,每个都列出代价了吗?
- 有没有第三方案?"不做什么"算不算一个方案?
- 每个方案的可逆性如何?撤销成本是多少?
④ 权衡取舍
- 我的取舍标准是什么?和"明确目标"对得上吗?
- 引入的每个组件,触发条件是什么?现在到没到?
- 这个决策是契约/不可逆的吗?是的话,要不要再想想?
⑤ 验证反馈
- 上线前能压测/演练吗?别等事故当验证。
- 决策记录(ADR)写了吗?
- 什么时候复盘?谁来复盘?
9.2 反模式清单:看起来专业,实则害人
| 反模式 | 表现 | 为什么害人 |
|---|---|---|
| 蓝图式设计 | 画两米长的架构图,不匹配任何真实约束 | 在真空中做决策,第一课就是空中楼阁 |
| 只有一个方案 | 拍板时只列了一个方案 | 没得选 ≠ 决策,没有对比就没有取舍 |
| 技术崇拜 | “Redis/MQ/微服务迟早要用,先上” | 触发条件没到就负债,复杂度先于业务 |
| 把可逆做成不可逆 | 信息不足时拆了微服务/定了数据归属 | 撤销成本极高,一步错步步错 |
| 赌运气当验证 | 压测不跑、预案不写,靠上线赌 | 事故是最贵的验证,而且常常验证的是别人的坑 |
| 决策不留痕 | 决策只存在于聊天记录和脑海里 | 人走决策亡,后来者重复踩坑 |
9.3 决策检查清单:下次做架构决策时逐条打勾
□ 约束清单:时间/人力/资金/组织/业务,列全了
□ 硬约束和软约束分开了
□ 第一目标写下来了,且能量化
□ 至少 2 个候选方案,代价都列了
□ 每个方案的可逆性评估了
□ 引入组件的触发条件写清楚了
□ 这个决策是不是契约/不可逆(格外谨慎)
□ 验证方式定了(压测/演练/灰度),不是靠上线赌
□ ADR 写完了
□ 复盘时间定了
最后说一句
两年,7 个决策节点,一个系统从 5 个人的单体长成 3 个服务的架构。回头看,没有一个决策是"标准答案"——每个都是当时约束下的最优解,有些后来被证明错了,但错了也是决策的一部分。
架构设计的本质,就是在约束下做决策,并为自己的决策付代价、做复盘、留记录。
你下一次面对架构选择的时候,记住这句话就够了:
好的架构不是设计出来的,是一个个决策攒出来的。
全文完。欢迎留言讨论你的架构决策故事。
更多推荐


所有评论(0)