Skip to content

单体到微服务:架构演进、服务拆分与数据边界

微服务不是把一个 Maven 工程拆成很多仓库,而是把业务能力、数据所有权、发布责任和故障边界一起拆开。拆得不合理,只会得到“分布式单体”:服务很多,但任何需求仍要所有团队一起改、一起发、一起失败。

一、学习目标

学完后应能判断系统是否需要微服务,按业务能力确定服务边界,解释数据所有权的意义,并设计可灰度、可回滚的渐进迁移方案。

二、架构通常怎样演进

mermaid
flowchart TD
    A["单体应用:统一进程和数据库"] --> B["模块化单体:代码边界清晰"]
    B --> C["按业务能力抽取少量独立服务"]
    C --> D["服务拥有数据与独立发布能力"]
    D --> E["治理平台完善:观测、弹性、安全、交付"]

2.1 单体并不是落后架构

单体具有本地调用快、事务简单、调试直接和部署单元少的优势。团队规模小、业务边界不稳定、流量不大时,结构良好的模块化单体通常比微服务更可靠。

值得评估拆分的信号包括:

  • 不同业务模块需要完全不同的扩容节奏。
  • 一个小改动必须发布整个大应用,发布窗口越来越长。
  • 团队频繁修改同一代码和同一数据表,交付相互阻塞。
  • 非核心模块耗尽线程或内存会拖垮全部业务。
  • 构建、启动和全量回归已经显著影响交付。

“代码行数很多”不是拆微服务的充分理由。领域边界尚未稳定时先拆进程,会把原本可重构的本地依赖固化成远程契约。

2.2 模块化单体是重要过渡形态

模块化单体仍是一个部署单元,但代码、依赖和数据访问有明确边界。例如订单模块只能通过库存模块公开接口使用库存能力,不能直接调用库存 Mapper。

text
order-api             对外接口与DTO
order-domain          订单规则和状态机
order-application     用例编排
order-infrastructure  数据库、消息和外部适配

先在进程内建立边界,再决定哪些边界值得跨进程,迁移成本更低。

三、什么是分布式单体

出现以下特征时,系统虽然有很多服务,却仍是分布式单体:

  1. 多个服务直接读写同一批业务表。
  2. 一次发布必须按固定顺序同时发布多个服务。
  3. 一个请求同步调用十几个服务,任何一个失败都让主流程失败。
  4. 服务之间存在 A 调 B、B 调 C、C 又调 A 的循环依赖。
  5. 契约没有兼容策略,字段修改要求所有消费者同步上线。
  6. 服务没有独立 SLO、容量、告警和负责人。

它承担了网络不确定性和数据一致性成本,却没有获得独立演进与故障隔离收益,因此往往比普通单体更难维护。

四、服务边界为什么要按业务能力划分

“用户表、订单表、库存表各拆一个服务”是按数据名词拆分,容易得到贫血 CRUD 服务。更好的问题是:哪个业务能力拥有规则、状态和数据的最终解释权?

服务拥有的能力拥有的数据不应该承担
商品服务商品信息、上下架、类目商品主数据库存扣减
定价服务价格规则、促销试算价格规则与计算快照修改订单状态
订单服务下单、取消、订单状态机订单与订单明细直接改库存表
库存服务可售量、预占、释放、扣减库存与预占流水决定支付结果
支付服务支付单、回调、退款支付与退款流水直接发货
履约服务出库、配送、签收履约单修改支付流水

同一个词在不同上下文可能有不同含义。商品上下文中的价格是当前售卖价,订单上下文中的价格是下单时的交易快照;历史订单不能每次远程查询商品当前价格。

五、数据所有权是微服务边界的硬约束

一份业务数据只有一个服务拥有写入解释权,其他服务通过 API、事件或受控查询模型获得副本。

mermaid
flowchart TD
    A["订单服务提交本地事务"] --> B["订单库成为订单事实源"]
    B --> C["发布OrderCreated事件"]
    C --> D["搜索服务更新查询索引"]
    C --> E["报表服务更新统计模型"]
    C --> F["通知服务发送消息"]

