基于DDD构建微服务:从战略设计到落地实践

微服务架构的核心难题不是技术选型,而是如何找到正确的服务边界。拆分得太粗,和单体无异;拆分得太细,分布式的复杂性会吞噬所有收益。领域驱动设计(DDD)提供了一套系统性的方法论,帮助我们从业务本质出发,找到合理的拆分边界。本文将从 DDD 的核心概念出发,结合电商领域的实例,完整展示如何基于 DDD 构建微服务。

微服务的本质:不是"小",而是"界限清晰"

微服务中的"微"虽然表示服务的规模,但它并不是微服务架构的核心标准。Adrian Cockcroft 对微服务有一个精炼的定义:

"面向服务的架构由具有界限上下文松散耦合的元素组成。"

一个真正的微服务架构应当具备以下特征:

特征 说明
业务边界清晰 服务以业务上下文为中心,而非技术抽象
实现细节隐藏 通过意图接口暴露功能,不泄露内部实现
数据独立 服务不共享数据库,每个服务拥有自己的数据存储
故障快速恢复 具备容错和弹性能力
独立部署 团队可以自主、频繁地发布变更
自动化文化 自动化测试、持续集成、持续交付

归纳起来:松散耦合的面向服务架构,每个服务封装在定义良好的界限上下文中,支持快速、频繁且可靠的交付。

微服务的强大之处在于:边界内建立高内聚,边界外建立低耦合——倾向于一起改变的事物应该放在一起。但说起来容易做起来难,业务在不断发展,设想也随之改变。因此,重构能力是设计系统时必须考虑的关键问题。

DDD 核心概念速览

领域驱动设计(Domain-Driven Design)因 Eric Evans 的同名著作而闻名,它是一组思想、原则和模式,帮助我们基于业务领域的底层模型来设计软件系统。

基本术语

概念 定义 示例
领域(Domain) 组织所从事的业务范围 零售、电子商务
子域(Subdomain) 领域下的业务单元,一个领域由多个子域组成 目录、购物车、履约、支付
统一语言(Ubiquitous Language) 开发人员与领域专家共同使用的、表达业务模型的语言 "商品"、"订单"、"履约"
界限上下文(Bounded Context) 模型的有效边界,同一术语在不同上下文中含义不同 见下文详述

界限上下文:同一个词,不同的含义

以电商系统中的 "Item"(商品) 为例,它在不同的上下文中有着截然不同的含义:

上下文 "Item" 的含义 关注的属性
Catalog(目录) 可出售的产品 名称、描述、价格、图片、分类
Cart(购物车) 客户添加到购物车的商品选项 SKU、数量、选中状态
Fulfillment(履约) 将要运送给客户的仓库物料 仓库位置、重量、物流单号

通过将这些模型分离并隔离在各自的边界内,我们可以自由地表达这些模型而不产生歧义。

子域 vs 界限上下文:子域属于问题空间(业务如何看待问题),界限上下文属于解决方案空间(如何实现问题的解决方案)。理论上一个子域可以有多个界限上下文,但我们努力做到每个子域只有一个。

从界限上下文到微服务

界限上下文 ≠ 微服务

每个界限上下文都能直接映射为一个微服务吗?不一定

以"定价"界限上下文为例,它可能包含三个不同的模型:

模型(聚合) 职责
Price(价格) 管理目录商品的价格
Priced Items(定价项) 计算商品列表的总价
Discounts(折扣) 管理和应用各类折扣规则

如果把这三个模型放在一个服务中,随着时间推移,界限可能变得模糊,职责开始重叠,最终退化为"大泥球"。

聚合(Aggregate):更精细的拆分单元

DDD 中的聚合是由相关模型组成的自包含单元,是数据变更的原子边界

聚合是关联对象的集群,被视为数据变更的单元。外部引用仅限于指定聚合的一个成员——聚合根(Aggregate Root)。在聚合的边界内需应用一组一致性规则。

聚合的核心约束:

  • 一致性在单个聚合内保证:跨聚合的一致性只能做到最终一致
  • 只能通过已发布的接口修改聚合:外部不能绕过聚合根直接操作内部对象
  • 任何违反这些规则的行为都有让应用退化为大泥球的风险

拆分策略:从保守到激进

策略 适用场景 优势 风险
一个界限上下文 = 一个微服务 领域模糊、业务初期 保守安全,避免过早拆分 服务可能过大
一个聚合 = 一个微服务 领域清晰、边界确定 粒度精细,独立演进 分布式复杂度高
一个界限上下文 = 多个微服务 上下文内聚合边界清晰 兼顾灵活与可控 需要精确的聚合划分

对于不完全了解的业务领域,建议从保守策略开始:将整个界限上下文及其聚合组成单个微服务。确保聚合之间通过接口充分隔离,后续再拆分的成本会低得多。将两个微服务合并为一个的成本远高于将一个微服务拆分为两个

