从单体应用到微服务:后端系统拆分的基本方法

摘要

微服务并不是把一个项目简单拆成几个 Spring Boot 应用。真正的微服务改造,需要重新设计业务边界、数据所有权、服务通信、部署方式、故障处理和团队协作模式。

单体应用在早期通常开发效率高、部署简单、事务边界清晰,但随着业务增长,代码耦合、发布风险、团队协作和系统扩展会逐渐变得困难。微服务可以让不同业务能力独立演进,却也会引入网络延迟、数据一致性、分布式事务、链路追踪和运维复杂度。

本文从一个包含用户、商品、订单和支付功能的单体系统出发,介绍微服务拆分前的判断方法、领域边界设计、数据库拆分、同步与异步通信、迁移步骤和治理能力。读完本文后,你应该能够:

  • 判断一个单体应用是否真的需要拆分;
  • 使用业务边界而不是代码数量设计服务;
  • 规划服务之间的调用和数据所有权;
  • 避免共享数据库和分布式事务带来的常见问题;
  • 设计从单体到微服务的渐进式迁移路径;
  • 了解微服务上线后必须补齐的治理能力。

一、背景与问题

1. 单体应用是什么

单体应用通常将多个业务模块打包成一个可部署单元:

text 复制代码
一个代码仓库
  -> 一个构建产物
      -> 一个进程
          -> 一个数据库

在电商系统中,用户、商品、订单、库存和支付可能都在同一个应用内:

text 复制代码
Web 层
  -> 用户模块
  -> 商品模块
  -> 订单模块
  -> 库存模块
  -> 支付模块
  -> 数据访问层
  -> 一个数据库

单体不代表设计差。模块边界清晰、团队规模较小、系统规模适中的单体应用,往往比过早拆分的微服务更容易开发和运维。

2. 单体应用什么时候开始出现问题

常见信号包括:

  • 修改一个小功能需要重新发布整个系统;
  • 不同团队频繁修改同一批代码;
  • 某个模块的流量增长拖慢所有模块;
  • 应用启动和回归测试耗时越来越长;
  • 数据库表被多个模块随意读写;
  • 一个模块升级依赖导致其他模块受到影响;
  • 故障影响范围无法隔离;
  • 不同业务需要不同的扩容策略;
  • 发布窗口和回滚风险持续增加。

这些信号说明系统的边界可能已经不适合当前规模,但不一定意味着必须立即微服务化。也可以先通过模块化、代码分层、数据库治理和发布优化解决一部分问题。

3. 微服务解决什么问题

微服务通常希望实现:

text 复制代码
业务能力独立
  -> 服务独立构建
  -> 服务独立部署
  -> 服务独立扩容
  -> 服务故障隔离
  -> 团队独立负责

例如:

text 复制代码
用户服务
商品服务
订单服务
库存服务
支付服务
通知服务

每个服务围绕一个业务能力负责,而不是围绕一个技术组件随意切分。

4. 微服务带来的新问题

拆分以后,原来的进程内调用变成网络调用:

text 复制代码
单体:
订单模块 -> 库存模块
         直接方法调用

微服务:
订单服务 -> 网络 -> 库存服务

随之而来的问题包括:

  • 网络可能超时或失败;
  • 服务之间有延迟;
  • 请求可能重复到达;
  • 数据不再共享一个事务;
  • 服务发现和配置变复杂;
  • 日志需要跨服务关联;
  • 部署和监控对象数量增加;
  • 一个请求可能依赖多个下游服务。

微服务的价值必须大于它引入的复杂度,不能把拆分本身当成目标。

二、核心概念

1. 服务边界

服务边界是一个业务能力的责任边界。它应该回答:

  • 这个服务负责什么业务规则;
  • 哪些数据归它所有;
  • 哪些操作由它决定;
  • 其他服务如何请求它;
  • 它需要发布什么事件;
  • 它对外承诺什么接口。

一个好的服务边界通常具有:

  • 高内聚:相关业务规则和数据放在一起;
  • 低耦合:跨服务依赖尽量少;
  • 独立演进:可以单独发布和扩展;
  • 清晰所有权:某类数据只有一个权威服务。

2. 按业务能力拆分,而不是按技术层拆分

不推荐按技术层拆分:

text 复制代码
用户 Controller 服务
订单 Controller 服务
公共 Service 服务
数据库 Service 服务

这种拆分会让一次业务请求跨越很多技术服务,反而增加耦合。

更合理的是按业务能力拆分:

text 复制代码
用户服务:用户资料、账户状态
商品服务:商品信息、价格、上下架
订单服务:订单生命周期和订单金额
库存服务:库存数量和扣减规则
支付服务:支付单和支付状态

每个服务内部仍然可以有 Controller、Service 和 Repository,但服务边界应首先由业务职责决定。

3. 领域驱动设计中的限界上下文

限界上下文可以理解为一组拥有明确模型和规则的业务边界。同一个词在不同上下文中可能有不同含义:

概念 用户上下文 订单上下文 支付上下文
用户 账户和身份 下单人快照 付款人
商品 收藏对象 购买条目 支付描述
状态 账户状态 订单状态 支付状态
金额 账户额度 订单应付金额 实际支付金额

拆分时不能因为多个模块都使用"用户"这个词,就让所有服务共享同一个 User Entity。服务可以保存自己需要的用户快照,通过用户服务接口或事件获取必要信息。

4. 数据所有权

微服务中的核心原则是:

text 复制代码
一个业务数据有一个权威拥有者
其他服务通过 API、事件或只读副本获取数据

例如:

  • 商品服务拥有商品名称和商品状态;
  • 库存服务拥有可售库存;
  • 订单服务拥有订单状态和订单金额;
  • 支付服务拥有支付状态;
  • 用户服务拥有账户基础信息。

其他服务可以保存必要的快照,但不能绕过拥有者直接修改原始数据。

5. 服务通信方式

服务之间常见两类通信:

方式 特点 适用场景
同步 HTTP/RPC 调用关系直观,实时返回 查询、必须立即得到结果
异步消息 解耦、削峰、最终一致 领域事件、通知、耗时任务
批量接口 减少网络往返 列表查询和批量处理
本地缓存 降低重复调用 稳定、可短暂过期的数据

选择通信方式时,要看业务是否必须同步得到结果。不是所有跨服务调用都应该使用同步 HTTP。

6. API 契约

服务之间通信需要稳定契约:

text 复制代码
请求路径和方法
请求参数和响应字段
状态码和错误码
超时和重试约定
幂等要求
权限要求
版本兼容策略

API 契约不是 Controller 方法签名的简单复制。服务之间应该隐藏内部实体和数据库结构,只暴露稳定的业务接口。

7. 事件驱动

事件表示某件已经发生的业务事实:

text 复制代码
OrderCreated
PaymentSucceeded
StockDeducted
OrderCancelled

事件的特点:

  • 过去发生的事实,不是命令;
  • 生产者不必知道所有消费者;
  • 消费者可以异步处理;
  • 适合传播状态变化;
  • 需要考虑重复消费和顺序。

事件不是万能的。对必须立即得到结果的校验,仍然需要同步调用。

8. 分布式事务与最终一致性

单体中可以使用一个数据库事务:

text 复制代码
创建订单
  -> 扣减库存
  -> 创建支付单
  -> 一个数据库事务提交

拆分后,不同服务通常拥有不同数据库,无法简单使用同一个本地事务。常见方案包括:

  • 可靠事件和最终一致性;
  • 本地消息表;
  • Saga 编排或协同;
  • 补偿事务;
  • TCC;
  • 尽量避免跨服务强一致事务。

方案选择取决于业务能否接受短暂中间状态、补偿是否可行以及失败后的人工处理成本。

三、工作原理

1. 单体到微服务的演进