多个服务共同写一张表会产生以下问题:

  • 表结构调整无法由单一团队决策。
  • 服务可以绕过对方状态机和校验规则。
  • 数据错误无法明确责任边界。
  • 数据库锁和事务形成隐藏的运行时耦合。
  • 服务无法独立迁移存储和回滚。

在线交易主链不应依赖跨服务数据库 Join。常用替代方案:

  1. API Composition:聚合服务并行调用多个服务,适合小结果集和实时查询。
  2. CQRS 查询模型:消费事件构建宽表或搜索索引,适合高频复杂查询。
  3. 数据快照:订单保存商品名、成交价等交易时刻信息。
  4. 离线数仓:报表通过 CDC/ETL 汇总,不反向耦合在线库。

六、服务拆分的证据化流程

mermaid
flowchart TD
    A["梳理业务事件和核心用例"] --> B["识别规则、状态和数据所有者"]
    B --> C["形成候选限界上下文"]
    C --> D["分析调用频率、事务和团队边界"]
    D --> E["先在单体内强制模块边界"]
    E --> F["选择收益最大的边界抽取"]
    F --> G["建立契约、观测、回滚和SLO"]
    G --> H["灰度迁移流量并验证独立性"]
维度适合独立的信号暂缓独立的信号
业务边界规则和术语稳定,有明确负责人需求仍频繁跨模块重构
数据有清晰事实源,可用事件同步必须在同一事务中频繁改多表
容量流量和资源模型明显不同模块负载相近且规模很小
发布需要独立发布和回滚每次需求仍必须同时改全部模块
故障值得建立独立隔离边界拆出后仍必须同步强依赖
团队团队能端到端负责没有服务 Owner 和运维能力

七、绞杀者模式:不要一次性重写

以抽取订单查询服务为例:

  1. 网关按接口或租户把少量流量路由到新服务。
  2. 新服务先只读,通过 CDC 构建自己的查询模型。
  3. 使用影子流量比较新旧结果,影子请求不得产生副作用。
  4. 指标稳定后逐步扩大读流量。
  5. 再迁写链路,期间保持单一事实源和兼容事件。
  6. 旧路径无流量且完成回滚观察期后再删除。
mermaid
flowchart TD
    A["网关接收订单查询"] --> B{"迁移路由规则"}
    B -- "旧路径" --> C["单体订单模块"]
    B -- "灰度路径" --> D["新订单查询服务"]
    C --> E["比较结果、延迟和错误率"]
    D --> E
    E --> F{"是否达到验收门槛"}
    F -- "是" --> G["扩大灰度比例"]
    F -- "否" --> H["回切旧路径并定位"]

八、服务独立发布依赖契约兼容

使用 Expand–Migrate–Contract:生产者先增加新字段或新接口并保留旧能力;消费者逐个迁移并通过契约测试;确认旧消费者全部退出后再删除旧字段。

数据库也遵循相同思路。先加可空列或新表、让双版本代码兼容,再回填和切读,最后删除旧列。发布时直接重命名字段,会让仍在运行的旧实例失败,灰度和回滚都无法成立。

九、代码 Demo:用模块接口阻止跨边界写库

java
public interface InventoryReservationService {
    ReservationResult reserve(String requestId, long skuId, int quantity);
    void release(String requestId);
}
java
public final class CreateOrderUseCase {
    private final OrderRepository orderRepository;
    private final InventoryReservationService inventoryService;

    public CreateOrderUseCase(OrderRepository orderRepository,
                              InventoryReservationService inventoryService) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
    }

    public OrderResult create(CreateOrderCommand command) {
        ReservationResult reservation = inventoryService.reserve(
                command.requestId(), command.skuId(), command.quantity());
        if (!reservation.success()) {
            return OrderResult.rejected(reservation.reason());
        }
        return orderRepository.save(Order.create(command, reservation.id()));
    }
}

模块化单体阶段,接口实现可以是本地 Bean;抽成远程服务后,替换为 RPC Adapter。业务用例不直接依赖 Feign、Dubbo 或 Mapper,协议变化就不会污染领域规则。

上述写法没有自动解决“库存预占成功、订单保存失败”。跨边界后仍需释放补偿、TCC、Saga 或事务消息完成收敛,不能把接口抽象误认为分布式事务。

十、常见反模式与后果