上下文映射:精确划分服务边界

上下文映射(Context Mapping)用于识别和定义各种界限上下文和聚合之间的关系。它帮助我们回答一个关键问题:这些服务之间应该如何协作?

一个错误的设计示例

以电商支付场景为例,假设有三个服务都需要处理支付:

服务 支付相关操作
购物车服务 在线支付授权
订单服务 订单履约后结算
联络中心服务 支付重试、变更支付方式

如果每个服务都内嵌支付聚合并直接对接支付网关,会产生严重问题:

  • 一致性不可保证:支付聚合分散在多个服务中,无法强制执行不变性
  • 并发冲突:联络中心更改支付方式时,订单服务可能正在用旧方式结算
  • 变更扩散:支付网关的任何变更都要改动多个服务、多个团队

重新定义服务边界

通过上下文映射,将支付聚合收拢到一个独立的支付服务中:

改造项 说明
支付服务独立 支付聚合有了专属的界限上下文,不变量在单个服务边界内管理
反腐层(ACL) 在支付服务和支付网关之间加入适配层,隔离核心领域模型与第三方数据模型
购物车→支付 同步 API 调用,因为下单时需要即时的支付授权反馈
订单→支付 异步事件驱动,订单服务发出域事件,支付服务监听并完成结算
联络中心→支付 异步事件驱动,变更支付方式时发出事件,支付服务撤销旧卡、处理新卡

核心原则:微服务架构的成败取决于聚合之间的低耦合以及聚合之内的高内聚。

事件风暴:协作式的服务边界发现

事件风暴(Event Storming)是 Alberto Brandolini 提出的一种轻量级的协作建模技术,它是识别聚合和微服务边界的另一种必不可少的工具。

什么是事件风暴?

简单来说,事件风暴是团队在一起进行的头脑风暴,目标是识别系统中发生的各种领域事件业务流程

工作方式:

  1. 所有相关团队在同一个房间(物理或虚拟)
  2. 在白板上用不同颜色的便利贴标记事件、命令、聚合和策略
  3. 识别重叠概念、模糊的领域语言和冲突的业务流程
  4. 对相关模型进行分组,重新定义聚合边界

便利贴颜色约定

颜色 含义 示例
橙色 领域事件(已发生的事实) "订单已创建"、"支付已完成"
蓝色 命令(触发事件的动作) "创建订单"、"取消订单"
黄色 聚合(命令作用的对象) "订单"、"支付"、"库存"
紫色 策略/规则(事件触发的后续逻辑) "支付完成后发送确认邮件"
红色 热点/问题(需要讨论的疑问) "退款流程和订单取消是否耦合?"

事件风暴的产出

一次成功的事件风暴通常会产出:

  • 重新定义的聚合列表:这些可能成为新的微服务
  • 领域事件清单:需要在微服务之间流动的事件
  • 命令清单:外部用户或其他服务直接调用的操作
  • 团队共识:对领域、统一语言和精确服务边界的共同理解

微服务间的通信:拥抱最终一致性

从单体到微服务的一致性挑战

在单体应用中,多个聚合在同一个进程边界内,可以在一个事务中完成:客户下单 → 扣减库存 → 发送邮件。所有操作要么都成功,要么都失败。

但微服务化后,这些聚合分散到了不同的分布式系统中。根据 CAP 定理

一个分布式系统只能同时满足三个特性中的两个:一致性(C)可用性(A)分区容错(P)

在现实系统中,分区容错(P)是不可协商的——网络不可靠、虚拟机可以宕机、区域延迟可能恶化。因此我们只能在可用性一致性之间选择。而在现代互联网应用中,牺牲可用性通常也不可接受。

结论:基于最终一致性设计应用程序。

事件驱动架构

微服务可以将聚合上发生的重要变更以领域事件(Domain Event) 的形式发出,感兴趣的服务监听这些事件并在自己的领域内执行相应操作。

以"订单取消"为例:

订单服务发布事件:OrderCancelled
  → 支付服务监听 → 执行退款
  → 库存服务监听 → 调整商品库存
  → 通知服务监听 → 发送取消确认邮件

这种方式避免了两种耦合:

耦合类型 事件驱动如何避免
行为耦合 一个领域无需规定其他领域应该做什么
时间耦合 一个流程的完成不依赖于所有系统同时可用

事件驱动的可靠性保障

角色 保障措施
生产者 确保事件至少发出一次(At Least Once),失败时有回退机制重新触发
消费者 幂等方式消费事件,同一事件重复到达不产生副作用
事件排序 事件可能乱序到达,消费者用时间戳或版本号保证正确性

何时仍需同步调用?

并非所有场景都适合事件驱动。当需要即时反馈时(如购物车→支付授权),仍需同步 API 调用。但要注意:

  • 同步调用引入了行为耦合时间耦合
  • 被调用服务不可用时,调用方也会受影响

