土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界

文章导航

分类入口
architecture
标签入口
#DDD#microservices#service-boundary#Team-Topologies#Conway-Law

目录

某电商团队按数据库表拆分微服务——用户服务管 t_user,商品服务管 t_product,订单服务管 t_order。看起来边界清晰,实际运行中却发现:下单需要同步调用商品服务查价格、调用库存服务检查库存、调用优惠服务算折扣、调用用户服务查地址,一个下单请求扇出 4 次 RPC,任意一个服务超时整条链路就失败。这种”一实体一服务”的拆分方式,本质上把单体的函数调用变成了网络调用,所有的缺点都放大了,优点一个也没得到。

微服务边界的正确划分,需要回到业务本身——用限界上下文(Bounded Context)而非数据表来定义服务边界。本文讨论如何将 DDD 的战略设计成果转化为微服务架构决策,覆盖服务拆分的启发式规则、Team Topologies 的团队组织方法,以及领域驱动的服务演进策略。

上一篇:CQRS + Event Sourcing 完整实战 | 下一篇:认证架构


一、“一实体一服务”为什么失败

1.1 数据驱动拆分的问题

按数据表拆分微服务的思路来源于关系数据库的规范化理论——每个实体一张表,每张表一个服务。但这种方式忽略了一个关键事实:业务操作几乎总是跨越多个实体

下单操作涉及的实体:
  用户 → 商品 → 库存 → 优惠 → 订单 → 支付

按实体拆分后的调用链:
  订单服务 ──RPC──→ 用户服务
           ──RPC──→ 商品服务
           ──RPC──→ 库存服务
           ──RPC──→ 优惠服务
           ──RPC──→ 支付服务

问题:
  - 5 次 RPC,延迟叠加
  - 分布式事务
  - 任一服务故障导致整体不可用

1.2 对比:限界上下文驱动的拆分

交易上下文(Trading Context):
  负责下单的完整业务流程
  内部持有商品快照、价格计算、库存预检
  只对外发布 OrderPlacedEvent

支付上下文(Payment Context):
  订阅 OrderPlacedEvent
  独立处理支付流程
  发布 PaymentCompletedEvent

物流上下文(Shipping Context):
  订阅 PaymentCompletedEvent
  独立处理配送流程

交易上下文内部的”下单”操作不需要任何 RPC 调用——它拥有完成这个操作所需的全部数据。


二、限界上下文作为服务边界

2.1 核心原则

微服务的边界应该对齐限界上下文的边界。一个限界上下文对应一个或一组微服务(而非一个实体对应一个微服务)。

graph TB
    subgraph "错误:按实体拆分"
        US["用户服务"]
        PS["商品服务"]
        OS["订单服务"]
        IS["库存服务"]
        CS["优惠服务"]
        US ---|RPC| OS
        PS ---|RPC| OS
        IS ---|RPC| OS
        CS ---|RPC| OS
    end

    subgraph "正确:按限界上下文拆分"
        TC["交易服务<br/>(下单、价格、库存预检)"]
        PC["支付服务"]
        SC["物流服务"]
        TC -.->|"事件"| PC
        PC -.->|"事件"| SC
    end

2.2 限界上下文到微服务的映射不是一对一

映射方式 适用场景
一个上下文 → 一个微服务 上下文复杂度适中
一个上下文 → 多个微服务 上下文内部有不同的伸缩需求
多个上下文 → 一个部署单元 早期系统,上下文尚未完全成熟

关键原则是:逻辑边界先于物理边界。先在代码中用模块/包隔离上下文,确认边界正确后再考虑拆分为独立服务。


三、服务拆分的 7 个启发式规则

以下 7 个启发式规则可以帮助判断两个功能是否应该属于同一个服务:

3.1 规则一:通用语言一致性

如果两个功能使用相同的术语表达相同的含义,它们可能属于同一个上下文/服务。如果同一术语在两处有不同含义,它们应该分开。

"订单"在交易域 = 购买意图
"订单"在物流域 = 配送指令
→ 应该分为两个服务

3.2 规则二:业务能力内聚

一个服务应该完整地封装一项业务能力(Business Capability)。如果完成一个业务操作需要同步调用另一个服务,说明边界可能画错了。

3.3 规则三:数据所有权

每个服务应该是其核心数据的唯一所有者。如果两个服务需要修改同一份数据,数据所有权可能未正确分配。

