跳转到内容

Java 微服务架构治理实践

微服务治理不是把单体应用拆成更多进程,也不是引入注册中心、网关和 Kubernetes 后就自然完成。真正需要治理的是系统中的变化、依赖、数据和故障

如果拆分后一次需求仍然需要修改五个服务,一次故障仍然会拖垮完整调用链,团队仍然通过共享数据库完成协作,那么系统只是从单体变成了分布式单体。

一套有效的微服务架构应该持续改善以下指标:

  • 业务变化能否被限制在清晰边界内
  • 服务是否拥有独立的数据和发布决策
  • 局部故障是否会演变为全链路故障
  • 跨服务一致性是否有明确的业务语义
  • 线上问题是否能被快速发现、定位和恢复

治理的目标不是追求服务数量,而是控制系统复杂度。

开始拆分或治理前,应先获得当前系统的真实结构,而不是只阅读架构图。

需要收集的基线包括:

维度 需要确认的事实
业务 核心流程、业务不变量、状态生命周期
代码 模块依赖、公共库、循环调用、修改热点
数据 表归属、跨库查询、共享字段、数据规模
调用 同步链路、消息流向、外部系统依赖
运行 流量、延迟、错误率、资源和容量峰值
交付 构建时间、发布频率、回滚方式、故障记录

可以从最近几个月的需求和故障开始调查:

  • 哪些模块经常一起修改
  • 哪些接口一改就影响多个团队
  • 哪些表被不同服务直接读写
  • 哪些调用链最容易超时
  • 哪些问题只能通过人工查库定位

这些事实比预先设计一套“标准微服务模板”更能说明治理优先级。

服务拆分应围绕业务能力、业务规则和数据生命周期,而不是按 Controller、Service、DAO 技术分层。

判断一个边界是否合理,可以检查四个问题:

  1. 它是否拥有一组内聚的业务规则?
  2. 它是否能够独立维护核心状态?
  3. 它的变化原因是否相对单一?
  4. 它是否有明确的团队或角色负责?

以通用的交易流程为例:

Order
负责订单生命周期与商品交易状态
Receivable
负责应收建立、调整、核销与余额
Payment
负责支付请求、渠道路由与支付结果
Settlement
负责结算规则、结算单与结算状态

这些模块之间存在业务协作,但它们拥有不同的不变量和状态变化原因。将它们强行放进一个“交易服务”,会让服务内部重新形成大型单体;拆得过细,则每个业务动作都需要跨多个网络边界。

“一张表一个服务”通常会制造大量贫血服务。表只是当前存储模型,不代表业务能力。

更可靠的顺序是:

业务能力
领域规则与状态
服务边界
数据模型

如果顺序反过来,历史数据库结构会永久限制架构演进。

服务自治的基础是数据所有权。每份核心业务数据应该有唯一的写入方,其他服务通过公开契约获取信息。

基本规则包括:

  • 一个服务负责核心数据的写入规则
  • 其他服务不能绕过接口直接修改其表
  • 跨服务查询通过 API、事件副本或专用读模型完成
  • 共享字段必须说明来源、更新时间和一致性要求
  • 数据修复同样需要经过所有者提供的流程

多个服务共用数据库的短期开发成本较低,但会带来长期耦合:

  • 表结构变化无法独立发布
  • 业务规则可能在多个服务重复实现
  • 无法追踪某个字段由谁修改
  • 跨表事务掩盖了真实的服务边界
  • 数据库故障会同时影响所有服务

拆分早期可以暂时共享数据库实例,但应明确 Schema、账号和写权限边界,并逐步消除跨服务直接访问。

跨服务列表和报表不应该简单复刻单体中的多表 Join。可以根据时效性选择:

场景 建议方式
少量实时详情 同步 API 查询
高频组合查询 建立专用读模型
统计和分析 数据仓库或分析平台
可接受延迟的列表 通过事件维护查询副本

读模型允许冗余,但必须明确数据来源、更新机制和重建方式。

同步 HTTP 或 RPC 调用适合需要即时结果的场景,但每增加一层调用,就增加一层延迟和故障概率。

需要治理的不是“能否调用”,而是调用失败时系统如何工作。

每个同步依赖至少应明确:

  • 连接和请求超时时间
  • 哪些错误允许重试
  • 最大重试次数与退避策略
  • 熔断和恢复条件
  • 并发隔离与资源上限
  • 降级结果是否可接受
  • 调用方如何记录失败状态