缓解策略:同步调用作为主路径,辅以基于事件或批处理的异步重试作为降级方案。在用户体验、系统弹性和运营成本之间做好权衡。

何时应该合并而非拆分? 如果发现两个聚合之间需要强 ACID 事务,这是一个强烈的信号——它们可能应该属于同一个聚合。在拆分之前,事件风暴和上下文映射可以帮助我们及早识别这些依赖关系。

BFF 模式:解耦前端与领域服务

问题:服务为了迎合调用者而变形

微服务架构中一个常见的反模式是:域服务为了满足前端的特定数据需求而编排其他服务

以"订单详情页"为例,页面需要同时展示订单信息和退款信息。如果让订单服务调用退款服务来组装复合响应:

  • 订单服务的自治性降低:退款聚合的变更会影响订单服务
  • 增加故障点:退款服务宕机时订单服务也受影响
  • 变更成本高:前端需求变化时需要两个团队同时改动

解决方案:Backend for Frontends(BFF)

BFF 是由消费者团队(前端团队)创建和维护的后端服务,负责:

  • 对多个域服务进行集成和编排
  • 为前端提供定制化的数据契约
  • 根据不同终端(Web/Mobile)优化响应格式和体积
对比 无 BFF 有 BFF
数据编排 域服务互相调用,或前端直接调多个服务 BFF 统一编排,域服务保持纯粹
变更自主性 前端需求变化要改多个域服务 前端团队自主改 BFF
性能优化 移动端可能获取过多冗余数据 可按终端定制负载大小
技术选型 受域服务 API 限制 BFF 可采用 GraphQL 等灵活方案

尽早构建 BFF 服务,可以避免两种不良后果:域服务被迫支持跨域编排,或前端不得不直接调用多个后端服务。

从单体到微服务:拆分路线图

将以上所有工具整合,从单体拆分到微服务的推荐路径:

第一步:战略设计(Strategic Design)

  1. 识别子域:与领域专家一起梳理业务,划分子域
  2. 定义界限上下文:为每个子域确定解决方案的边界
  3. 建立统一语言:在每个上下文内建立一致的业务术语

第二步:战术发现(Tactical Discovery)

  1. 事件风暴:跨团队协作,识别领域事件、命令、聚合和热点问题
  2. 上下文映射:绘制上下文之间的依赖关系和协作模式
  3. 识别聚合:在每个上下文内找到自包含的数据变更单元

第三步:服务划分(Service Decomposition)

  1. 确定服务边界:根据聚合和上下文映射,确定每个微服务的边界
  2. 设计通信方式:区分同步调用和异步事件,优先使用事件驱动
  3. 规划 BFF 层:为不同终端设计专属的后端聚合层

第四步:渐进式拆分(Incremental Migration)

  1. 从边缘开始:先拆分耦合最少、边界最清晰的服务
  2. 绞杀者模式:新功能用微服务实现,老功能逐步迁移
  3. 持续验证:每拆分一个服务,验证边界是否正确,必要时调整

DDD 战略设计与战术设计的关系

很多团队在实践 DDD 时过度关注战术设计(实体、值对象、聚合根、仓储等代码层面的模式),而忽视了战略设计(子域、界限上下文、上下文映射)。对于微服务架构而言,战略设计的价值远大于战术设计:

维度 战略设计 战术设计
关注点 服务边界、团队协作、系统结构 代码结构、领域模型、设计模式
影响范围 整个系统架构 单个服务内部
决策成本 错误的边界划分代价极高 内部重构成本相对可控
适用阶段 架构设计初期 服务实现阶段

先做对战略设计(找到正确的边界),再做好战术设计(在边界内写好代码)。 边界划错了,代码写得再漂亮也是徒劳。

总结

基于 DDD 构建微服务的核心认知:

  1. 微服务的本质是界限清晰,不是规模小。边界内高内聚,边界外低耦合
  2. 界限上下文是服务拆分的起点,但不是终点——聚合才是更精细的拆分单元
  3. 上下文映射揭示服务间的真实依赖,帮助我们避免聚合被错误地分散到多个服务中
  4. 事件风暴是最有效的协作式建模工具,它能让团队在分解前就达成共识
  5. 拥抱最终一致性,优先使用事件驱动架构,减少服务间的行为耦合和时间耦合
  6. BFF 模式解耦前端与域服务,让域服务专注于核心业务逻辑
  7. 先保守后激进:不确定时将整个上下文作为一个服务,确保聚合间接口隔离,后续再拆分
  8. 合并的成本远高于拆分:将两个数据库合并为一个,远比将一个数据库拆为两个要困难

DDD 不是银弹,它是一种思考方式。它引导我们从业务本质出发,用结构化的方法找到正确的服务边界。在微服务架构中,找到正确的边界比选择正确的技术栈重要十倍

加载导航中...

评论