正确:
  交易服务拥有订单数据
  支付服务拥有支付数据
  物流服务拥有配送数据

错误:
  交易服务和物流服务都能修改订单状态
  → 订单状态应该由交易服务统一管理

3.4 规则四:变更频率

变更频率相似的功能可以放在同一个服务中。变更频率差异大的功能应该分开,以减少不必要的部署。

3.5 规则五:伸缩需求

需要独立伸缩的功能应该拆分为独立服务。例如:搜索功能需要水平扩展到数十个实例,而商品管理只需要少量实例。

3.6 规则六:团队边界

一个服务应该由一个团队完全拥有和维护。如果一个服务需要多个团队协作开发,它可能太大了。

3.7 规则七:故障隔离

一个功能的故障不应该影响其他不相关功能的可用性。如果搜索服务宕机导致下单也不可用,说明耦合太紧。

启发式规则汇总表

规则 检查问题 “同一服务”信号 “不同服务”信号
通用语言一致性 术语含义一致吗? 相同术语相同含义 相同术语不同含义
业务能力内聚 能独立完成操作吗? 不需要同步 RPC 需要同步 RPC
数据所有权 谁修改这份数据? 只有一个服务修改 多个服务修改
变更频率 改动频率相似吗? 频率相近 频率差异大
伸缩需求 伸缩要求相同吗? 伸缩要求一致 伸缩要求差异大
团队边界 一个团队能负责吗? 一个团队负责 需要多团队协作
故障隔离 故障影响范围大吗? 故障影响可接受 需要独立隔离

四、Team Topologies 与康威定律

4.1 康威定律

梅尔文·康威(Melvin Conway)的定律指出:

设计系统的组织,其产出的系统架构必然是该组织沟通结构的映射。

这意味着:如果你的三个团队各自负责前端、后端和数据库,你的系统就会自然分为这三层。如果你希望系统按业务域拆分,就需要按业务域组织团队。

4.2 反向康威策略

反向康威策略(Inverse Conway Maneuver)是指主动调整团队结构,使其与目标系统架构对齐。

当前架构和团队:
  前端团队 → 所有前端代码
  后端团队 → 所有后端代码
  DBA 团队 → 所有数据库

期望架构:
  交易服务、支付服务、物流服务

反向康威调整:
  交易团队 → 交易的前端 + 后端 + 数据库
  支付团队 → 支付的前端 + 后端 + 数据库
  物流团队 → 物流的前端 + 后端 + 数据库

4.3 Team Topologies 的四种团队类型

Matthew Skelton 和 Manuel Pais 在《Team Topologies》中定义了四种基础团队类型:

graph TB
    subgraph "Team Topologies"
        SA["流对齐团队<br/>Stream-aligned Team"]
        EN["使能团队<br/>Enabling Team"]
        PL["平台团队<br/>Platform Team"]
        CSS["复杂子系统团队<br/>Complicated-subsystem Team"]
    end

    SA -->|"使用"| PL
    EN -->|"赋能"| SA
    CSS -->|"提供组件"| SA

    subgraph "与 DDD 子域的映射"
        CORE["核心子域 → 流对齐团队"]
        SUPP["支撑子域 → 复杂子系统团队"]
        GEN["通用子域 → 平台团队"]
    end

流对齐团队(Stream-aligned Team)

平台团队(Platform Team)

使能团队(Enabling Team)

复杂子系统团队(Complicated-subsystem Team)

4.4 团队交互模式

Team Topologies 定义了三种团队交互模式:

交互模式 说明 典型场景
协作(Collaboration) 两个团队紧密合作 Partnership 映射模式
X 即服务(X-as-a-Service) 一个团队提供 API,另一个消费 OHS + PL 映射模式
赋能(Facilitating) 一个团队帮助另一个团队提升能力 使能团队的工作方式

五、数据所有权与跨服务数据

5.1 核心原则

每份业务数据只有一个”权威来源”(Source of Truth)。其他需要该数据的服务,持有的是数据的副本或投影。

交易服务:拥有订单数据(权威来源)
物流服务:持有订单的配送信息副本
统计服务:持有订单的汇总数据投影

5.2 跨服务数据的传播模式

模式 实现方式 一致性 适用场景
事件驱动 发布领域事件,消费方更新副本 最终一致 大多数场景
API 查询 需要时直接调用权威来源 实时 偶尔需要最新数据
数据复制 定期同步数据 延迟高 批量分析场景
共享数据库 直接访问其他服务的数据库 强一致 不推荐