一个渐进式演进过程可以是:
#mermaid-svg-Qefro14KhWNizXUX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qefro14KhWNizXUX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qefro14KhWNizXUX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qefro14KhWNizXUX .error-icon{fill:#552222;}#mermaid-svg-Qefro14KhWNizXUX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qefro14KhWNizXUX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qefro14KhWNizXUX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qefro14KhWNizXUX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qefro14KhWNizXUX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qefro14KhWNizXUX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qefro14KhWNizXUX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qefro14KhWNizXUX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qefro14KhWNizXUX .marker.cross{stroke:#333333;}#mermaid-svg-Qefro14KhWNizXUX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qefro14KhWNizXUX p{margin:0;}#mermaid-svg-Qefro14KhWNizXUX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Qefro14KhWNizXUX .cluster-label text{fill:#333;}#mermaid-svg-Qefro14KhWNizXUX .cluster-label span{color:#333;}#mermaid-svg-Qefro14KhWNizXUX .cluster-label span p{background-color:transparent;}#mermaid-svg-Qefro14KhWNizXUX .label text,#mermaid-svg-Qefro14KhWNizXUX span{fill:#333;color:#333;}#mermaid-svg-Qefro14KhWNizXUX .node rect,#mermaid-svg-Qefro14KhWNizXUX .node circle,#mermaid-svg-Qefro14KhWNizXUX .node ellipse,#mermaid-svg-Qefro14KhWNizXUX .node polygon,#mermaid-svg-Qefro14KhWNizXUX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Qefro14KhWNizXUX .rough-node .label text,#mermaid-svg-Qefro14KhWNizXUX .node .label text,#mermaid-svg-Qefro14KhWNizXUX .image-shape .label,#mermaid-svg-Qefro14KhWNizXUX .icon-shape .label{text-anchor:middle;}#mermaid-svg-Qefro14KhWNizXUX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Qefro14KhWNizXUX .rough-node .label,#mermaid-svg-Qefro14KhWNizXUX .node .label,#mermaid-svg-Qefro14KhWNizXUX .image-shape .label,#mermaid-svg-Qefro14KhWNizXUX .icon-shape .label{text-align:center;}#mermaid-svg-Qefro14KhWNizXUX .node.clickable{cursor:pointer;}#mermaid-svg-Qefro14KhWNizXUX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Qefro14KhWNizXUX .arrowheadPath{fill:#333333;}#mermaid-svg-Qefro14KhWNizXUX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Qefro14KhWNizXUX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Qefro14KhWNizXUX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qefro14KhWNizXUX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Qefro14KhWNizXUX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qefro14KhWNizXUX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Qefro14KhWNizXUX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Qefro14KhWNizXUX .cluster text{fill:#333;}#mermaid-svg-Qefro14KhWNizXUX .cluster span{color:#333;}#mermaid-svg-Qefro14KhWNizXUX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Qefro14KhWNizXUX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Qefro14KhWNizXUX rect.text{fill:none;stroke-width:0;}#mermaid-svg-Qefro14KhWNizXUX .icon-shape,#mermaid-svg-Qefro14KhWNizXUX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qefro14KhWNizXUX .icon-shape p,#mermaid-svg-Qefro14KhWNizXUX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Qefro14KhWNizXUX .icon-shape .label rect,#mermaid-svg-Qefro14KhWNizXUX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qefro14KhWNizXUX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Qefro14KhWNizXUX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Qefro14KhWNizXUX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 模块化单体
识别业务边界
稳定内部接口
抽离一个低风险服务
建立服务通信和观测
迁移数据所有权
逐步拆分其他能力

不建议一次性把所有模块拆成几十个服务。每拆一个服务,都应该获得明确收益,并能验证迁移是否成功。

2. 服务边界示例

假设原单体中包含以下模块:

text 复制代码
UserModule
ProductModule
OrderModule
InventoryModule
PaymentModule
NotificationModule

可以先形成候选边界:

text 复制代码
用户服务
  -> 用户资料、账户状态、用户认证关联

商品服务
  -> 商品信息、价格、上下架

库存服务
  -> 库存、预占、释放、扣减

订单服务
  -> 订单创建、状态流转、订单查询

支付服务
  -> 支付单、支付状态、支付回调

通知服务
  -> 短信、邮件、站内通知

候选边界还需要结合团队职责、数据依赖和发布频率验证。

3. 从同步调用到异步事件