以下链路很难稳定:

Gateway
→ Order
→ Customer
→ Account
→ Risk
→ External API

任一节点变慢都会占用上游线程和连接。优化方式不一定是把所有调用改成消息,而是重新判断:

  • 当前服务是否真的需要这些实时数据
  • 数据是否可以在本地维护只读副本
  • 是否可以把流程改成可追踪的异步状态机
  • 是否存在职责放错位置导致的反向依赖

同步链路应服务于业务语义,而不是成为跨服务取数工具。

分布式系统无法依赖一个覆盖所有服务的本地数据库事务。架构设计必须先确认业务真正需要的一致性等级。

资金扣减、库存锁定、额度占用等操作通常需要在单个服务内部保持强一致。应尽量让关键不变量落在同一个数据所有者和本地事务中。

通知、搜索索引、报表、跨域状态同步通常可以接受最终一致。关键是定义:

  • 允许多长时间的延迟
  • 用户在中间状态看到什么
  • 失败后如何重试和补偿
  • 如何发现长期未完成的流程

“最终一致”不能等同于“以后应该会一致”。它需要完整的状态和恢复机制。

一个常见问题是业务数据提交成功,但消息发送失败,导致下游永远收不到变更。

可以使用 Outbox 模式:

同一本地事务
├── 更新业务数据
└── 写入 Outbox 事件
异步发布器
└── 将 Outbox 事件发送到消息系统

发布成功后更新发送状态;失败则继续重试。这样可以保证业务数据和待发送事件同时提交。

消费端可以使用 Inbox 或业务唯一键记录处理结果:

接收消息
检查 eventId / businessKey
执行业务事务
记录消费结果

这套机制不能保证消息只投递一次,但可以让业务处理具备幂等性。

幂等是业务设计,不是中间件开关

Section titled “幂等是业务设计,不是中间件开关”

消息系统、网关或 SDK 提供的去重能力只能覆盖部分场景。真正的幂等需要业务层定义“重复请求是否代表同一个动作”。

常见幂等键包括:

  • 客户端生成的 requestId
  • 上游业务单号与操作类型
  • 支付渠道流水号
  • 消息 eventId
  • 聚合根 ID 与目标状态

实现时可以组合使用:

  • 数据库唯一约束
  • 幂等记录表
  • 条件更新
  • 状态机校验
  • 乐观锁版本号

例如,支付成功消息重复到达时,不应再次增加账户余额。处理逻辑需要确认当前支付状态、流水号和入账结果,而不是只依赖 Redis 中一个可能过期的锁。

消息名称会影响服务之间的耦合方式。

事件描述已经发生的事实:

OrderConfirmed
PaymentSucceeded
ReceivableWrittenOff

发布方不应该知道有哪些消费者。

命令要求特定接收方执行动作:

CreateReceivable
FreezeCredit
SendPaymentRequest

命令需要明确接收方、执行结果和失败语义。

如果把命令伪装成事件,发布方会暗中依赖消费者行为;如果把所有事实都设计成命令,服务之间会形成中心化流程控制。

事件至少应包含:

eventId
eventType
eventVersion
occurredAt
producer
traceId
businessKey
payload

字段变更需要兼容策略。优先新增可选字段,避免直接改变已有字段语义;无法兼容时应升级事件版本,并允许消费者逐步迁移。

跨服务业务流程通常会经历等待、失败、重试和补偿。只通过代码中的 if/else 和消息重试表达流程,很难判断当前业务处于什么状态。

显式状态机应该定义:

  • 合法状态
  • 允许的状态转换
  • 触发转换的命令或事件
  • 每次转换的前置条件
  • 超时和补偿动作

例如:

PENDING
→ PROCESSING
→ SUCCEEDED
→ FAILED
→ COMPENSATING
→ COMPENSATED

状态转换必须持久化,并记录业务单号、触发来源、时间和失败原因。这样才能支持查询、重放、补偿和审计。

只监控 CPU、内存和 JVM 堆使用量,无法判断系统是否正确完成业务。

建议建立三层指标:

  • CPU、内存、磁盘和网络
  • Pod 重启、线程池、连接池
  • JVM GC、堆和类加载
  • 请求量、错误率和延迟分位数
  • 超时、熔断、重试和限流次数
  • 消息积压、消费延迟和失败次数
  • 数据库连接、慢查询和锁等待
  • 创建、支付、核销等流程成功率
  • 长时间停留在中间状态的单据数
  • 金额或数量对账差异
  • 补偿任务和人工处理数量