5.3 事件驱动的数据同步

// 交易服务:发布订单事件
func (s *OrderService) PlaceOrder(ctx context.Context,
    cmd PlaceOrderCommand) (*Order, error) {

    order, err := s.createOrder(ctx, cmd)
    if err != nil {
        return nil, err
    }

    // 发布集成事件
    event := OrderPlacedIntegrationEvent{
        OrderID:    order.ID().String(),
        CustomerID: order.CustomerID().String(),
        Items:      toItemDTOs(order.Lines()),
        Total:      order.Total().String(),
        ShippingAddress: toAddressDTO(order.ShippingAddress()),
    }
    s.eventBus.Publish(ctx, "order.placed", event)

    return order, nil
}

// 物流服务:订阅订单事件,维护本地数据副本
func (h *ShippingEventHandler) OnOrderPlaced(
    ctx context.Context, event OrderPlacedIntegrationEvent) error {

    // 将订单信息转换为物流域的模型
    shippingOrder := ShippingOrder{
        OrderID:     event.OrderID,
        Destination: translateAddress(event.ShippingAddress),
        Items:       translateItems(event.Items),
        Status:      ShippingStatusPending,
    }

    return h.repo.Save(ctx, &shippingOrder)
}

六、领域驱动的服务演进策略

6.1 从单体开始

Sam Newman 在《Building Microservices》中建议:

如果你无法在单体中构建出一个结构良好的系统,那你也无法通过拆分来解决问题。

推荐的演进路径:

阶段 1:模块化单体(Modular Monolith)
  ├── 在单体内部用模块/包划分限界上下文
  ├── 模块之间通过内存事件通信
  └── 验证上下文边界的正确性

阶段 2:选择性拆分
  ├── 识别需要独立伸缩、独立部署的上下文
  ├── 将这些上下文拆分为独立服务
  └── 通过消息队列替换内存事件

阶段 3:持续演进
  ├── 根据业务发展调整上下文边界
  ├── 合并过度拆分的服务
  └── 拆分增长过快的服务

6.2 模块化单体的实现

project/
├── cmd/
│   └── server/
│       └── main.go          # 单一部署单元
├── internal/
│   ├── trading/             # 交易上下文
│   │   ├── domain/
│   │   │   ├── order.go
│   │   │   └── events.go
│   │   ├── application/
│   │   │   └── order_service.go
│   │   ├── infrastructure/
│   │   │   └── order_repo.go
│   │   └── api/
│   │       └── order_handler.go
│   ├── payment/             # 支付上下文
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   └── api/
│   ├── shipping/            # 物流上下文
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   └── api/
│   └── shared/              # 共享内核(最小化)
│       └── kernel/
│           └── money.go
└── go.mod

每个上下文是一个 Go 包,它们之间的依赖关系由编译器强制约束:

// 规则:trading 包不能 import payment 包
// 它们之间通过事件通信

// internal/trading/domain/events.go
type OrderPlacedEvent struct {
    OrderID    string
    CustomerID string
    Total      string
}

// internal/shared/eventbus/bus.go
type EventBus interface {
    Publish(ctx context.Context, topic string, event interface{}) error
    Subscribe(topic string, handler func(ctx context.Context, event interface{}) error)
}

6.3 拆分决策矩阵

何时将一个模块从单体中拆分为独立服务?

flowchart TD
    START["模块是否需要拆分?"] --> Q1{"该模块的伸缩需求<br/>与主体差异大吗?"}
    Q1 -->|是| SPLIT["考虑拆分"]
    Q1 -->|否| Q2{"该模块的部署频率<br/>与主体差异大吗?"}

    Q2 -->|是| SPLIT
    Q2 -->|否| Q3{"该模块需要<br/>不同的技术栈吗?"}

    Q3 -->|是| SPLIT
    Q3 -->|否| Q4{"该模块的故障<br/>需要独立隔离吗?"}

    Q4 -->|是| SPLIT
    Q4 -->|否| KEEP["保留在单体中"]

    SPLIT --> Q5{"团队有能力管理<br/>分布式复杂性吗?"}
    Q5 -->|是| DO["执行拆分"]
    Q5 -->|否| KEEP2["暂缓,先提升能力"]

七、工程案例:SaaS 项目管理工具的服务演进

7.1 背景