反模式为什么危险改进
按数据库表一表一服务调用碎片化、缺少业务内聚按业务能力和状态机聚合
所有服务共享数据库绕过业务规则、无法独立发布单一数据所有者,通过契约同步
同步调用链过长延迟相加、可用率相乘缩短主链,副作用异步化
每个方法都做远程接口Chatty RPC,失败点暴增设计粗粒度业务用例接口
一次性重写无法持续比较和快速回滚绞杀者模式分阶段迁移
没有服务所有者告警、容量和数据问题无人闭环建立 Owner、SLO 和 Runbook

十一、架构耦合排查 Runbook

  1. 从变更记录统计总是共同发布的服务集合。
  2. 从 Trace 统计同步调用边数量、深度和 P95/P99。
  3. 从数据库审计确认哪些服务账号写同一张表。
  4. 从依赖图查循环调用和公共 Jar 的反向依赖。
  5. 从契约仓库确认哪些接口没有兼容窗口。
  6. 选择一个高频耦合点,先建立单一数据所有者和兼容 API。
  7. 使用事件或查询模型替代跨库读取。
  8. 用独立发布和故障隔离演练证明边界真实存在。

十二、怎样识别限界上下文

限界上下文不是画几个框,而是找到“同一个词在什么范围内有稳定含义、由谁负责解释、状态怎样变化”。常用识别方法是从业务事件和状态机开始。

mermaid
flowchart TD
    A["收集核心业务事件"] --> B["按业务目标聚类"]
    B --> C["找每组事件里的状态机"]
    C --> D["识别谁拥有规则和事实源"]
    D --> E["形成候选限界上下文"]
    E --> F["用调用频率、事务范围和团队边界验证"]

以电商或医疗资产平台为例:

事件可能归属事实源
OrderCreated订单上下文订单库
StockReserved库存上下文库存库和预占流水
PaymentConfirmed支付上下文支付流水
AssetCollected采集上下文采集批次和原始报文
AssetNormalized资产上下文资产主数据
SearchIndexed搜索上下文ES索引视图,不是事实源

判断边界时重点看四件事:

  1. 术语是否稳定:同一个词在两个场景含义不同,通常是边界信号。比如“价格”在商品服务是当前售价,在订单服务是成交快照。
  2. 状态机是否独立:订单取消、支付退款、库存释放有各自合法状态流转,不能让别的服务直接改表绕过状态机。
  3. 规则由谁解释:库存服务解释可售、预占、释放;订单服务解释订单生命周期。
  4. 数据由谁写入:一个事实只能有一个写入所有者,否则出现冲突时无法确定谁对。

十三、服务粒度怎样判断

服务不是越小越好。粒度过粗会发布和容量耦合,粒度过细会产生 Chatty RPC、事务分散和排查困难。

现象可能说明
两个模块总是一起改、一起发可能不该拆成两个服务
两个模块事务强绑定且无法补偿可能应保留同一事实源或本地事务
一个模块流量远高于其他模块可能适合单独扩容
一个模块故障经常拖垮全局可能需要独立故障域
一个服务只有CRUD没有规则可能拆得太碎
一个服务拥有多个不相关状态机可能拆得太粗

一个实用评分表:

维度高分信号低分信号
业务内聚有独立规则、状态机和负责人只是某几张表的薄封装
数据所有权有明确事实源多服务共同写同一表
发布价值能独立发布并降低风险每次仍要协调其他服务
容量差异资源模型明显不同负载与主应用一致
故障隔离独立后能阻断故障扩散失败仍让主链路不可用
契约稳定接口语义稳定可版本化需求每天重构字段含义

拆分前可以先问一句:如果这个服务今天独立宕机,其他服务是否知道怎样降级、补偿或停止相关功能? 如果答案是否定的,说明边界、契约和故障策略还没准备好。

十四、共享数据库怎样迁移

共享数据库是很多老系统迁微服务的最大阻碍。不能一刀切把表复制出去,否则会出现双写、数据漂移和回滚困难。

推荐迁移路径:

mermaid
flowchart TD
    A["识别表的写入服务账号和SQL"] --> B["指定唯一写入所有者"]
    B --> C["其他服务改为只读或调用API"]
    C --> D["新增事件或Outbox同步副本"]
    D --> E["消费者构建查询模型"]
    E --> F["灰度切读新查询模型"]
    F --> G["禁止跨服务直连数据库"]