每条日志和消息应携带统一的 traceId、业务单号和关键状态。技术调用链与业务状态关联后,问题定位才不需要依赖人工逐库查询。

服务可以独立部署,不代表应该随意发布。稳定的交付流程至少包含:

代码检查
单元与集成测试
制品构建
配置和迁移检查
灰度发布
指标观察
全量或回滚

数据库变更应采用向前兼容顺序:

  1. 先增加新字段或新表。
  2. 发布同时兼容新旧结构的代码。
  3. 完成历史数据迁移。
  4. 切换读取和写入逻辑。
  5. 确认无旧版本使用后再清理。

直接在一次发布中重命名字段、修改类型并删除旧列,会让应用发布和数据库变更无法独立回滚。

配置需要具备:

  • 类型和取值校验
  • 环境差异说明
  • 敏感信息独立管理
  • 修改记录与审核
  • 错误配置的快速回滚

动态配置并不意味着所有参数都应该在线修改。线程池、金额规则和安全策略需要不同的权限与发布要求。

大型系统不适合一次性重写。更可靠的方式是选择边界清晰、价值明确的能力逐步迁移。

一个可执行的顺序是:

  1. 识别高变化或高故障的业务能力。
  2. 在单体内部先整理模块与数据边界。
  3. 建立明确接口和防腐层。
  4. 将新需求优先实现到目标服务。
  5. 同步或迁移存量数据。
  6. 对比新旧链路的业务结果。
  7. 分批切换流量并保留回退路径。
  8. 稳定后删除旧逻辑。

先做模块化再做进程拆分,可以降低分布式改造的不确定性。如果单体内部边界仍然混乱,直接拆成服务只会把方法调用变成网络调用。

架构规则如果只存在于文档中,很快会与代码脱节。应尽可能转化为可以执行或审查的机制:

  • 使用依赖检查阻止跨层或跨域调用
  • 通过 Schema 管理 API 和事件契约
  • 在 CI 中执行测试、静态检查和迁移检查
  • 为核心服务定义 SLO 和告警
  • 在 Code Review 中检查边界与数据所有权
  • 定期回顾服务依赖、故障和容量变化

Code Review 不只检查代码风格,还应关注:

  • 新依赖是否符合服务方向
  • 是否绕过数据所有者直接访问存储
  • 重试是否可能产生重复副作用
  • 异常是否携带足够的业务上下文
  • 新事件是否有版本和幂等设计
  • 发布是否具备兼容与回退路径

服务必须按固定顺序一起发布,任一服务不可用都会导致完整流程失败。

多个服务依赖同一个包含大量业务对象的公共包。一次字段变化会触发全系统升级。

共享库更适合放置日志、追踪和基础协议,不应承载跨领域业务模型。

服务为了获取少量字段不断增加 RPC,最终形成深层调用链。应重新评估数据副本、读模型和职责归属。

系统发送大量消息,却无法查询一个流程当前执行到哪里。消息必须与持久化状态、幂等和补偿机制组合使用。

引入 Kubernetes、Service Mesh 或消息队列不能自动解决边界、数据和一致性问题。基础设施只能执行已经明确的架构决策。

  • 服务是否对应明确的业务能力
  • 核心状态和业务不变量是否内聚
  • 是否存在需要多个服务同时发布的修改
  • 是否有明确的服务所有者
  • 每份核心数据是否有唯一写入方
  • 是否存在跨服务直接写库
  • 强一致与最终一致范围是否明确
  • 消息发布和业务事务之间是否有可靠机制
  • 消费者是否具备业务幂等性
  • 同步调用是否设置超时和资源上限
  • 重试是否有次数、退避和幂等保护
  • 是否能够发现消息积压和流程卡住
  • 核心业务是否有结果指标和告警
  • API、事件和数据库变更是否向前兼容
  • 发布是否支持灰度和快速回滚
  • CI 是否执行核心检查
  • Code Review 是否覆盖架构和数据边界

微服务架构治理的价值不在于把系统拆得更细,而在于让业务变化、数据一致性和运行故障都有明确边界。

服务边界决定变化范围,数据所有权决定自治能力,一致性和幂等决定业务可靠性,可观测性和发布治理决定系统能否长期运行。只有这些机制同时成立,微服务才是降低复杂度的工具,而不是复杂度的来源。