单体在坊间被误读为"又一个巨石",微服务则被当成万能解药。但现实是:绝大多数团队真正需要的,是模块化单体(Modular Monolith)——一个可独立演进、边界清晰、部署却仍是一个进程的架构。本文讲清楚它是什么、不是微服务的原因,以及如何把模块边界砌漂亮。
1. 引言:为什么微服务之前应该先考虑模块化单体
微服务不是起点
微服务能带来独立部署与弹性,也带来分布式系统的一切麻烦:网络故障、数据一致、链路追踪、版本兼容。复杂度不会消失,只会从代码转移到底层设施。
对大多数业务,单体交付最快、最稳。而单体的最大问题不是"技术",而是模块边界崩塌——互相 import、共享大对象、牵一发动全身。模块化单体正好补上这一课:保留单体的部署简单性,引入微服务的模块边界纪律。
模块化单体定义
一个进程/一个大仓库,内部按业务领域切成清晰模块,
模块间通过显式接口通信,禁止任意跨模块访问内部细节。
一句话:微服务是"把模块分到进程",模块化单体是"把模块纪律放进一个进程"。先学会边界,再谈拆分进程。
2. 模块边界:限界上下文的落地
2.1 以 DDD 限界上下文为边界
模块最自然的边界是 DDD 的限界上下文(Bounded Context):订单、库存、支付、用户……每个上下文内部高内聚,上下文之间通过显式接口往来。
订单模块 ──(下单事件/接口)──→ 库存模块
│ │
└──(支付命令)──→ 支付模块
2.2 明确禁止的依赖
- 禁止跨模块直接访问另一模块的内部类/数据表;
- 禁止跨模块
new对方的实现类,只能调公开接口; - 禁止模块间共享"大杂烩"工具类导致隐性耦合。
2.3 防倾倒依赖:防腐
跨模块协作时,用接口隔离内部表征。例如订单模块要通知库存扣减,只暴露 StockService.markReserved(orderId, sku, qty),不暴露库存内部模型。
一句话:模块边界 = 限界上下文 + 显式接口 + 禁止内部访问;边界画得好,模块才是"可插拔的零件"而不是"互相缠绕的线团"。
3. 模块内的结构与依赖治理
3.1 模块内部分层
模块(如 Order)
├── api/ 对外接口(DTO、门面)
├── domain/ 领域逻辑(实体、服务、策略)
├── application/ 用例编排(应用服务)
└── infra/ 实现(仓储、外部调用)
对外可见的只有 api/,其余对模块外部不可见(包私有/模块私有)。
3.2 依赖方向约束
- 单向依赖:模块 A 依赖模块 B,则 B 不得反向依赖 A;
- 禁止环:模块间成环会让"谁能改谁"说不清,用架构测试(ArchUnit 等)在 CI 强制;
- 依赖倒置:跨模块调用依赖接口,具体实现留在各自模块。
3.3 结构可视化
用依赖图检查工具(如 JDepend/ArchUnit/ndepend)把模块依赖画出来——环与多层依赖会立刻现形,并作为 CI 质量门禁。
一句话:模块内部分层、模块之间单向依赖、用架构测试守门——这是模块化单体不腐化的三条支腿。
4. 数据层的单边界:模块各自建表
4.1 一份 Schema,多模块共库不共表
模块化单体通常共用一个数据库实例,但各模块拥有自己的表集合:
单独库: 用户库 / 订单库 / 库存库(强隔离,重)
共享库分表:db 同一库,order 模块只管 order_*表
推荐共享库 + 各模块私有表组 + 明确命名前缀:
- 订单模块只碰
order_前缀的库表; - 存根:库存模块从"读自己的表",不读订单模块的表;
- 跨模块数据需求一律走接口,不许直连对方表。
4.2 事务的边界与妥协
单体共享库带来一个好处:跨模块一个本地事务能做到。但也容易被滥用成"巨事务"。
取舍:
强一致的跨模块写 → 一体事务(简单,但耦合)
可最终一致的写 → 模块内事务 + 消息/事件(边界更清晰)
4.3 分库边界先行
如果确定未来会拆分,现在就按模块开设独立 Schema/独立库,模块化单体到微服务时数据层基本不动。
一句话:数据层也要守模块边界——共库不共表、命名前缀隔离、跨模块靠接口而不是 SQL join。
5. 模块间通信:接口与事件的取舍
5.1 同步接口
模块间同步调用(RPC/本地方法)适合请求-响应、强实时的场景;代价是调用方持有被调模块的引用,耦合加深,但胜在简单直接。
订单服务 → 同步调用 → 支付模块.submit()
5.2 异步事件
模块间发布领域事件(订单已创建 → 库存模块监听扣库存)。解耦最好,但引入了最终一致性:
// 事件发布(伪)
orderModule.publish('order.placed', { orderId, items });
// 库存模块订阅
inventoryModule.on('order.placed', async (e) => { await reserve(e.items); });
| 维度 | 同步接口 | 异步事件 |
|---|---|---|
| 耦合度 | 高 | 低 |
| 实时性 | 实时 | 有延迟 |
| 一致性 | 强 | 最终一致 |
| 排障复杂度 | 低 | 高 |
设计原则:业务边界紧密、需强一致的用同步;对实时性不敏感、想解耦的用事件。演进到微服务时,模块间的接口与会话能直接映射为服务间远程调用。
6. 部署形态:一体进程,模块可独立演进
6.1 一个大部署,多模块独立发布
模块化单体仍是一个进程、一条部署流水线,但模块内部 3 日能独立演进(加字段、改接口)不影响全局。
好处:
- 部署一个
jar/镜像即完成,运维简单; - 不需要分布式的服务发现、网关、链路追踪(尚不需);
- 一个请求穿越模块,本地调试、断点、单元测试都顺手。
6.2 演进到微服务的准备
当年级增长使"独立部署的独立团队协作"成为瓶颈时,再考虑分进程:
模块化单体 → 把"最热/最独立"的模块拆成独立服务
→ 拆的边界 = 模块边界(已验证)
→ 数据随模块迁移
模块化单体的最大价值,就是让未来拆分"沿模块边界、干净利落",而不是推倒重来。
一句话:模块化单体先把"边界与依赖"做对,后面要分按模块边界切就是了——它是某种意义上的"微服务的地基预习"。
7. 何时不该用 / 何时必须转向微服务
| 不构成用模块单体的信号 | 应该转向微服务的信号 |
|---|---|
| 项目 < 3 个领域的完整体 | 组织 > 10 个团队并自用发布节奏 |
| 团队 < 2 个 | 模块要独立扩容/弹性伸缩 |
| 尚无明确领域边界 | 模块间异构技术栈(多语言) |
| 单体已 BE | 需要独立部署不同团队的发布窗口 |
一句话:别因为"听说单体坏"就不敢用模块化单体。简单场景它是更负责任的默认;等团队、规模真正触发需求,微服务才入场。
8. 踩坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| 无边界模块 | 互相 import、缠成团 | 用限界上下文切模块 + 架构测试 |
| 共享大工具类 | 隐性耦合 | 工具类按模块拆分/就近放置 |
| 跨模块直连他人表 | 数据耦合 | 只走公开接口 + 命名前缀隔离 |
| 慕求进程式微服务 | 没分工先扛分布式成本 | 先模块化单体打磨模块边界 |
| 模块间成环 | 无法独立演进 | 依赖方向治理 + ArchUnit 拦截 |
| 过度拆数据库 | 部署至少两个 | 共享库 + 模块表组即可 |
9. 总结
| 环节 | 要点 |
|---|---|
| 是什么 | 一个部署,内部按限界上下文切模块 |
| 边界 | 限界上下文 + 显式接口 + 防腐 |
| 依赖 | 单向、无环、依赖倒置、架构测试 |
| 数据 | 共库不共表、命名前缀、接口通信 |
| 通信 | 同步接口(强) / 异步事件(解耦) |
| 演进 | 沿模块边界平滑拆微服务 |
一句话记住:模块化单体是"进可微服务、退可单体"的中间态——它把模块边界、依赖、数据隔离这些核心能力在一个进程里练扎实,是企业级架构最被低估、性价比最高的第一步。
延伸阅读
- 单体应用 vs 微服务 — 拆分演进策略与利弊分析
- 领域驱动设计(DDD) — 限界上下文与聚合建模
- CQRS 与事件溯源 — 读写分离与领域事件
- 架构决策记录(ADR) — 记录模块边界的决策因果
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。