☰
模块化单体 + DDD + 绞杀式迁移:从单体到微服务的渐进式演进
2026/10/11 1:45:39 网站建设 项目流程

模块化单体 + DDD + 绞杀式迁移:从单体到微服务的渐进式演进

一句话结论:模块化单体是“一个可部署单元 + 内部强边界”的架构,DDD 负责识别边界,绞杀式迁移负责在不重写系统的前提下逐步把模块抽成微服务。三者的关系是:DDD 定义“切哪里”,模块化单体提供“切得动”的代码结构,绞杀式迁移给出“安全切出去”的路径。

一、为什么需要模块化单体:先看传统单体的困境

1.1 传统单体的典型问题

❌ 传统单体(大泥球) ├── OrderService.cs ← 2万行,什么都往里塞 ├── CustomerService.cs ← 直接 new SqlConnection ├── InventoryService.cs ← 调用 OrderService 的私有方法 └── Controllers/ ← 直接操作数据库

典型症状:边界侵蚀(模块互相 reach-through)、共享数据库耦合(跨模块 JOIN)、变成大泥球(只是按文件夹分类,没有真正的模块)。

1.2 模块化单体的定位

模块化单体是“一个可部署单元,内部拆成强边界模块”的架构。它提供微服务级别的组织清晰度,但不承担分布式系统的成本(网络失败、最终一致性、运维开销)。

维度传统单体模块化单体微服务
部署单元1个1个N个
模块边界文件夹强强制边界网络边界
数据库共享共享(可 schema 隔离)独立
重构成本低中(边界约束)高(跨服务)
分布式复杂度无无高

适用场景:小到中型团队、领域边界仍在探索中、希望保持微服务就绪的接缝但暂时不想支付分布式税。

二、DDD 如何定义模块边界

2.1 战略设计:限界上下文 → 模块

DDD 战略设计的第一步是划分限界上下文(Bounded Context)——一个领域内特定模型适用的边界。在模块化单体中,每个限界上下文对应一个模块。

以电商系统为例:

限界上下文划分: ├── 订单上下文 (Order BC) → OrderModule ├── 支付上下文 (Payment BC) → PaymentModule ├── 库存上下文 (Inventory BC) → InventoryModule └── 客户上下文 (Customer BC) → CustomerModule

同一个词在不同上下文中含义不同:在订单上下文中,“商品”是购买快照(名称、价格、数量);在库存上下文中,“商品”是可量化和预留的库存单元。这正是限界上下文存在的意义——每个上下文有自己的模型,不强行统一。

2.2 战术设计:模块内部的领域模型

每个模块内部应用 DDD 战术模式:

模式说明示例
实体有唯一标识,随时间持久Order(订单号不变,状态可改)
值对象无标识,属性定义,不可变Address、Money、OrderItem
聚合一致性边界,一个聚合根Order是根,OrderItem只能通过 Order 修改
领域事件记录已发生的事,用于解耦OrderPlaced、PaymentCompleted
仓储聚合的持久化接口(定义在领域层)IOrderRepository

核心原则:实体应封装行为,而非仅传数据。如果业务逻辑在 Service 层而非实体中,就是贫血模型反模式。

三、模块化单体的代码结构与强制边界

3.1 分包结构(facade + internal 模式)

Spring Modulith 推荐的结构:只有基础包中的类型对其他模块可见,internal/下的全部隐藏。

com.example.app.{module}/ ├── {Module}Service.kt # 公开门面(跨模块 API,只返回 DTO) ├── {PublicDto}.kt # 公开 DTO ├── events/ # 领域事件(公开) │ └── {DomainEvent}.kt └── internal/ # 全部私有 ├── model/ # 实体、值对象 ├── repository/ # 仓储实现 ├── application/ # 内部服务 └── controller/ # REST 控制器

关键规则:orders模块可以依赖inventory :: events(只依赖事件子包),但不能依赖inventory的完整 API。

3.2 边界强制手段

模块化单体的最大风险是边界侵蚀——模块互相 reach-through。必须用架构测试强制边界:

// ArchUnitNET 示例:禁止跨模块直接访问 internal[Fact]publicvoidOrderModule_ShouldNotAccess_InventoryInternal(){varresult=Types.InAssembly(typeof(OrderService).Assembly).That().ResideInNamespace("App.Order").ShouldNot().HaveDependencyOn("App.Inventory.Internal").GetResult();result.IsSuccessful.Should().BeTrue();}

3.3 模块间通信:契约 + 事件

模块之间不能直接调用对方的内部服务或访问数据库。通信方式:

模块间通信规则: ├── 同步查询 → 通过公开的 Facade 接口(返回 DTO) ├── 状态变更 → 通过领域事件(进程内事件总线) └── 禁止:跨模块 JOIN、直接引用对方实体、调用对方 internal 方法

进程内事件总线(如 MediatR / EventEmitter)在模块化单体中充当“微型消息代理”,让模块间通信像微服务一样,但零网络开销。这为后续绞杀式迁移铺路——把进程内事件换成 Kafka 消息,边界不动。

四、绞杀式迁移:从模块化单体到微服务的路径

4.1 绞杀植物模式的启示

“绞杀植物”的生态过程:种子被鸟带到寄主树顶 → 空中发芽 → 气根下垂入土 → 网状根系包裹寄主 → 寄主枯死 → 绞杀植物独立成树。

软件映射:

  • 寄主树= 旧单体/遗留系统
  • 绞杀植物种子= 新功能或新模块
  • 气根入土= 新模块逐渐接管流量
  • 寄主枯死= 旧功能被完全替换
  • 独立成树= 新模块独立部署为微服务

4.2 在模块化单体上实施绞杀式迁移的完整步骤

前置条件:你已经有一个模块化单体,模块边界清晰,模块间通过契约和事件通信。

步骤 1:选定绞杀目标

从模块化单体中选一个边界清晰、依赖少、有独立伸缩需求的模块。优先选择:

  • 对伸缩有独立需求的模块(如 AI 推理需要 GPU)
  • 变更频率高、影响面大的模块
  • 技术栈不同的模块(如 Python AI 服务)

步骤 2:将进程内事件替换为跨进程消息

模块化单体中,模块通过进程内事件总线通信。迁移第一步:把该模块的入站/出站事件通过消息中间件(Kafka/RabbitMQ)桥接。

// 迁移前:进程内事件eventBus.Publish(newOrderPlaced(orderId));// 迁移后:进程内事件 + Kafka 桥接eventBus.Publish(newOrderPlaced(orderId));// 本地继续kafkaProducer.SendAsync(newOrderPlaced(orderId));// 同时发到 Kafka

这样新模块可以订阅 Kafka 事件,而旧模块仍然处理本地事件。两者并行运行。

步骤 3:新模块独立部署,接管部分流量

新模块作为独立服务部署,通过网关或特性开关逐步接管流量:

流量切换策略(绞杀式): ├── 阶段 1:新模块只接收 1% 流量(影子模式) ├── 阶段 2:新模块接收 10% 流量(金丝雀) ├── 阶段 3:新模块接收 50% 流量 ├── 阶段 4:新模块接收 100%,旧模块停写 └── 阶段 5:移除旧模块代码

步骤 4:数据拆分(最困难的一步)

模块化单体通常共享数据库。绞杀式迁移的最大障碍是数据耦合。策略:

  1. Schema 分离:先把该模块的表移到独立 schema,禁止跨 schema JOIN
  2. 读写分离:新模块写自己的库,旧模块读旧库,通过事件同步
  3. 双写过渡:新模块写新库的同时,通过事件让旧模块更新旧库(反之亦然)
  4. 最终一致:接受短暂不一致,用补偿/对账机制兜底

步骤 5:移除旧模块

当新模块完全接管,旧模块的代码、数据库表、配置全部移除。绞杀完成。

五、完整案例:ITS M 系统的渐进式演进

5.1 背景

