27.5 单体内部的演进

一句话

单体内部的演进顺序是固定的:先分包(可读)→ 再立边界(可校验)→ 再拆数据(可独立)→ 最后才谈拆进程;跳过前三步直接拆服务,等于把泥球装进容器。

四个阶段与退出标准

阶段 做什么 退出标准 常见错误
① 分包 按业务能力重组目录 新人能按目录找到 80% 的代码 按技术层(service/dao)切
② 立边界 加依赖规则、定义模块 API CI 能阻止跨模块内部依赖 规则只写在文档
③ 拆数据 每模块独立 schema/表前缀 改表只影响一个模块 共享表 + 视图绕过
④ 拆进程 只拆 “有独立伸缩/发布/隔离需求” 的模块 该模块可独立部署且回滚 按模块数量平均拆

代码:从 “直接调用” 到 “接口 + 实现”

// 演进前:订单模块直接依赖用户模块的具体实现
func (s *OrderService) Create(uid int64) error {
    u := userRepo.Get(uid)          // 跨模块内部调用,边界被击穿
    return s.repo.Insert(u.ID)
}

// 演进后:订单只依赖自己定义的接口(依赖倒置),实现由装配层注入
type UserReader interface { Get(ctx context.Context, id int64) (User, error) }

func (s *OrderService) Create(ctx context.Context, uid int64) error {
    u, err := s.users.Get(ctx, uid) // 面向接口,可替换、可 mock
    if err != nil { return err }
    return s.repo.Insert(ctx, u.ID)
}

实测:用 “退出标准” 验收演进,而不是靠感觉

演进到阶段 ③(拆数据/立边界)后,用同一脚本复测:

退出标准 修复前 修复后 是否达标
跨模块 internal 违规 1 0 ✅
模块级循环依赖 1 0 ✅
最高扇入 2 下降(改为依赖公开接口) ✅
单次变更影响模块数 2–3 1–2 ✅

这就是 “可验证的退出标准”:每个阶段都用同一组指标复测,达标才进入下一阶段。没有这套验收,“演进” 很容易停在 “目录看起来很整齐” 的中间态。

反模式

反模式 后果
把 “拆服务” 当演进第一步 分布式复杂度先到,边界问题没解决
只做 ①②、不做 ③ 数据仍共享,独立发布无从谈起
按模块数平均拆进程 拆出一堆 “必须一起发” 的服务
演进没有度量 无法判断 “是否变好”,也无法说服管理层
演进期间冻结所有需求 业务不可接受,最终半途而废

| 演进按“时间”推进而不是按“指标”推进 | 到点就宣布完成,实际耦合没降 | | 只测一次不复测 | 新代码又把环加回来,无人察觉 |

两条铁律

🔑 每一步都要有可验证的退出标准;没有标准就会停在 “看起来整齐” 的中间态。

🔑 只有 “独立伸缩/发布/故障隔离” 三者之一成立,才值得拆进程。

自检

Q1 为什么 “依赖倒置” 是拆进程前的必要准备?

▸ 答案:因为拆进程本质是把进程内调用换成跨进程调用:只有当调用方依赖的是自己定义的接口时,替换实现(本地 → 远程)才不需要改业务代码。直接依赖具体实现时,拆进程等于全量重写。

Q2 “按模块数平均拆” 为什么错?

▸ 答案:因为拆分的依据是变更与伸缩的独立性,不是数量。平均拆会把 “总是一起改” 的模块拆成两个服务,导致每次需求都要双发、双回滚,协作成本上升而收益为零。

Q3 演进到阶段 ③ 时,如何避免 “用视图绕过数据隔离”?

▸ 答案:把视图也纳入依赖规则:跨模块只能读对方暴露的 API/只读物化视图,且该视图由对方 owner 维护。绕过检查的 JOIN 要在评审中显式拒绝,否则数据边界会被 “临时需求” 反复打穿。

Q4 怎么衡量演进是否有效?

▸ 答案:三个指标:单次变更影响模块数(应下降)、循环依赖数(应为 0)、独立发布的服务占比(应上升)。指标不动而结构大改,说明在做 “形式重构”。

Q5 为什么 “阶段 ②(立边界)” 不能跳过?

▸ 答案:因为没有边界时,阶段 ③(拆数据)会立刻失败:你无法判断 “哪些表属于哪个模块”,也挡不住别的模块直接读写。边界规则是拆数据的前提条件——它先回答 “谁拥有什么”,数据归属才有依据。

Q6 演进过程中,如何避免 “为了达标而做形式化重构”?

▸ 答案:把每个阶段的退出标准写成可自动检查的指标(违规数、环数、扇入、影响模块数),并在 CI 里持续跑。指标不降就不是重构,只是搬代码。另一条纪律:每次重构都必须绑定一个真实的业务痛点(发布慢/改一处坏三处),否则容易被当成 “技术自嗨” 而失去支持。

Q7 演进到 “拆进程” 前,还有哪条指标必须看?

▸ 答案:独立发布能力:该模块能否在不发布其他模块的情况下上线、回滚。它无法靠静态分析得出,要做一次真实演练(改一行、只发这个模块、观察其他模块是否受影响)。演练通过,拆进程才有意义;否则拆出来仍是 “必须一起发” 的分布式单体。

故障复盘(典型模式,脱敏)

阶段 现象 判断 动作
拆服务后 订单服务与库存服务必须同版本发布 数据仍共享一张表 拆 schema
灰度 无法只发一个服务,回滚要双回滚 独立发布能力未验证 演练单发
演练 单发订单服务导致库存读不到数据 缺少契约与兼容层 加双写+兼容读
达标后 可单发、可单回滚 退出标准通过 继续拆下一个

根因:跳过 “拆数据” 与 “独立发布演练” 直接拆进程。代价:两个月无效拆分成分布式单体。根治:每个阶段用同一组指标复测(违规 0、环 0、单发通过)再进入下一阶段。

关联