某 SaaS 项目管理工具的初始架构是一个 Django 单体应用。随着用户增长到 50 万,系统面临以下挑战:

7.2 DDD 分析

团队通过事件风暴(参见 领域事件与事件风暴)识别出以下限界上下文:

限界上下文 核心聚合 子域类型 伸缩特征
项目管理 Project、Sprint、Task 核心 中等并发
实时协作 Presence、Cursor、Edit 核心 高连接数
报表分析 Report、Chart、DataSet 支撑 CPU 密集
文件管理 File、Preview、Version 支撑 IO 密集
用户管理 User、Team、Permission 通用 低频
通知 Notification、Channel 通用 突发流量

7.3 演进路径

阶段 1:模块化单体

在 Django 应用内部按限界上下文重组代码,确保模块之间通过事件总线通信。

myapp/
├── trading/    →  project_mgmt/
├── billing/    →  billing/
├── reporting/  →  reporting/
├── files/      →  file_mgmt/
├── users/      →  user_mgmt/
└── shared/     →  shared_kernel/

阶段 2:拆分实时协作

实时协作需要 WebSocket 长连接,伸缩模式与 HTTP 服务完全不同。使用 Go 重写为独立服务。

阶段 3:拆分报表分析

报表生成是 CPU 密集型操作,拆分后可以独立伸缩,不影响主应用。

阶段 4:拆分文件管理

文件上传和预览是 IO 密集型操作,拆分后可以使用专门的存储方案。

7.4 最终架构

graph TB
    subgraph "流对齐团队"
        PM["项目管理服务<br/>(Python/Django)"]
        RT["实时协作服务<br/>(Go/WebSocket)"]
    end

    subgraph "复杂子系统团队"
        RP["报表分析服务<br/>(Python/Celery)"]
    end

    subgraph "平台团队"
        FM["文件管理服务<br/>(Go/S3)"]
        UM["用户管理服务<br/>(Python/Django)"]
        NT["通知服务<br/>(Go/WebSocket)"]
    end

    PM -.->|"事件"| RT
    PM -.->|"事件"| RP
    PM -.->|"事件"| NT
    PM -->|"API"| UM
    PM -->|"API"| FM

    MQ["消息队列<br/>(Kafka)"]
    PM -.-> MQ
    MQ -.-> RT
    MQ -.-> RP
    MQ -.-> NT

7.5 效果

指标 演进前(单体) 演进后(混合)
P99 响应时间 2.5s 200ms
WebSocket 连接数 5,000 50,000
报表生成等待时间 30s 5s
部署频率 每周 1 次 每天 3-5 次
团队规模 15 人全栈 4 个 4-6 人团队

八、服务间通信模式

8.1 同步 vs 异步

维度 同步通信(RPC/HTTP) 异步通信(消息/事件)
耦合度 高(时间耦合) 低(解耦)
一致性 强一致 最终一致
可用性 受被调用方影响 不受影响
可追踪性 高(请求链路清晰) 中(需要分布式追踪)
适用场景 需要立即获取结果 可以容忍延迟

8.2 推荐策略

上下文内部:同步调用(进程内函数调用)
上下文之间(核心路径):异步事件(消息队列)
上下文之间(查询数据):同步 API(带降级策略)

8.3 Saga 模式处理跨服务事务

当业务操作跨越多个服务时,使用 Saga 模式替代分布式事务:

下单 Saga(编排式):

1. 交易服务:创建订单(状态=待支付)
2. 支付服务:处理支付
   ├── 成功 → 3. 交易服务:确认订单
   │            4. 库存服务:扣减库存
   │            5. 物流服务:创建配送单
   └── 失败 → 补偿:交易服务取消订单
// Saga 编排器
type PlaceOrderSaga struct {
    orderService    OrderServiceClient
    paymentService  PaymentServiceClient
    inventoryService InventoryServiceClient
}

func (s *PlaceOrderSaga) Execute(ctx context.Context,
    cmd PlaceOrderCommand) error {

    // Step 1: 创建订单
    orderID, err := s.orderService.CreateOrder(ctx, cmd)
    if err != nil {
        return fmt.Errorf("创建订单失败: %w", err)
    }

    // Step 2: 处理支付
    err = s.paymentService.ProcessPayment(ctx, orderID, cmd.Amount)
    if err != nil {
        // 补偿:取消订单
        s.orderService.CancelOrder(ctx, orderID)
        return fmt.Errorf("支付失败: %w", err)
    }

    // Step 3: 扣减库存
    err = s.inventoryService.ReserveStock(ctx, cmd.Items)
    if err != nil {
        // 补偿:退款 + 取消订单
        s.paymentService.Refund(ctx, orderID)
        s.orderService.CancelOrder(ctx, orderID)
        return fmt.Errorf("库存不足: %w", err)
    }

    // Step 4: 确认订单
    return s.orderService.ConfirmOrder(ctx, orderID)
}

