去年帮一个团队做架构评审,对方甩过来一张画得相当漂亮的部署图,五颜六色的方框、箭头纵横交错,看着就很有"技术含量"。我问了一句:这套东西为什么要拆成这么多块?对方沉默了十几秒,回答说"参考了行业里比较主流的做法"。
这个场景我见过太多次了。项目产品技术具体架构设计这件事,最难的从来不是画出那张图,而是图上的每一个方框、每一条箭头背后,都能说清楚"为什么是它,而不是别的"。图是结果,决策才是内容。一份没有决策记录的架构图,本质上和一张装修效果图没区别——好看,但经不起追问。
我写这篇东西,是想把架构设计从"画图"这个动作里拽出来,还原成它本来该有的样子:一串有先后顺序的判断题。适合谁看?准备从零搭一个产品的技术负责人、需要接手别人架构的中层开发、以及那些画完图被老板追问"为什么这么设计"的同学。下面这些内容不是标准答案,是我在实际项目里反复验证过的一套推进顺序,你可以按需取用。
1. 架构设计的真正起点:把模糊需求翻译成可计算的约束
绝大多数架构设计的失败,根源不在技术选型,而在起点就跑偏了——拿到一份需求文档,直接开始想"用什么框架、拆几个服务"。这条路走不通,因为需求文档写的是"用户要什么",而架构要回答的是"系统在什么条件下必须成立"。
1.1 需求清单为什么不能直接变成架构图
需求文档里的每一句话,几乎都是形容词和动词的组合:"系统要快""支持多人协作""数据要安全""以后可能扩展"。这些描述对产品经理是有意义的,对架构师几乎没有信息量。因为"快"没有单位,"多人"没有量级,"以后"没有时间点。
我习惯做的第一件事,是把需求逐条改写成带数字的约束。举个例子,"系统要快"这句话,在架构层面至少要拆成三问:多快算快(接口响应时间的目标值是多少)、在什么负载下要快(并发用户数、峰值QPS)、慢到什么程度算不可接受(超时阈值和降级边界)。这三问答完,"快"才变成一个可以指导设计的输入。
这个改写过程有个副作用特别好:它会把需求里那些被含糊带过的部分全部暴露出来。某项目组曾经在需求评审时写"支持未来业务增长",我追问了一句"增长到多少",结果发现产品和研发对"增长"的理解差了整整两个数量级——产品想的是用户翻三倍,研发以为要按十倍预留。这种错位如果不在架构阶段解决,后面所有的容量规划、数据库选型全是空中楼阁。
1.2 一张约束采集表该怎么填
我一般会用一张表把所有约束摊开,四个维度,每个维度填不满就先别往下走。
| 维度 | 需要采集的信息 | 填不出来的后果 |
|---|---|---|
| 业务约束 | 核心业务流程、业务峰值的时间规律、不能中断的功能清单 | 分不清主次,把边缘功能做成核心链路 |
| 规模约束 | 用户量级、请求量级、数据增长速率、单条数据的大小 | 容量估算失真,存储和带宽选型全错 |
| 质量约束 | 响应时间目标、可用性目标、数据一致性要求 | 无法判断该用同步还是异步、要不要分布式事务 |
| 团队与资源约束 | 团队人数与技能栈、交付时间节点、可用的基础设施条件 | 选了一堆团队驾驭不了的技术,上线即负债 |
这张表里最容易被跳过的是最后一行。很多人觉得架构是纯技术问题,团队情况不该掺和进来。这是书生气。一个五人团队维护八个微服务,技术再先进也是灾难,因为运维成本、排障成本、沟通成本会成倍吃掉技术进步带来的收益。我在实际项目里见过太多"教科书式架构"死在团队规模上。
1.3 从约束推导架构风格的三条判断线
约束采集完之后,怎么推出架构风格?我一般看三条线。
第一条线是变更频率的差异度。如果产品里不同模块的变更频率差距很大(比如订单规则一周改三次,用户基础信息一年不动),那按变更频率做拆分是合理的,模块边界大致能对上团队边界。如果所有模块都是一起改、一起发,硬拆成多个独立部署单元只会增加发布协调成本。
第二条线是负载特征的差异度。有的功能是 IO 密集型(大量查询、文件读写),有的是 CPU 密集型(报表计算、图像处理)。这两类混在一个进程里,会互相拖累——计算任务把线程池占满,查询请求全部排队。这时候按负载特征分离部署是有价值的。
第三条线是故障影响范围的容忍度。核心交易链路和非核心的运营后台,对可用性的要求完全不同。如果非核心模块出问题会拖垮核心模块(共享数据库连接池、共享线程池),那就必须做隔离。
这三条线的判断结果,直接决定了你是走单体、模块化单体,还是服务化。注意,模块化单体在绝大多数中小规模项目里是被严重低估的选项——它在代码层面已经有清晰边界,但部署仍是一体,省掉了分布式系统那一整套复杂度。先能拆而不拆,等真需要拆的时候再拆,比一上来就拆散了拼不回去要安全得多。
2. 分层与边界的划法:让每一层只对一件事负责
分层这个词谁都会说,但真正做到"层与层之间只通过约定的方式通信",十个项目里能有两三个就不错了。大部分所谓的分层,最后都退化成了"文件夹分层"——代码放在不同目录里,调用关系依旧是网状穿透。
2.1 四层划分的实际职责红线
我用的分层一般是四层,但重点不在层数,而在每层的红线:什么东西绝对不能出现在这一层。
接入层负责协议转换、参数校验、鉴权、限流。红线是:不做业务判断。我见过接入层里写"如果用户等级是VIP且订单金额大于500则走特殊逻辑"的代码,这种代码一旦业务规则变了,改起来要在网关里翻半天,而且完全无法单测。
应用层负责编排——把一次业务请求拆成若干个领域动作,控制事务边界。红线是:不写业务规则本身。应用层应该是"薄"的,它的价值在于顺序和边界,不在于逻辑。
领域层负责业务规则和状态流转。红线是:不依赖任何框架和外部资源。这一层应该是纯逻辑,输入是数据,输出是数据,不碰数据库、不发消息、不调接口。做到这一点,业务规则才能真正被单元测试覆盖。
基础设施层负责持久化、消息、外部服务调用。红线是:不含业务分支。这一层只做"怎么存取",不做"存什么、什么时候存"。
这套红线的核心价值在于:当业务规则变化时,改动应该主要落在领域层;当存储方案变化时,改动应该主要落在基础设施层。如果改一个业务规则要动五个文件分属四个层,说明边界已经失效了。
2.2 边界划错之后会出现的四种典型症状
判断边界有没有划对,不用看设计文档,看代码症状就行。
症状一:一个改动牵连多个模块同时发布。你想改个折扣计算,结果发现用户模块、订单模块、结算模块都得跟着改。这说明领域边界切在了业务流程的中间,而不是切在业务概念的分界上。
症状二:循环依赖。A 模块调 B,B 又反过来调 A。这在编译期可能被语言特性掩盖了,但运行时的初始化顺序、测试时的 mock 成本会立刻暴露出来。循环依赖的本质是"两个模块共享了同一个业务概念",正确的做法是把这个概念单独提出来,变成一个双方都依赖的第三块。
症状三:大量公用工具类。一开始是"日期格式化""字符串处理",后来变成"订单状态判断""权限校验"也放进去了。工具包一旦开始装业务逻辑,就说明有业务概念找不到归属,这是边界划分不清的典型信号。
症状四:跨模块的直接数据库访问。订单模块直接查用户表,结算模块直接改订单表。这种情况短期看省事,长期看等于把两个模块焊死在一起,任何一方的表结构变更都会引发连锁反应。
2.3 模块依赖方向的可视化校验方法
依赖方向对不对,光靠人眼看代码是看不准的。我一般用两个土办法做校验。
第一个办法是画依赖矩阵。把所有模块列成行和列,行依赖列就在格子里打勾。画完之后看两件事:对角线右上角是不是大部分为空(说明依赖方向单一),有没有对称的勾(说明存在双向依赖)。这个表用表格软件十分钟能画完,比看几千行代码直观得多。
第二个办法更硬核一点:在构建脚本里加依赖检查。大多数语言生态都有现成的依赖分析工具,能在编译期阻断非法依赖。比如规定领域层不能引用基础设施层的包,一旦有人在领域层 import 了数据库驱动,构建直接失败。这种做法一开始会让团队很不适应,但坚持两周之后,边界就变成肌肉记忆了。
注意:依赖检查规则不要一次加太多。我建议第一版只加三条最要命的规则,跑顺了再逐步扩展。一次性加二十条,团队的第一反应是找办法绕过,而不是遵守。
3. 技术选型的取舍账本:不选最强的,选最扛得住的
技术选型是架构设计里争议最大的环节,因为每个人都有自己的偏好,而且偏好往往来自上一份工作的经验。我见过一个团队为了"技术先进性"选了某个刚发布半年的大数据组件,结果上线三个月遇到两次内存泄漏,社区里连相关 issue 都没几条,最后只能自己啃源码打补丁。
3.1 选型评估的五个维度与权重
我的做法是给每个候选技术打五个维度的分,权重按项目实际情况调。这套东西不新鲜,但真正填一遍,结论往往和直觉不一样。
| 维度 | 评估要点 | 中小项目建议权重 | 大型项目建议权重 |
|---|---|---|---|
| 功能匹配度 | 能否覆盖当前需求,缺口需要多少自研补 | 30% | 25% |
| 成熟度与稳定性 | 版本迭代历史、已知严重缺陷、社区活跃度 | 25% | 30% |
| 团队熟悉度 | 团队里有几个人能独立排障 | 25% | 20% |
| 运维成本 | 部署复杂度、监控接入难度、故障恢复手段 | 15% | 15% |
| 退出成本 | 用错了之后换掉的代价有多大 | 5% | 10% |
这张表里最关键的是最后一行——退出成本。很多选型讨论只谈"用起来爽不爽",不谈"用错了怎么办"。一个技术再优秀,如果数据格式是私有的、迁移工具不成熟,那你实际上是在和一个供应商绑定终身。我一般会问一句:"假设三个月后我们要换掉它,需要做什么?"如果答案是"数据要重写、业务代码要改三百处",那这个选择的权重就要往上调。
3.2 中间件与存储选型的对照表
具体到组件层面,我把常见的取舍整理成下面这张表。这里说的不是绝对结论,是决策依据。
| 场景 | 倾向选择 | 判断理由 | 什么时候该反过来选 |
|---|---|---|---|
| 事务型业务数据 | 关系型数据库 | 需要多表关联、强一致性、复杂查询 | 单表数据量过亿且查询模式极简时,考虑分片或列式存储 |
| 高频读取的配置/字典数据 | 进程内缓存 + 定期刷新 | 数据量小、变更不频繁,省掉一次网络往返 | 多实例之间要求秒级一致时,改用集中式缓存 |
| 异步任务与解耦 | 消息队列 | 削峰、解耦、失败重试有天然支持 | 任务量很小且无峰值时,直接同步调用更简单 |
| 全文检索 | 独立检索引擎 | 关系库的模糊查询在数据量上来后性能断崖 | 数据量在十万级以内且查询简单时,数据库索引足够了 |
| 大文件存储 | 对象存储 | 与业务库分离,避免备份和扩容互相牵连 | 文件数量极少且需要强事务关联时,可考虑库内存储 |
用这张表的时候有个原则:每一项"倾向选择"都要能对应到第 1 章采集的某条约束。如果对应不上,说明这个选择是凭感觉做的,要么补上依据,要么就选更简单的那个。架构设计里,"简单"本身就是一个被低估的优势——少一个组件,就少一处可能出故障的地方,少一套需要学习的运维知识。
3.3 被低估的"团队熟悉度"成本
这一条我要单独拎出来说,因为它太容易被忽略。
假设有两个方案,A 方案在性能上比 B 方案好 30%,但团队里只有一个人用过。B 方案性能一般,但团队一半人熟悉。选哪个?我的答案几乎总是 B。
原因很实际:性能差距是可以用机器弥补的(加节点、加缓存、加索引),但人的知识缺口没法用钱快速填平。线上出故障的时候是凌晨两点,那个唯一懂 A 方案的人如果联系不上,整个团队就只能干等。而且知识集中在一个人身上,本身就是组织风险。
我在一个项目里吃过这个亏。当时为了追求更优的写入性能,选了一个相对小众的存储方案,团队里没人有生产环境经验。上线后遇到一个写入放大的问题,排查了整整四天才定位到是某个配置项的默认值导致的。四天里业务侧不停催,最后是靠翻官方文档的一个角落才找到线索。后来复盘,如果当初选团队熟悉的那套方案,性能大概低 20%,但多花的机器成本折下来还不到那四天人力成本的三分之一。
选型的本质不是找最优解,是找在你这个团队、这个时间窗口内,综合代价最小的解。
4. 数据流与接口契约:架构里最容易含糊过去的部分
架构图画完之后,最容易糊弄的就是数据怎么流、接口怎么定。图上一个箭头,实际落到代码里可能是同步调用、异步消息、批量同步三种完全不同的东西,而这三种的错误处理、超时策略、重试语义全都不一样。箭头不标清楚,实现阶段必然各写各的。
4.1 端到端数据流的画法与检查点
我画数据流不看模块,看数据本身。挑一条最核心的业务链路(比如"用户下单到订单生效"),然后追踪这份数据从进入到落地经过的每一个环节,每个环节标注三件事:数据形态、处理动作、失败后的处置。
举个具体的,一次下单请求的流转大概是:接入层校验参数(形态:原始请求体;失败:直接返回错误码)→ 应用层编排(形态:业务对象;失败:回滚已执行的步骤)→ 领域层计算价格与库存(形态:领域对象;失败:抛出业务异常)→ 基础设施层写库(形态:持久化记录;失败:重试或进入补偿队列)→ 发消息通知下游(形态:消息体;失败:本地消息表 + 定时重投)。
这条链路上有三个检查点必须明确:数据在哪一步从"不可信"变成"可信"(通常是有一次完整校验的地方)、数据在哪一步产生副作用(写库、扣库存、发消息)、副作用之间的顺序能不能调换。最后这一问特别重要——如果"扣库存"和"发消息"的顺序调换会导致下游拿到一个不存在的订单,那就说明这个顺序是强约束,实现时不能随便优化。
4.2 接口契约的七个必填字段
接口定义我一般要求写满七项,缺一项就不算定完。
- 入参的完整结构,包括字段类型、是否必填、长度范围、枚举取值。
- 出参的完整结构,包括成功和失败两种形态。
- 错误码清单,每个错误码对应什么业务含义,调用方看到之后该做什么。
- 超时时间,以及超时后调用方的行为(重试还是放弃)。
- 幂等键,以及重复请求的返回语义(返回首次结果还是报错)。
- 数据量上限,单次请求最大条目数、最大报文体积。
- 版本策略,字段新增和废弃怎么处理,老版本调用方什么时候必须升级。
第三项和第五项是最常被跳过的。错误码不定义清楚,调用方只能靠 HTTP 状态码猜,最后所有错误都变成"系统异常,请稍后重试",用户看到这句话完全不知道该怎么办。幂等键不定义,重试就会变成重复下单、重复扣款,这类问题在真实业务里造成的事故一点都不少。
4.3 幂等、重试与补偿:契约里必须提前写死的三件事
这三件事必须在架构阶段就定好规则,不能留到编码时各自发挥。
幂等的核心是找到一个业务上唯一的标识。订单号、支付流水号、外部请求 ID 都可以,关键是要保证同一个业务动作重复提交时,这个标识不变。有了标识,服务端做"先查后写"或者"唯一索引兜底"都行。我倾向于两者都做——查询拦截挡住 99% 的重复,唯一索引兜住剩下的 1%。只靠查询拦截,在并发场景下是挡不住的。
重试的规则要写清楚三件事:重试几次、间隔多久、什么错误可以重试。我的默认设置是:网络超时和 5xx 类错误可以重试,业务参数错误不重试;重试不超过三次,间隔采用指数退避(比如 200ms、600ms、1.8s)。重试次数不能多,因为重试次数乘以调用量,就是故障时的放大倍数。下游已经扛不住了,你还猛重试,等于雪上加霜。
补偿是针对那些"做了一半"的场景。典型的如扣了库存但订单创建失败,这时候需要一个反向操作把库存加回去。补偿的设计要点是补偿动作本身也要幂等,而且要能重复执行——因为补偿也可能失败,失败了还得再补偿一次。
注意:补偿不是万能的。如果补偿逻辑本身有 bug,会造成数据越补越乱。所以我在项目里的原则是:能用一次本地事务解决的事情,绝不引入补偿;只有在跨系统、跨存储的情况下才用补偿,并且必须配一个对账机制定期核对。
5. 非功能性需求的量化:把"要快、要稳"变成可核算的数字
非功能性需求是架构设计里最像"玄学"的部分,因为它不像功能那样有明确的验收标准。但实际上它比功能更该被量化——因为架构方案的主要差异,几乎全部体现在性能、可用性、扩展性这些维度上。
5.1 容量估算的一个可复算的例子
容量估算不需要多精确,但必须能算。我给一个我自己常用的推演过程。
假设一个协作类产品,目标用户 20 万,日活按 10% 算是 2 万,人均每天打开 8 次,产生约 30 次接口请求。那么日请求量约 60 万次。业务高峰集中在上午十点和晚上八点这两个两小时窗口,假设 60% 的请求落在这 4 小时里,那高峰期的平均 QPS 大约是 36 万 / 14400 秒 = 25。再乘一个 3 倍的峰值系数(真实场景里瞬时峰值往往是均值的数倍),设计目标按 75 QPS 考虑。
75 QPS 是什么概念?单台普通配置的应用服务器,只要没有慢查询和外部调用阻塞,承载这个量级毫无压力。这个结论很重要,因为它直接告诉你:不需要做复杂的分布式设计,瓶颈根本不在应用层。
那瓶颈在哪?继续算。每次请求平均产生 3 条数据库操作,高峰就是 225 QPS 的数据库请求。如果单条查询平均耗时 5ms,那数据库的负载率大约是 225 × 0.005 = 1.125,也就是已经过载了。这时候优化方向就很明确:要么减少查询次数(合并查询、加缓存),要么优化单次查询(加索引、减少回表),要么读写分离。
你看,同一个业务,算完之后会发现真正的瓶颈和直觉常常不一致。不做估算直接上架构方案,等于闭着眼睛开药方。
5.2 性能预算的分配方法
总量算出来之后,要往下分配。我的习惯是给一次端到端请求设一个总预算,然后按环节切分。
假设接口的响应时间目标是 300ms,那可以这么分:网络传输 30ms,接入层处理 10ms,应用层编排 20ms,领域逻辑 50ms,数据库访问 150ms,序列化与返回 40ms。留出的余量不算多,但够用。
这个分配表的价值在于,当某个环节超支时你能立刻知道是谁的问题。比如实测发现数据库访问用了 280ms,那不用怀疑,去看慢查询日志就行,而不是在应用层加一堆缓存来掩盖问题。没有预算分配的性能优化,最后都会变成到处加缓存,而缓存带来的数据一致性问题,往往比原来的性能问题更难处理。
5.3 可用性目标与降级策略的对应关系
可用性目标不是喊口号,它直接决定了你要准备几套方案。经验上的对应关系大致是这样:
| 可用性目标 | 年停机时间量级 | 需要的能力 |
|---|---|---|
| 99% | 约 3.6 天 | 基础监控、快速重启、定期备份 |
| 99.9% | 约 8.7 小时 | 多实例部署、健康检查、自动摘除 |
| 99.95% | 约 4.4 小时 | 跨可用区部署、数据实时同步、故障自动切换 |
| 99.99% | 约 52 分钟 | 多活架构、流量调度、故障演练常态化 |
看清这张表很重要,因为每往上走一档,成本和复杂度都是非线性上升的。一个内部使用的运营后台,非要做到 99.99%,这是在浪费资源;而一个承载核心收入的交易链路只做到 99.9%,那就是在赌运气。
降级策略要和可用性目标配套设计。比如目标是 99.95%,那就必须明确:当依赖的某个外部服务不可用时,主流程是阻塞等待、返回兜底数据,还是直接跳过这个环节?这三种选择对用户体验和数据正确性的影响完全不同,必须提前定,不能等故障发生了临时拍脑袋。
6. 从图纸到代码:架构落地那段最容易被忽略的灰色地带
架构文档写完、评审通过,很多人就觉得工作完成了。但从文档到代码之间,还有一段很长的路,这段路上如果没人管,架构会在三个月内自然退化成一锅粥。
6.1 架构决策记录的写法
我的做法是给每个重要决策写一条记录,格式很短,但必须写全五项:
- 决策内容:我们决定做什么。
- 背景约束:当时的哪些条件促成了这个决定。
- 备选方案:还考虑过什么,为什么没选。
- 预期后果:这个决定会带来什么好处,以及接受什么代价。
- 失效条件:什么情况下这个决定需要重新评估。
最后一项是最有价值的,也是最容易被漏掉的。比如"选择单体部署"这条决策,失效条件可以写"当团队规模超过 15 人且发布冲突每周超过 3 次时重新评估"。写清楚失效条件,这个决策就有了生命周期,而不是变成一个谁也不敢动的祖传约定。
我见过一个项目,三年前因为"团队只有三个人"选了单体,三年后团队三十人了还在单体里挣扎,问为什么不分拆,回答是"当初架构评审定的就是单体"。如果当初写了失效条件,这个决策早就该被拿出来重新讨论了。
6.2 防腐层与适配层的实际落地
架构里说要隔离外部依赖,落到代码上就是适配层。但很多项目的适配层形同虚设——名字叫 Adapter,里面直接调第三方 SDK,返回的还是第三方的对象。这种隔离等于没做,因为第三方一升级,业务代码还是得跟着改。
真正的隔离要做到两点:入参出参是自己的数据结构,第三方对象在适配层内部就转换掉;异常也转换,第三方的错误码统一映射成自己的业务异常。这两点做到之后,换供应商的改动范围就被限制在适配层的几个类里,不会外溢。
防腐层则用在新旧系统并存的场景。老系统的数据模型往往很混乱,直接让新代码去读,等于把历史包袱继承过来。做法是在中间加一层转换,把老模型翻译成新模型,让新系统看到的是干净的数据。这层转换代码写起来有点枯燥,但它是新旧系统能够长期共存的前提。
6.3 灰度与开关:让架构变更可控地生效
架构层面的改动,风险远大于功能改动,因为它影响的是全局。所以必须有能力"只让一部分流量走新路径"。
最基础的手段是功能开关。在配置中心放一个开关,控制某段逻辑走新实现还是旧实现。开关的粒度要足够细,能精确到"某个租户""某个渠道""某个百分比"。但开关不能无限加,我一般要求每个开关在文档里登记:谁加的、为什么加、计划什么时候删。没有删除计划的开关,最后会变成永久的技术债,代码里到处是 if-else,谁也不敢删。
再往上一层是流量灰度。按用户 ID 哈希或者按请求头标记,把 1%、5%、20%、50%、100% 的流量逐步切到新路径,每一档观察足够长的时间。这里的关键是观察指标要提前定好,不能切完了才想"我该看什么"。至少要盯:错误率、关键接口耗时、核心业务转化指标。前两个是技术指标,第三个是业务指标——有时候技术指标全绿,但业务指标掉了,说明新逻辑在语义上有问题。
7. 几个反复出现的坑,以及我现在的处理手法
架构设计这门手艺,进步主要靠踩坑。下面这几个是我在不同项目里反复遇到的,写出来供参考。
7.1 过早抽象:三层变五层,代码量翻倍
有个项目一开始就设计了"接入层—应用层—领域层—仓储层—基础设施层"五层,理由是"以后可能要拆微服务,先分好"。结果一个简单的查询功能,要写五个类、跨五个包。三个月后团队开始抱怨"改一行代码要翻八个文件",然后有人开始抄近路,直接在接入层写业务逻辑。半年后,五层变成了"名义五层,实际两层",而且比一开始就写两层更乱。
我现在的做法是:默认从最少的层开始,只在真正需要的地方分层。什么时候加层?当一段逻辑被三个以上地方复用时,或者当它需要被独立测试时。抽象是对重复的回应,不是对未知的预防。
7.2 把缓存当数据库用
缓存能解决性能问题,这个说法没错,但有个前提:缓存里丢了数据,业务还能从别处恢复。一旦这个前提不成立,缓存就从加速器变成了唯一数据源,风险指数级上升。
我遇到过的典型场景是:把会话状态、临时计算结果、甚至部分业务计数放在缓存里,并设置较长的过期时间,但没有持久化。结果缓存节点重启,数据全丢,用户登录态全部失效,业务计数归零。更麻烦的是,这些数据没有任何地方可以还原。
现在的原则很简单:缓存只放"丢了能重建"的数据。凡是不满足这条的,要么进数据库,要么做持久化,没有第三条路。
7.3 分布式事务用得太随意
微服务拆开之后,跨服务的数据一致性成了绕不过去的问题。很多人的第一反应是上分布式事务框架,觉得"专业的事交给专业的组件"。但分布式事务的代价被严重低估了——它带来的性能损耗、实现复杂度、故障排查难度,往往超过它解决的问题。
我现在的判断顺序是:先问这个一致性要求是不是真的强一致。绝大多数业务场景其实是最终一致就够了,比如"下单后通知库存扣减",中间差个几百毫秒完全无感,用本地消息表 + 重试就能解决,简单可靠。只有当"两个操作必须同时成功或同时失败,且不允许任何中间状态被外部观察到"时,才值得考虑分布式事务。而这种情况在真实业务里其实很少——大部分所谓的强一致需求,追根究底是业务方没想清楚中间状态该怎么处理。
7.4 架构文档和代码脱节
这个问题几乎每个项目都有,只是程度不同。文档写完就没人更新,三个月后代码已经改了五轮,文档还停留在最初的样子。新人来了看文档,按文档理解代码,然后彻底懵掉。
我的处理办法是把文档"贴"在代码旁边,而不是放在一个独立的文档系统里。架构说明、决策记录、关键约束,全部以注释或者 Markdown 文件的形式放在代码库里,跟代码一起提交、一起评审。这样至少能保证:改了代码的人看得到文档,评审的人会发现文档没同步。放在外部系统里的文档,改代码的人根本不会想起来去更新它。
另外一个技巧是把能自动检查的约束写成脚本。比如"领域层不能依赖基础设施层"这条,写成构建时的检查规则,比写在文档里有效一百倍。文档靠自觉,脚本靠机器,后者的可靠性高得多。
这四条坑有一个共同点:都是在"当时看起来合理"的情况下发生的。过早抽象是为了未来扩展,缓存当库是为了性能,分布式事务是为了正确性,文档独立存放是为了"统一管理"。每一个决策在当时都有正当理由,问题出在没有配套的约束条件和退出机制。
我个人在实际操作中的体会是,架构设计的功夫,一半在"设计",另一半在"给自己留后路"——每做一个决定,都想清楚它什么时候该被推翻,推翻的代价有多大。想清楚这两点,架构就从一个静态的图纸,变成了一个能跟着业务一起长的东西。