一个 ITSM(IT 服务管理)系统,Go 单体应用,包含80+ 服务和 80+ 控制器。面临的问题:

  • 部署不灵活:AI 推理需要 GPU,但整个单体一起部署
  • 数据库耦合:所有模块共享 PostgreSQL,大租户场景性能瓶颈
  • AI 紧耦合:Python AI 服务与 Go 后端紧耦合,无法独立扩展
  • 测试困难:全量测试耗时长

5.2 决策:模块化单体 + 事件驱动中台

不直接拆微服务,而是先采用“模块化单体 + 事件驱动中台”的渐进式架构:

itsm-backend/ ├── domain/ │ ├── ticket/ # 工单领域(模块) │ │ ├── model/ │ │ ├── service/ │ │ └── repository/ │ ├── incident/ # 事件领域 │ ├── problem/ # 问题领域 │ ├── change/ # 变更领域 │ └── common/ # 公共领域(共享内核) ├── infrastructure/ │ ├── event/ # 事件总线实现(Kafka) │ ├── cache/ │ └── storage/ └── api/ # API 层

关键决策:

  1. 模块化单体结构:保持单部署单元,按业务领域划分子包
  2. 事件驱动解耦:引入 Kafka,定义工单/审批/SLA/通知事件
  3. AI 服务独立化:Python AI 服务独立部署,通过 HTTP/gRPC 通信
  4. 多租户强化:租户上下文贯穿全链路

5.3 实施计划

阶段内容时间
Phase 1搭建 Kafka、定义事件 schema、实现事件总线2 周
Phase 2按领域重组代码包、关键逻辑改事件驱动4 周
Phase 3AI 服务 Docker 化、定义 Go-Python 协议2 周

5.4 效果

正面:

  • 保留现有开发/调试体验,无需大幅重构
  • 支持独立扩展 AI 服务(GPU 按需)
  • 事件驱动降低模块耦合,便于后续拆分
  • 多租户能力为 SaaS 化奠定基础

负面:

  • 需要引入 Kafka 等消息基础设施
  • 事件一致性需要额外处理(幂等、补偿)
  • 团队扩大后需评估是否进一步拆分为微服务

六、关键设计原则清单

模块化单体原则

原则说明验证方式
显式接口模块通过公开 Facade 暴露能力,返回 DTO架构测试禁止跨模块引用 internal
Schema 隔离每模块独立 schema,禁止跨 schema JOIN数据库审计
事件通信模块间状态变更通过领域事件解耦检查是否有跨模块直接调用
高内聚低耦合模块内强内聚,模块间弱依赖依赖数量分析

DDD 边界原则

原则说明
一个上下文一个模型不强行统一不同上下文的概念
聚合是事务边界一个事务只修改一个聚合
值对象优先默认用值对象建模,仅在需跟踪身份时用实体
实体封装行为避免贫血模型,业务规则放在实体内

绞杀式迁移原则

原则说明
不重写新模块逐步接管,旧系统继续运行
事件桥接进程内事件 → 跨进程消息,边界不动
流量渐进影子 → 金丝雀 → 全量
数据最后拆代码边界先拆,数据边界后拆

七、三者的关系总览

┌─────────────────────────────────────────────────────────┐ │ 演进路线 │ │ │ │ 传统单体 模块化单体 微服务 │ │ (大泥球) → (强边界模块) → (独立服务) │ │ │ │ ↑ ↑ ↑ │ │ │ │ │ │ │ DDD 战略设计 DDD 战术设计 绞杀式迁移 │ │ 划分子域 设计聚合/事件 逐步抽出 │ │ │ │ 关键:模块化单体是"过渡态",不是"终态" │ │ DDD 保证边界质量,绞杀保证迁移安全 │ └─────────────────────────────────────────────────────────┘

最终心法:

  1. 不要过早微服务——先用模块化单体验证边界
  2. DDD 是边界工具——限界上下文决定模块怎么切
  3. 绞杀是迁移纪律——永远保持系统可运行,逐步替换
  4. 模块化单体是微服务的准备阶段——边界清晰的模块化单体,拆微服务只是“把进程内调用换成 HTTP/gRPC + 把进程内事件换成 Kafka”

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询