订单创建可能经历:
通知服务 支付服务 库存服务 订单服务 客户端 通知服务 支付服务 库存服务 订单服务 客户端 #mermaid-svg-60dRIV4tMKifKlSt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-60dRIV4tMKifKlSt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-60dRIV4tMKifKlSt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-60dRIV4tMKifKlSt .error-icon{fill:#552222;}#mermaid-svg-60dRIV4tMKifKlSt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-60dRIV4tMKifKlSt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-60dRIV4tMKifKlSt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-60dRIV4tMKifKlSt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-60dRIV4tMKifKlSt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-60dRIV4tMKifKlSt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-60dRIV4tMKifKlSt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-60dRIV4tMKifKlSt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-60dRIV4tMKifKlSt .marker.cross{stroke:#333333;}#mermaid-svg-60dRIV4tMKifKlSt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-60dRIV4tMKifKlSt p{margin:0;}#mermaid-svg-60dRIV4tMKifKlSt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-60dRIV4tMKifKlSt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-60dRIV4tMKifKlSt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-60dRIV4tMKifKlSt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-60dRIV4tMKifKlSt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-60dRIV4tMKifKlSt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-60dRIV4tMKifKlSt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-60dRIV4tMKifKlSt .sequenceNumber{fill:white;}#mermaid-svg-60dRIV4tMKifKlSt #sequencenumber{fill:#333;}#mermaid-svg-60dRIV4tMKifKlSt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-60dRIV4tMKifKlSt .messageText{fill:#333;stroke:none;}#mermaid-svg-60dRIV4tMKifKlSt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-60dRIV4tMKifKlSt .labelText,#mermaid-svg-60dRIV4tMKifKlSt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-60dRIV4tMKifKlSt .loopText,#mermaid-svg-60dRIV4tMKifKlSt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-60dRIV4tMKifKlSt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-60dRIV4tMKifKlSt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-60dRIV4tMKifKlSt .noteText,#mermaid-svg-60dRIV4tMKifKlSt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-60dRIV4tMKifKlSt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-60dRIV4tMKifKlSt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-60dRIV4tMKifKlSt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-60dRIV4tMKifKlSt .actorPopupMenu{position:absolute;}#mermaid-svg-60dRIV4tMKifKlSt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-60dRIV4tMKifKlSt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-60dRIV4tMKifKlSt .actor-man circle,#mermaid-svg-60dRIV4tMKifKlSt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-60dRIV4tMKifKlSt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建订单请求预占库存返回预占结果创建支付单返回支付单返回待支付订单发布支付成功事件发布订单状态变化事件发送通知

这里可以把支付成功后的通知改成异步事件,让订单服务不必同步等待通知服务完成。

4. 数据迁移的核心流程

从共享数据库迁移到服务独立数据库,不能只复制表:

text 复制代码
梳理表和字段所有者
  -> 确定服务边界
  -> 建立服务内部模型
  -> 迁移历史数据
  -> 建立双写或变更同步
  -> 校验新旧数据
  -> 切换读取
  -> 停止旧写入
  -> 删除共享访问

双写期间需要处理:

  • 一边写成功、一边写失败;
  • 两边写入顺序不一致;
  • 重复写入;
  • 数据格式不同;
  • 历史数据缺失;
  • 补偿和校验。

如果无法可靠保证双写一致,应优先采用事件同步或分阶段迁移,避免长期维护不可控的双写逻辑。

5. 服务调用的超时与重试

一次同步调用至少要明确:

text 复制代码
连接超时
读取超时
总调用超时
最大重试次数
退避时间
是否允许重试
降级结果

不是所有失败都适合重试:

  • 查询通常比较容易重试;
  • 创建操作必须具备幂等键后才能重试;
  • 支付请求需要严格遵循业务幂等;
  • 超时不代表服务端没有执行成功;
  • 重试可能放大下游压力。

6. 服务故障的传播

如果订单服务同步依赖库存、优惠、地址和支付服务:

text 复制代码
订单请求
  -> 库存慢
      -> 订单线程等待
  -> 优惠慢
      -> 更多线程等待
  -> 连接池耗尽
      -> 订单服务超时
  -> 上游重试
      -> 故障扩大

微服务必须配合:

  • 超时;
  • 限流;
  • 熔断;
  • 舱壁隔离;
  • 降级;
  • 异步化;
  • 依赖分级;
  • 资源池隔离。

四、实战示例

下面以订单服务调用库存服务为例,展示 API 契约、幂等、事件和补偿设计。

1. 定义服务接口

库存服务可以提供预占接口:

http 复制代码
POST /api/inventories/reservations
Idempotency-Key: order-10001-create

{
  "orderId": 10001,
  "items": [
    {
      "productId": 10,
      "quantity": 2
    }
  ]
}

响应:

json 复制代码
{
  "reservationId": "reservation-90001",
  "status": "RESERVED",
  "expiresAt": "2026-09-05T12:00:00Z"
}

