第2章:分解应用程序(Decomposing Applications)
一、导读
1.1 本章学习目标
服务拆分是微服务架构中最关键也最困难的决策。第2章深入讲解如何定义服务边界——这直接决定了微服务架构的成败。拆分过粗,失去微服务的优势;拆分过细,运维复杂度失控。Richardson 以 DDD(领域驱动设计)的限界上下文为核心方法论,结合两种拆分策略(自顶向下和自底向上),为读者提供了系统化的拆分指南。
本章的核心学习目标包括:
- 掌握 DDD 限界上下文的概念:理解它为什么是服务边界的最佳划分依据。
- 学会自顶向下的拆分方法:从业务能力分解图出发,识别顶层服务能力。
- 学会自底向上的拆分方法:从现有单体代码中逐步剥离出独立服务。
- 理解服务粒度的权衡:如何在"太粗"和"太细"之间找到平衡点。
- 掌握数据所有权原则:每个服务拥有自己的数据存储,服务之间不共享数据库。
- 了解稳定身份原则:服务命名、版本管理和 API 契约的长期稳定性。
1.2 为什么本章如此重要
如果说第1章回答了"为什么需要微服务",那么第2章回答的就是"如何拆分微服务"——这是整个微服务架构中最核心的设计决策。Richardson 明确指出:服务边界的定义是不可逆的(或高成本的)。一旦拆分错误,后续合并或重新拆分的代价极高。错误的服务边界会导致服务之间频繁通信、循环依赖、分布式事务泛滥,最终使微服务架构退化为"分布式单体"。
本章的重要性还在于它引入了 DDD(领域驱动设计)这一关键方法论。DDD 的限界上下文为服务边界提供了语义化的划分标准——服务的边界不是按技术层(如数据访问层、业务逻辑层)划分,而是按业务领域(如订单、支付、库存)划分。这种划分方式天然对应业务团队的组织结构,符合康威定律。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
如何判断两个功能应该属于同一个服务还是不同服务?
DDD 的限界上下文如何指导服务拆分?它与传统的分层架构有什么本质区别?
自顶向下和自底向上两种拆分策略分别适用于什么场景?
服务粒度太粗和太细分别会导致什么问题?如何找到平衡点?
"每个服务拥有自己的数据库"原则在实践中如何落地?跨服务查询怎么处理?
如何处理拆分过程中发现的服务之间的循环依赖?
二、核心概念详解
2.1 领域驱动设计与限界上下文
2.1.1 DDD 的核心思想
Eric Evans 在 2003 年提出的 DDD 核心思想是:软件的模型应该反映业务领域概念。DDD 将复杂的业务领域划分为若干个限界上下文(Bounded Context),每个限界上下文内部有统一的领域模型,上下文之间通过明确的接口协作。
// FTGO 的业务领域模型:
// 限界上下文1:订单管理
// 核心概念:Order, OrderItem, OrderStatus
// "Order" 在订单上下文中的含义:
// - 包含商品列表、总价、配送地址、状态
// - 状态流转:CREATED → APPROVED → PREPARING → READY → DELIVERED
// 限界上下文2:厨房管理
// 核心概念:KitchenOrder, MenuItem, PreparationStatus
// "Order" 在厨房上下文中的含义完全不同:
// - 只关心菜品列表、制作时间、出餐状态
// - 不关心价格、支付方式、配送地址
// 限界上下文3:配送管理
// 核心概念:Delivery, Courier, Route
// "Order" 在配送上下文中的含义又不同:
// - 只关心取餐地址、送餐地址、预计时间
// - 不关心菜品详情、价格
// 关键洞察:同一个"Order"概念在不同上下文中有不同的模型
// 这正是服务拆分的依据!2.1.2 限界上下文作为服务边界
Richardson 的核心观点是:每个限界上下文对应一个微服务。限界上下文的边界就是服务的边界。
// FTGO 的限界上下文 → 微服务映射:
// 限界上下文 → 微服务 → 数据库
// ─────────────────────────────────────────────────────────
// 用户管理 → user-service → users_db
// 餐厅管理 → restaurant-service → restaurants_db
// 订单管理 → order-service → orders_db
// 厨房管理 → kitchen-service → kitchen_db
// 配送管理 → delivery-service → delivery_db
// 支付管理 → payment-service → payments_db
// 通知管理 → notification-service → notifications_db
// 每个服务:
// 1. 有自己的领域模型(实体、值对象、聚合根)
// 2. 有自己的数据库(其他服务无法直接访问)
// 3. 通过 API 或事件与其他服务通信2.1.3 限界上下文之间的通信模式
DDD 定义了限界上下文之间的四种关系模式,每种模式对应不同的微服务通信方式:
// 1. 共享内核(Shared Kernel)
// 两个上下文共享一小部分模型(如 Money、Address)
// 微服务实现:共享库(shared-library)
// 注意:共享部分必须非常稳定,否则变更会影响多个服务
// 2. 客户-供应商(Customer-Supplier)
// 上游服务为下游服务提供数据
// 微服务实现:REST API 或 gRPC
// 例:order-service(客户)调用 restaurant-service(供应商)获取菜单
// 3. 遵从者(Conformist)
// 下游服务完全遵从上游服务的模型
// 微服务实现:直接使用上游服务的 API 定义
// 例:notification-service 完全遵从 order-service 的事件格式
// 4. 防腐层(Anti-Corruption Layer)
// 在两个上下文之间建立翻译层,避免外部模型污染内部模型
// 微服务实现:适配器模式
// 例:delivery-service 内部使用 Route 模型
// 但调用外部地图服务时,通过 ACL 将 Route 转换为 LatLng2.2 自顶向下的拆分策略
2.2.1 业务能力分解
自顶向下的拆分从公司的业务能力出发,逐层分解。Richardson 推荐使用业务能力分解图:
// FTGO 的业务能力分解图:
// Level 0: FTGO 外卖平台
// ├── Level 1: 订单管理
// │ ├── Level 2: 创建订单
// │ ├── Level 2: 取消订单
// │ ├── Level 2: 订单查询
// │ └── Level 2: 订单状态跟踪
// ├── Level 1: 餐厅管理
// │ ├── Level 2: 餐厅注册
// │ ├── Level 2: 菜单管理
// │ └── Level 2: 餐厅搜索
// ├── Level 1: 支付处理
// │ ├── Level 2: 支付授权
// │ ├── Level 2: 退款处理
// │ └── Level 2: 对账管理
// ├── Level 1: 配送管理
// │ ├── Level 2: 配送调度
// │ ├── Level 2: 骑手管理
// │ └── Level 2: 路线规划
// └── Level 1: 厨房管理
// ├── Level 2: 接单确认
// ├── Level 2: 出餐管理
// └── Level 2: 出餐状态更新
// 拆分原则:
// 1. 每个 Level 1 的业务能力对应一个微服务
// 2. Level 2 的操作是该服务的 API
// 3. 如果某个 Level 1 能力过于复杂,可以进一步拆分2.2.2 拆分原则
Richardson 提出了两条关键的拆分原则:
原则一:单一职责原则(SRP)
每个服务只负责一个业务能力。服务的职责应该有内聚性——所有操作都围绕同一个业务领域。
// 好的拆分:
// order-service:负责订单的创建、查询、取消、状态管理
// → 所有操作都围绕"订单"这个业务概念
// 坏的拆分:
// crud-service:负责所有实体的增删改查
// → 没有业务内聚性,只是技术层的划分原则二:高内聚低耦合
服务内部的功能应该高度相关(高内聚),服务之间的依赖应该尽可能少(低耦合)。
// 高内聚:order-service 的所有功能都围绕订单
// 低耦合:order-service 只通过 API 与 payment-service 通信
// 判断标准:
// 如果两个功能经常一起变更 → 应该在同一个服务
// 如果两个功能很少一起变更 → 可以在不同服务
// 如果两个功能变更时需要不同团队协调 → 应该在不同服务2.3 自底向上的拆分策略
2.3.1 从现有代码中识别服务
对于已有的单体应用,自底向上的拆分更加实际。Richardson 建议从以下步骤开始:
分析代码结构:识别包/模块的边界和依赖关系。
识别数据边界:哪些表总是一起被访问?哪些表可以独立?
识别变更频率:哪些功能总是一起修改?
识别团队边界:哪些团队负责哪些功能?
// 分析单体代码的依赖关系:
// 使用工具(如 jdeps、ArchUnit)分析类之间的依赖
// 依赖矩阵:
// User Order Payment Kitchen Delivery
// User - ↓ ↓ - -
// Order ↑ - ↓ ↓ ↓
// Payment ↑ ↑ - - -
// Kitchen - ↑ - - ↓
// Delivery - ↑ - ↑ -
// 分析结果:
// Order 和 Payment 之间有强依赖 → 考虑是否应该拆分
// Kitchen 和 Delivery 之间有依赖 → 可能需要协调
// User 被 Order 和 Payment 依赖 → 应该独立为 user-service2.3.2 Strangler Fig 模式的实践
Richardson 详细描述了 Strangler Fig 模式的实施步骤:
// 步骤1:识别要提取的服务
// 选择变更频率高、与其他模块耦合低的模块优先提取
// 例:notification-service 耦合度低,适合首先提取
// 步骤2:创建新的微服务
// 新建 notification-service 项目
// 实现通知相关的 API
// 步骤3:设置路由
// 在 API Gateway 中配置路由规则
// /api/notifications → notification-service
// 其他请求 → 单体应用
// 步骤4:数据迁移
// 从共享数据库中迁移通知相关的数据
// 可以采用双写策略:同时写入新旧数据库
// 验证数据一致性后,切换到新数据库
// 步骤5:流量切换
// 逐步将通知相关的请求路由到新服务
// 监控错误率和延迟
// 确认无误后,完全切换到新服务
// 步骤6:清理
// 从单体中删除通知相关的代码
// 下线单体中的通知模块2.4 数据所有权原则
2.4.1 Database-per-Service 模式
Richardson 强调的核心原则:每个微服务拥有自己的数据存储,服务之间不共享数据库。
// 反模式:共享数据库
// order-service ─┐
// payment-service ─┼──→ 同一个 Oracle 数据库
// inventory-service┘
// 问题:
// 1. 服务之间通过数据库表耦合
// 2. Schema 变更需要跨团队协调
// 3. 无法独立选择数据库类型
// 4. 数据库成为性能瓶颈
// 正确模式:每个服务独立数据库
// order-service → orders_db (PostgreSQL)
// payment-service → payments_db (PostgreSQL)
// inventory-service → inventory_db (MongoDB)
// 优势:
// 1. 服务之间通过 API 通信,解耦
// 2. 每个服务独立管理 Schema
// 3. 可以选择最适合的数据库类型
// 4. 数据库负载分散2.4.2 跨服务查询的处理
Database-per-Service 带来的最大挑战是跨服务查询。在单体中,一个 SQL JOIN 就能解决的问题,在微服务中需要跨服务获取数据。
// 场景:订单详情页需要显示订单信息 + 餐厅信息 + 配送信息
// 方案1:API 组合(同步)
// 前端或 API Gateway 调用多个服务:
// 1. GET /api/orders/123 → order-service
// 2. GET /api/restaurants/456 → restaurant-service
// 3. GET /api/deliveries/789 → delivery-service
// 4. 组合结果返回给前端
// 优点:简单直接
// 缺点:多次网络调用,延迟高
// 方案2:数据冗余(异步复制)
// order-service 中冗余存储餐厅名称
// 当餐厅信息变更时,通过事件通知 order-service 更新
// 优点:查询效率高
// 缺点:数据可能不一致,需要维护冗余数据
// 方案3:CQRS + 物化视图
// 创建专门的查询服务(Query Service)
// 订阅各服务的事件,构建物化视图
// 优点:查询效率最高
// 缺点:架构复杂,数据有延迟2.5 服务粒度的权衡
2.5.1 服务太粗的问题
// 服务太粗(接近单体):
// commerce-service → 包含订单、支付、库存、配送
// 问题:
// 1. 代码量大,团队难以管理
// 2. 部署频率低(任何修改都要部署整个服务)
// 3. 无法独立伸缩(订单高峰 ≠ 配送高峰)
// 4. 技术栈锁定(只能用一个数据库)2.5.2 服务太细的问题
// 服务太细(纳米服务):
// create-order-service → 只负责创建订单
// cancel-order-service → 只负责取消订单
// query-order-service → 只负责查询订单
// 问题:
// 1. 服务数量爆炸(几十个服务)
// 2. 运维复杂度失控
// 3. 服务间通信开销大
// 4. 分布式事务泛滥
// 5. 团队精力分散在基础设施而非业务2.5.3 寻找平衡点
Richardson 的建议:服务的粒度应该由业务边界决定,而不是人为规定。如果一个业务能力天然包含多个操作(如订单的创建、查询、取消),它们应该属于同一个服务。
// 判断标准:
// 1. 这些操作是否围绕同一个业务实体?
// → 是:属于同一服务
// → 否:可能应该拆分
// 2. 这些操作是否总是一起变更?
// → 是:属于同一服务
// → 否:可能应该拆分
// 3. 这些操作是否由同一个团队负责?
// → 是:属于同一服务
// → 否:可能应该拆分
// 4. 一个服务最多由一个团队(5-9人)维护
// → 超过:考虑拆分三、实战案例
3.1 案例一:电商系统的服务拆分
// 电商系统的业务能力分解:
// Level 0: 电商平台
// ├── 用户管理
// │ ├── 注册/登录
// │ ├── 个人信息管理
// │ └── 地址管理
// ├── 商品管理
// │ ├── 商品发布
// │ ├── 商品搜索
// │ └── 库存管理
// ├── 交易管理
// │ ├── 购物车
// │ ├── 下单
// │ └── 订单管理
// ├── 支付管理
// │ ├── 支付处理
// │ ├── 退款
// │ └── 对账
// └── 物流管理
// ├── 发货
// ├── 物流跟踪
// └── 签收确认
// 拆分结果:
// user-service → 用户管理(PostgreSQL)
// product-service → 商品管理(MongoDB + Elasticsearch)
// order-service → 交易管理(PostgreSQL)
// payment-service → 支付管理(PostgreSQL)
// logistics-service → 物流管理(PostgreSQL + Redis)3.2 案例二:从单体到微服务的渐进拆分
// 初始单体:ecommerce-monolith(Java/Spring + MySQL)
// 第1步:模块化单体
// 将代码重组为模块:user-module, product-module, order-module, payment-module
// 使用 Spring Modulith 强制执行模块边界
// 第2步:提取 user-service
// 用户管理变更频率低,耦合度低 → 首先提取
// 通过 API Gateway 路由 /api/users → user-service
// 第3步:提取 product-service + search-service
// 商品管理需要 Elasticsearch → 需要独立数据库
// 搜索功能独立为 search-service
// 第4步:提取 payment-service
// 支付处理对安全性要求高 → 独立部署、独立审计
// 第5步:提取 logistics-service
// 物流管理需要与外部系统对接 → 独立服务便于集成
// 最终:order-service 最后拆分(最复杂,依赖最多)3.3 案例三:处理循环依赖
// 问题场景:
// order-service 需要调用 payment-service 确认支付状态
// payment-service 需要调用 order-service 获取订单金额
// → 循环依赖!
// 解决方案:事件驱动
// 1. order-service 创建订单 → 发布 OrderCreated 事件
// 2. payment-service 监听事件 → 处理支付 → 发布 PaymentCompleted 事件
// 3. order-service 监听事件 → 更新订单状态
//
// 通过事件异步解耦,消除循环依赖
// 每个服务只依赖事件,不直接依赖其他服务的 API四、本章小结
本章深入讲解了微服务拆分的方法论和最佳实践:
核心知识点:
- DDD 限界上下文是服务边界的最佳划分依据,每个限界上下文对应一个微服务。
- 自顶向下拆分从业务能力分解图出发,适合新项目。
- 自底向上拆分从现有代码中识别边界,适合存量系统迁移。
- Database-per-Service 原则:每个服务拥有独立的数据存储。
- 服务粒度由业务边界决定,避免过粗(接近单体)或过细(纳米服务)。
- 跨服务查询通过 API 组合、数据冗余或 CQRS 解决。
五、思考与练习
思考题
限界上下文与传统的"模块"有什么本质区别?为什么模块不能直接等同于微服务?
在你的项目中,哪些功能之间的耦合度最高?如何降低它们之间的耦合?
Database-per-Service 原则是否总是适用?有没有例外情况?
练习题
为一个在线教育平台设计服务拆分方案:画出业务能力分解图,识别限界上下文,确定服务边界和数据库选型。
选择一个你熟悉的单体项目,使用自底向上的方法分析代码依赖关系,制定拆分计划。
设计一个跨服务查询的解决方案:订单详情页需要显示用户信息、商品信息、支付状态和物流信息,如何设计数据获取策略?