九、常见误区

9.1 误区一:微服务数量越多越好

服务越多,运维复杂度越高。每个服务都需要独立的 CI/CD、监控、日志、告警。没有足够的基础设施支撑,微服务会成为负担而非优势。

9.2 误区二:先拆分再理解领域

正确顺序:理解领域 → 划分上下文 → 模块化单体 → 选择性拆分。不要在不理解领域的情况下拆分微服务。

9.3 误区三:共享数据库是捷径

多个微服务共享数据库,看似简化了数据同步,但实际上创建了一个隐式的耦合点。任何 schema 变更都可能影响所有服务。

9.4 误区四:忽视网络不可靠性

微服务之间的网络调用可能失败、超时、返回错误数据。需要在架构中考虑重试、降级、熔断、幂等性等弹性模式(参见之前的弹性模式文章)。

9.5 误区五:所有通信都用事件

不是所有服务间通信都适合事件驱动。同步查询、需要立即结果的操作,使用 API 调用更合适。过度使用事件会导致系统行为难以追踪。


十、综合权衡

决策维度 单体/模块化单体 微服务 权衡要点
部署复杂度 基础设施成熟度
开发速度(早期) 团队规模和业务阶段
开发速度(后期) 慢(耦合阻碍) 快(独立迭代) 业务复杂度增长
技术多样性 统一技术栈 可选不同技术 团队技能分布
数据一致性 强一致 最终一致 业务对一致性的要求
故障影响 整体可用性 局部故障隔离 可用性要求
运维成本 运维团队能力
适合团队 < 20 人 > 20 人多团队 团队规模
适合阶段 产品验证期 规模化增长期 业务生命周期

十一、总结

微服务的边界不应该按数据实体划分,而应该基于限界上下文——从通用语言、业务能力、数据所有权等多个维度综合判断。Team Topologies 提供了组织结构与架构对齐的方法论,反向康威策略帮助主动塑造团队结构。最重要的是,微服务不是目标而是手段——从模块化单体开始,根据真实的伸缩、部署和团队需求选择性拆分,是风险最低的演进路径。

下一篇:认证架构


参考资料

  1. Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
  2. Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
  3. Newman, Sam. Building Microservices. 2nd ed., O’Reilly, 2021.
  4. Newman, Sam. Monolith to Microservices. O’Reilly, 2019.
  5. Skelton, Matthew; Pais, Manuel. Team Topologies. IT Revolution, 2019.
  6. Richardson, Chris. Microservices Patterns. Manning, 2018.
  7. Conway, Melvin. “How Do Committees Invent?” Datamation, 1968.
  8. Fowler, Martin. “Monolith First.” martinfowler.com.
  9. Brandolini, Alberto. Introducing EventStorming. Leanpub, 2021.
  10. Tune, Nick; Millett, Scott. Patterns, Principles, and Practices of Domain-Driven Design. Wrox, 2015.

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略

2026-04-13 · architecture

【系统架构设计百科】DDD 战略设计:限界上下文与上下文映射

一个中型电商系统里,"订单"在交易团队意味着"待支付的购物车快照",在物流团队意味着"等待拣货的配送单",在财务团队意味着"一条应收账款记录"。三个团队共用同一张 torder 表、同一个 OrderService 类,每次迭代都互相踩脚。这种混乱的根源不是代码质量,而是缺少一项最基本的架构决策——限界上下文(Boun…

2026-04-13 · architecture

【系统架构设计百科】DDD 战术模式:聚合、实体与值对象

某团队在实施领域驱动设计时,把整个"订单"建模为一个聚合根(Aggregate Root),其中包含订单基本信息、所有订单行、配送信息、支付记录、物流轨迹、评价数据。结果这个聚合加载一次需要从 7 张表联查,保存一次需要锁定整个订单树。并发下单高峰期,数据库锁等待飙升至秒级。这就是典型的"大聚合"反模式——聚合的边界画…


By .