接口契约需要明确:

  • orderId 是否必须唯一;
  • 重复请求返回什么;
  • 库存不足返回什么错误码;
  • 预占多久自动过期;
  • 如何释放预占;
  • 订单取消后谁负责触发释放。

2. 订单服务调用库存服务

java 复制代码
@Service
public class OrderApplicationService {

    private final InventoryClient inventoryClient;
    private final OrderRepository orderRepository;

    public OrderApplicationService(
            InventoryClient inventoryClient,
            OrderRepository orderRepository) {
        this.inventoryClient = inventoryClient;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public OrderResult createOrder(
            CreateOrderCommand command) {

        Order order = orderRepository.createPending(
                command.userId(),
                command.items()
        );

        ReservationResult reservation =
                inventoryClient.reserve(
                        new ReserveInventoryCommand(
                                order.id(),
                                command.items()
                        ),
                        "order-" + order.id()
                );

        if (!reservation.success()) {
            orderRepository.markFailed(
                    order.id(),
                    "INSUFFICIENT_STOCK"
            );
            return OrderResult.failed(
                    order.id(),
                    "库存不足"
            );
        }

        orderRepository.markReserved(
                order.id(),
                reservation.reservationId()
        );

        return OrderResult.success(order.id());
    }
}

这里的 @Transactional 只覆盖订单服务自己的数据库,不能自动覆盖库存服务。库存调用失败时,订单状态需要通过本地事务和明确的业务状态表达。

3. 设计客户端超时和幂等

java 复制代码
@Component
public class InventoryClient {

    private final RestClient restClient;

    public InventoryClient(RestClient.Builder builder) {
        this.restClient = builder
                .baseUrl("http://inventory-service")
                .build();
    }

    public ReservationResult reserve(
            ReserveInventoryCommand command,
            String idempotencyKey) {

        return restClient.post()
                .uri("/api/inventories/reservations")
                .header("Idempotency-Key", idempotencyKey)
                .body(command)
                .retrieve()
                .body(ReservationResult.class);
    }
}

实际项目还需要在 HTTP 客户端层配置连接超时、读取超时、状态码映射和重试策略。创建类接口必须传递稳定幂等键,避免客户端超时重试导致重复预占。

4. 库存服务实现幂等

库存服务可以保存请求幂等记录:

text 复制代码
idempotency_key
service_name
request_hash
response_body
status
created_at
expires_at

处理流程:

text 复制代码
收到请求
  -> 根据幂等键查记录
  -> 已成功:返回原响应
  -> 处理中:返回处理中状态或等待
  -> 已失败:根据策略重试或返回原错误
  -> 未找到:执行业务并保存结果

如果同一个幂等键对应的请求内容发生变化,应返回参数冲突,而不是静默复用原结果。

5. 使用事件完成后续流程

订单服务可以发布订单已预占事件:

json 复制代码
{
  "eventId": "event-10001",
  "eventType": "OrderInventoryReserved",
  "aggregateId": "10001",
  "occurredAt": "2026-09-05T12:00:00Z",
  "payload": {
    "orderId": 10001,
    "reservationId": "reservation-90001"
  }
}

支付服务、通知服务或履约服务可以订阅这个事件。事件消费者必须支持重复消费:

java 复制代码
@Component
public class InventoryReservedHandler {

    private final EventRecordRepository eventRecordRepository;
    private final PaymentService paymentService;