迁移阶段:

阶段目标注意
盘点查清谁读写哪些表用SQL审计、账号、慢日志和代码扫描
收口写入每张业务表指定唯一写Owner先拦截新写入,不急着拆库
暴露契约其他服务通过API或事件拿数据避免跨库Join变成跨服务雪崩
构建副本查询方用事件、CDC或定时同步建读模型副本不是事实源
切读灰度把读流量切到新模型比对结果、延迟和缺失率
物理拆分最后再拆库、账号和权限保留回滚窗口

错误做法:

text
订单服务、报表服务、搜索服务都直接 update order 表

正确方向:

text
订单服务独占订单写入
报表服务消费订单事件
搜索服务消费订单事件或CDC
其他服务通过订单API查询必要事实

十五、防腐层:不要让老系统模型污染新服务

迁移时新服务经常要调用老单体或第三方系统。不要把老系统字段、状态码和异常直接渗透到新领域模型里,应建立防腐层。

mermaid
flowchart TD
    A["新订单服务"] --> B["防腐层Adapter"]
    B --> C["老单体接口或第三方接口"]
    C --> D["老状态码、字段和异常"]
    D --> B
    B --> E["转换为新领域对象和稳定错误语义"]

防腐层负责:

  1. 字段名转换。
  2. 枚举和状态映射。
  3. 错误码和异常分类。
  4. 超时、重试、幂等和限流。
  5. 老接口兼容逻辑。
  6. 日志脱敏和 Trace 关联。

Java 8 示例:

java
public final class LegacyInventoryAdapter implements InventoryReservationService {
    private final LegacyInventoryClient legacyClient;

    public LegacyInventoryAdapter(LegacyInventoryClient legacyClient) {
        this.legacyClient = legacyClient;
    }

    @Override
    public ReservationResult reserve(String requestId, long skuId, int quantity) {
        LegacyReserveResponse response = legacyClient.freeze(requestId, skuId, quantity);
        if ("00".equals(response.getCode())) {
            return ReservationResult.success(response.getFreezeNo());
        }
        if ("STOCK_NOT_ENOUGH".equals(response.getCode())) {
            return ReservationResult.rejected("STOCK_NOT_ENOUGH");
        }
        if ("TIMEOUT".equals(response.getCode())) {
            return ReservationResult.unknown("LEGACY_TIMEOUT");
        }
        return ReservationResult.rejected("LEGACY_REJECTED");
    }
}

这样领域层只认识 success/rejected/unknown,不会被老系统 000199 这类代码污染。

十六、循环依赖怎么治理

循环调用是分布式单体的重要信号。

mermaid
flowchart TD
    A["订单服务"] --> B["库存服务"]
    B --> C["活动服务"]
    C --> A

循环依赖的风险:

  1. 发布顺序被锁死。
  2. 启动和注册互相等待。
  3. 超时和重试形成环形放大。
  4. 领域责任不清。
  5. 出问题时没有清晰 Owner。

治理方式:

方法适用场景
上移编排由应用服务或流程编排器统一协调
领域事件A完成事实后发布事件,B异步响应
查询模型C不回调A,而是订阅A事件构建本地读模型
合并边界两个服务总是互相强依赖,可能应合并
引入防腐层第三方或老系统不可控时隔离模型

循环依赖不是靠“把 Feign 改成 MQ”自动解决。MQ 只是改变通信方式,如果事件语义仍是互相命令、没有幂等和状态机,依然会得到异步版分布式单体。

十七、服务边界验收清单

一个服务边界至少要能通过这些问题:

问题合格答案
这个服务拥有什么业务能力能用业务语言描述,不是表名堆砌
它拥有哪份事实数据有明确写入所有者
其他服务怎样获取它的数据API、事件、查询模型或快照
它能否独立发布有契约兼容和回滚方案
它失败时影响谁有SLO、降级和告警
它是否和其他服务共享库表不共享写入,读副本受控
它的接口粒度是否合适不Chatty,围绕业务用例
它是否存在循环依赖没有同步强循环
它是否有Owner有团队负责功能、数据、容量和事故

十八、关联知识点