    @Transactional
    public void handle(OrderInventoryReserved event) {
        if (eventRecordRepository.exists(event.eventId())) {
            return;
        }

        paymentService.createPayment(
                event.orderId(),
                event.reservationId()
        );

        eventRecordRepository.markProcessed(
                event.eventId()
        );
    }
}

事件去重记录和业务更新最好放在同一个本地事务中,避免业务处理成功但去重记录没有保存。

6. 本地消息表

如果订单数据库和消息系统之间没有统一事务,可以使用本地消息表:

sql 复制代码
CREATE TABLE outbox_event (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    event_id VARCHAR(64) NOT NULL UNIQUE,
    event_type VARCHAR(100) NOT NULL,
    aggregate_id VARCHAR(64) NOT NULL,
    payload JSON NOT NULL,
    status VARCHAR(20) NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at DATETIME NULL,
    created_at DATETIME NOT NULL,
    published_at DATETIME NULL
);

订单创建时:

text 复制代码
本地事务
  -> 保存订单
  -> 保存 outbox_event
  -> 一起提交

后台发布器:

text 复制代码
查询待发布事件
  -> 发送到消息系统
  -> 标记已发布
  -> 失败则增加重试次数

消息可能重复发布,因此消费者必须幂等。Outbox 解决的是"业务数据和待发布事件不一致"的问题,不会自动解决所有分布式事务问题。

7. 统一错误响应

服务间错误响应应包含稳定的错误码:

json 复制代码
{
  "code": "INVENTORY_NOT_ENOUGH",
  "message": "库存不足",
  "requestId": "req-202609050001",
  "retryable": false
}

不要让调用方依赖异常堆栈、数据库错误字符串或可变的 message。retryable 可以帮助调用方区分是否允许重试,但最终仍需结合接口幂等性判断。

8. 迁移一个模块的步骤

以商品服务为例:

text 复制代码
1. 梳理商品模块的表、接口和业务规则
2. 在单体内部建立 ProductFacade
3. 禁止其他模块直接访问商品表
4. 为 ProductFacade 编写契约测试
5. 创建独立商品服务
6. 将商品数据同步到新库
7. 先灰度读取新服务
8. 校验新旧结果
9. 切换写入和数据所有权
10. 删除单体中的旧实现

先在单体内部建立边界,能够降低后续抽离难度,也可以提前发现模块间隐藏耦合。

五、常见问题与实践建议

1. 是否所有系统都适合微服务

不一定。以下情况可以优先保持模块化单体:

  • 团队规模较小;
  • 业务还在快速试错;
  • 访问规模不大;
  • 模块边界尚未稳定;
  • 没有完善的部署和监控能力;
  • 事务一致性要求很高;
  • 服务拆分收益不明确。

微服务不是成熟度徽章。能够稳定交付和快速演进,比服务数量更多更重要。

2. 服务是不是越小越好

服务过小会导致:

  • 调用链变长;
  • 网络开销增加;
  • 数据一致性更复杂;
  • 部署单元数量膨胀;
  • 团队需要维护更多接口;
  • 故障定位困难。

合理边界应该让服务内部有足够业务内聚,同时避免一个业务动作跨越过多服务。拆分粒度要结合团队和系统规模动态调整。

3. 为什么共享数据库会破坏微服务边界

如果多个服务直接访问同一张表:

text 复制代码
订单服务直接修改库存表
商品服务直接修改订单表
支付服务直接查询订单内部字段

会导致:

  • 数据所有权不清晰;
  • 表结构变更互相影响;
  • 服务无法独立发布;
  • 本地事务边界失去意义;
  • 权限难以控制;
  • 未来拆库成本很高。

过渡阶段可以暂时共享数据库,但应通过访问层、表权限和迁移计划限制这种状态,不能把它当作长期架构。

4. 是否应该为每个服务准备独立数据库

独立数据库有利于数据隔离和独立演进,但也会增加:

  • 数据同步;
  • 分布式事务;
  • 运维实例;
  • 备份恢复;
  • 跨服务查询;
  • 数据分析整合。

可以先做到"逻辑所有权独立",再根据规模和发布需求物理拆库。不要为了形式上的独立数据库而过早承担不必要的复杂度。

5. 服务间调用为什么不能直接共享 Entity

共享 Entity 会造成:

  • 一个服务修改字段影响所有调用方;
  • 内部字段暴露;
  • 依赖同一版本类库;
  • 数据模型无法独立演进;
  • 序列化兼容变复杂。

服务之间应使用 DTO、Command、Query 和 Event 等契约对象,必要时进行显式映射。

6. 重试为什么可能造成重复操作

网络超时只说明调用方没有及时收到响应,不说明服务端没有执行成功:

text 复制代码
客户端发送创建请求
  -> 服务端执行成功
  -> 响应在网络中丢失
  -> 客户端认为失败并重试
  -> 服务端再次执行

因此创建订单、支付、扣库存等操作必须设计幂等键、业务唯一约束或状态机,不能只依赖调用方"不要重复请求"。

7. 分布式事务应该怎么选

先问业务能否接受中间状态:

  • 可以接受:优先使用事件、状态机和补偿;
  • 不可接受:评估 Saga、TCC 或重新设计边界;
  • 只涉及同一数据库:优先使用本地事务;
  • 依赖外部系统:考虑幂等、对账和人工补偿。

不要为了追求"看起来像一个事务",把多个远程调用强行串在长事务中。

8. 调用链变长怎么办

优化方向:

  • 合并批量接口;
  • 使用并行调用;
  • 将非核心流程异步化;
  • 增加本地缓存;
  • 减少跨服务查询;
  • 设置依赖分级;
  • 对弱依赖提供降级;
  • 用聚合服务封装复杂编排。

并行调用需要考虑线程池隔离和下游承载能力,不能简单地把所有调用都改成并行。

9. 如何处理跨服务查询

不要让一个服务直接连接另一个服务的数据库。可选方式:

  • 调用对方查询 API;
  • 使用事件构建本地只读视图;
  • 使用数据同步任务;
  • 由查询聚合服务统一编排;
  • 对报表场景进入独立分析库。

实时一致性和查询性能需要权衡。面向用户的核心交易查询,通常优先保持业务边界;面向报表的复杂跨域查询,可以使用数据同步和分析模型。

10. 服务发现和配置中心是不是必需

服务数量少、部署环境简单时,可以先使用固定地址或平台服务发现。随着实例动态扩缩容和环境增多,再引入统一服务发现、配置管理和密钥管理。

无论是否使用配置中心,都要做到:

  • 配置与代码分离;
  • 敏感信息不进代码仓库;
  • 配置变更可审计;
  • 配置错误可快速回滚;
  • 关键配置启动时校验。

11. 日志中只看服务本地 requestId 可以吗

不够。一次请求可能经过多个服务,需要使用贯穿全链路的 traceId:

text 复制代码
网关
  -> 订单服务
      -> 库存服务
      -> 支付服务
          -> 消息消费者

每个服务可以有自己的 spanId,但必须传播 traceId。日志、指标和分布式追踪应该能够关联同一次业务请求。

12. 微服务部署后如何回滚

每个服务应具备:

  • 可重复构建的版本;
  • 独立镜像或构建产物;
  • 健康检查;
  • 灰度或分批发布;
  • 数据库迁移回滚策略;
  • API 兼容窗口;
  • 旧版本和新版本并存能力。

代码可以快速回滚,但数据库结构和消息格式可能已经变化。因此发布前要设计向后兼容,避免新版本写入旧版本无法理解的数据。

六、进阶思考

1. 模块化单体是很好的中间形态

模块化单体可以做到:

text 复制代码
一个部署单元
  + 清晰的业务模块
  + 独立的内部接口
  + 明确的数据访问边界
  + 模块级测试
  + 统一但可观察的运行环境

它可以先解决代码和责任边界问题,同时保留本地调用、单库事务和简单部署的优势。未来只有真正需要独立扩展的模块,才抽离为微服务。

2. 服务拆分应该由变化驱动

一个服务是否应该独立,通常看:

  • 它是否有独立发布频率;
  • 它是否有独立扩容需求;
  • 它是否需要不同技术栈;
  • 它是否由独立团队负责;
  • 它是否需要故障隔离;
  • 它的数据和规则是否边界清晰。

如果两个模块总是一起修改、一起发布、一起扩容,拆成两个服务可能只是增加远程调用。

3. 领域事件和状态机

订单状态通常不应该允许任意修改:

text 复制代码
PENDING
  -> RESERVED
      -> PAID
          -> COMPLETED

PENDING
  -> CANCELLED
RESERVED
  -> CANCELLED

服务可以通过状态机限制合法转换,并通过领域事件传播状态变化。状态机让补偿、重试和异常恢复更容易设计。

4. 可观测性是微服务的基础设施

至少需要:

  • 指标:请求量、错误率、延迟、资源使用;
  • 日志:结构化、带 traceId;
  • 追踪:跨服务调用链;
  • 健康检查:存活和就绪状态;
  • 告警:超时、错误、积压和资源异常;
  • 审计:关键状态和权限操作。

没有可观测性,服务拆分后只能看到"某个接口失败",却无法知道是哪个下游、哪条消息或哪个版本造成的。

5. 依赖治理

可以把依赖分级:

text 复制代码
核心依赖:失败时请求不能完成
重要依赖:失败时可以降级部分功能
弱依赖:可以异步处理
可选依赖:可以直接关闭

根据分级配置不同的超时、线程池、熔断和降级策略。不要让非核心服务占满核心业务的线程池和连接池。

6. 数据一致性与业务补偿

最终一致性需要有可执行的补偿流程:

text 复制代码
订单已创建
  -> 库存预占失败
  -> 订单标记失败
  -> 释放已预占资源
  -> 发布失败事件
  -> 重试或转人工

补偿不是简单地再调用一次接口。需要记录状态、重试次数、最后错误、下次重试时间和人工处理入口。

7. API 版本和兼容策略

服务升级时,旧调用方可能还没有同步发布。可以采用:

  • 新增字段而不是删除字段;
  • 保持旧字段一段时间;
  • 对接口进行版本化;
  • 对事件采用向后兼容格式;
  • 先升级消费者,再升级生产者;
  • 通过契约测试验证兼容性。

API 兼容不仅包括 JSON 字段,还包括状态码、错误码、分页规则、时间格式和幂等行为。

8. 微服务拆分的收益评估

每次拆分前后都应该评估:

text 复制代码
发布耗时是否降低
故障影响范围是否缩小
扩容是否更精准
团队交付是否更独立
调用延迟是否增加
运维成本是否上升
数据一致性问题是否增加
问题定位是否更容易

如果只统计服务数量,没有统计交付速度、故障恢复时间和业务稳定性,就很难判断拆分是否真的成功。

9. 微服务治理的最小清单

一个服务上线前至少应具备:

text 复制代码
明确的业务边界和数据所有权
稳定的 API 契约
超时和重试策略
幂等处理
健康检查
结构化日志和 traceId
核心指标和告警
权限认证
灰度和回滚能力
数据库迁移方案
消息重试和积压处理
故障降级和补偿流程

这些能力比注册中心、网关数量和服务数量更能决定微服务是否可维护。

结论

从单体应用到微服务,不是一次代码搬家,而是业务边界、数据所有权、通信方式、事务模型和运维能力的整体变化。单体应用适合早期快速交付,微服务适合在业务边界稳定、团队和流量规模达到一定程度后,按实际收益逐步引入。

本文的重点可以归纳为:

  • 微服务拆分的目标是独立演进、扩展和故障隔离;
  • 服务边界应优先依据业务能力和数据所有权设计;
  • 模块化单体是低风险的过渡形态;
  • 每个业务数据应有明确的权威拥有者;
  • 同步调用适合实时结果,异步事件适合解耦和最终一致;
  • 跨服务操作不能直接依赖本地数据库事务;
  • 幂等、超时、重试、熔断和补偿是微服务的基础能力;
  • 共享数据库会削弱服务边界,应设置迁移计划;
  • 可观测性和发布回滚能力必须与服务拆分同步建设;
  • 服务不是越多越好,拆分收益必须大于分布式复杂度。

下一篇可以继续学习 Spring Cloud 微服务架构,进一步了解注册中心、配置中心、网关和服务间调用如何落地。

相关推荐
萝卜鱼蛋1 小时前
微服务环境下如何避免孤儿数据:结合递归,墓碑标记与事件流
微服务
今天AI了吗2 小时前
Codex使用技巧:深度解析 Plan Mode 与 Goal Mode
java·网络·人工智能·架构·java-ee
the sun342 小时前
CMSIS标准与各种库的发展历史+哈弗架构+CPU统一编址
stm32·架构·嵌入式
天远Date Lab2 小时前
零信任架构实战:基于天远全能个人大数据报告构建自动化核心KYC网关
大数据·人工智能·架构·自动化
dishugj2 小时前
系统架构的常用建模方法和评价架构的4+1观察视角
架构·系统架构
风123456789~2 小时前
【架构专栏】第7章 系统架构设计基础知识 3/3
架构·系统架构
天空之城--2 小时前
Flutter线程模型完全指南:从架构到实战
flutter·架构
头茬韭菜3 小时前
第 1 篇:「架构鸟瞰与 Memory 初始化」—— 从 pip install 到三个工厂
jvm·架构·pip·mem0
风123456789~3 小时前
【架构专栏】第7章 系统架构设计基础知识 2/3
架构·系统架构