Java 微服务架构治理实践
微服务治理不是把单体应用拆成更多进程,也不是引入注册中心、网关和 Kubernetes 后就自然完成。真正需要治理的是系统中的变化、依赖、数据和故障。
如果拆分后一次需求仍然需要修改五个服务,一次故障仍然会拖垮完整调用链,团队仍然通过共享数据库完成协作,那么系统只是从单体变成了分布式单体。
一套有效的微服务架构应该持续改善以下指标:
- 业务变化能否被限制在清晰边界内
- 服务是否拥有独立的数据和发布决策
- 局部故障是否会演变为全链路故障
- 跨服务一致性是否有明确的业务语义
- 线上问题是否能被快速发现、定位和恢复
治理的目标不是追求服务数量,而是控制系统复杂度。
先建立架构基线
Section titled “先建立架构基线”开始拆分或治理前,应先获得当前系统的真实结构,而不是只阅读架构图。
需要收集的基线包括:
| 维度 | 需要确认的事实 |
|---|---|
| 业务 | 核心流程、业务不变量、状态生命周期 |
| 代码 | 模块依赖、公共库、循环调用、修改热点 |
| 数据 | 表归属、跨库查询、共享字段、数据规模 |
| 调用 | 同步链路、消息流向、外部系统依赖 |
| 运行 | 流量、延迟、错误率、资源和容量峰值 |
| 交付 | 构建时间、发布频率、回滚方式、故障记录 |
可以从最近几个月的需求和故障开始调查:
- 哪些模块经常一起修改
- 哪些接口一改就影响多个团队
- 哪些表被不同服务直接读写
- 哪些调用链最容易超时
- 哪些问题只能通过人工查库定位
这些事实比预先设计一套“标准微服务模板”更能说明治理优先级。
服务边界来自业务能力
Section titled “服务边界来自业务能力”服务拆分应围绕业务能力、业务规则和数据生命周期,而不是按 Controller、Service、DAO 技术分层。
判断一个边界是否合理,可以检查四个问题:
- 它是否拥有一组内聚的业务规则?
- 它是否能够独立维护核心状态?
- 它的变化原因是否相对单一?
- 它是否有明确的团队或角色负责?
以通用的交易流程为例:
Order 负责订单生命周期与商品交易状态
Receivable 负责应收建立、调整、核销与余额
Payment 负责支付请求、渠道路由与支付结果
Settlement 负责结算规则、结算单与结算状态这些模块之间存在业务协作,但它们拥有不同的不变量和状态变化原因。将它们强行放进一个“交易服务”,会让服务内部重新形成大型单体;拆得过细,则每个业务动作都需要跨多个网络边界。
避免按数据库表拆服务
Section titled “避免按数据库表拆服务”“一张表一个服务”通常会制造大量贫血服务。表只是当前存储模型,不代表业务能力。
更可靠的顺序是:
业务能力 ↓领域规则与状态 ↓服务边界 ↓数据模型如果顺序反过来,历史数据库结构会永久限制架构演进。
明确数据所有权
Section titled “明确数据所有权”服务自治的基础是数据所有权。每份核心业务数据应该有唯一的写入方,其他服务通过公开契约获取信息。
基本规则包括:
- 一个服务负责核心数据的写入规则
- 其他服务不能绕过接口直接修改其表
- 跨服务查询通过 API、事件副本或专用读模型完成
- 共享字段必须说明来源、更新时间和一致性要求
- 数据修复同样需要经过所有者提供的流程
不要把共享数据库当集成平台
Section titled “不要把共享数据库当集成平台”多个服务共用数据库的短期开发成本较低,但会带来长期耦合:
- 表结构变化无法独立发布
- 业务规则可能在多个服务重复实现
- 无法追踪某个字段由谁修改
- 跨表事务掩盖了真实的服务边界
- 数据库故障会同时影响所有服务
拆分早期可以暂时共享数据库实例,但应明确 Schema、账号和写权限边界,并逐步消除跨服务直接访问。
跨服务列表和报表不应该简单复刻单体中的多表 Join。可以根据时效性选择:
| 场景 | 建议方式 |
|---|---|
| 少量实时详情 | 同步 API 查询 |
| 高频组合查询 | 建立专用读模型 |
| 统计和分析 | 数据仓库或分析平台 |
| 可接受延迟的列表 | 通过事件维护查询副本 |
读模型允许冗余,但必须明确数据来源、更新机制和重建方式。
为同步调用设置边界
Section titled “为同步调用设置边界”同步 HTTP 或 RPC 调用适合需要即时结果的场景,但每增加一层调用,就增加一层延迟和故障概率。
需要治理的不是“能否调用”,而是调用失败时系统如何工作。
每个同步依赖至少应明确:
- 连接和请求超时时间
- 哪些错误允许重试
- 最大重试次数与退避策略
- 熔断和恢复条件
- 并发隔离与资源上限
- 降级结果是否可接受
- 调用方如何记录失败状态
限制调用链长度
Section titled “限制调用链长度”以下链路很难稳定:
Gateway → Order → Customer → Account → Risk → External API任一节点变慢都会占用上游线程和连接。优化方式不一定是把所有调用改成消息,而是重新判断:
- 当前服务是否真的需要这些实时数据
- 数据是否可以在本地维护只读副本
- 是否可以把流程改成可追踪的异步状态机
- 是否存在职责放错位置导致的反向依赖
同步链路应服务于业务语义,而不是成为跨服务取数工具。
选择正确的一致性模型
Section titled “选择正确的一致性模型”分布式系统无法依赖一个覆盖所有服务的本地数据库事务。架构设计必须先确认业务真正需要的一致性等级。
强一致适用范围
Section titled “强一致适用范围”资金扣减、库存锁定、额度占用等操作通常需要在单个服务内部保持强一致。应尽量让关键不变量落在同一个数据所有者和本地事务中。
最终一致适用范围
Section titled “最终一致适用范围”通知、搜索索引、报表、跨域状态同步通常可以接受最终一致。关键是定义:
- 允许多长时间的延迟
- 用户在中间状态看到什么
- 失败后如何重试和补偿
- 如何发现长期未完成的流程
“最终一致”不能等同于“以后应该会一致”。它需要完整的状态和恢复机制。
使用本地事务与事件协作
Section titled “使用本地事务与事件协作”一个常见问题是业务数据提交成功,但消息发送失败,导致下游永远收不到变更。
可以使用 Outbox 模式:
同一本地事务 ├── 更新业务数据 └── 写入 Outbox 事件
异步发布器 └── 将 Outbox 事件发送到消息系统发布成功后更新发送状态;失败则继续重试。这样可以保证业务数据和待发送事件同时提交。
消费端可以使用 Inbox 或业务唯一键记录处理结果:
接收消息 ↓检查 eventId / businessKey ↓执行业务事务 ↓记录消费结果这套机制不能保证消息只投递一次,但可以让业务处理具备幂等性。
幂等是业务设计,不是中间件开关
Section titled “幂等是业务设计,不是中间件开关”消息系统、网关或 SDK 提供的去重能力只能覆盖部分场景。真正的幂等需要业务层定义“重复请求是否代表同一个动作”。
常见幂等键包括:
- 客户端生成的 requestId
- 上游业务单号与操作类型
- 支付渠道流水号
- 消息 eventId
- 聚合根 ID 与目标状态
实现时可以组合使用:
- 数据库唯一约束
- 幂等记录表
- 条件更新
- 状态机校验
- 乐观锁版本号
例如,支付成功消息重复到达时,不应再次增加账户余额。处理逻辑需要确认当前支付状态、流水号和入账结果,而不是只依赖 Redis 中一个可能过期的锁。
区分事件与命令
Section titled “区分事件与命令”消息名称会影响服务之间的耦合方式。
事件描述已经发生的事实:
OrderConfirmedPaymentSucceededReceivableWrittenOff发布方不应该知道有哪些消费者。
命令要求特定接收方执行动作:
CreateReceivableFreezeCreditSendPaymentRequest命令需要明确接收方、执行结果和失败语义。
如果把命令伪装成事件,发布方会暗中依赖消费者行为;如果把所有事实都设计成命令,服务之间会形成中心化流程控制。
事件至少应包含:
eventIdeventTypeeventVersionoccurredAtproducertraceIdbusinessKeypayload字段变更需要兼容策略。优先新增可选字段,避免直接改变已有字段语义;无法兼容时应升级事件版本,并允许消费者逐步迁移。
用状态机管理长流程
Section titled “用状态机管理长流程”跨服务业务流程通常会经历等待、失败、重试和补偿。只通过代码中的 if/else 和消息重试表达流程,很难判断当前业务处于什么状态。
显式状态机应该定义:
- 合法状态
- 允许的状态转换
- 触发转换的命令或事件
- 每次转换的前置条件
- 超时和补偿动作
例如:
PENDING → PROCESSING → SUCCEEDED → FAILED → COMPENSATING → COMPENSATED状态转换必须持久化,并记录业务单号、触发来源、时间和失败原因。这样才能支持查询、重放、补偿和审计。
可观测性必须覆盖业务结果
Section titled “可观测性必须覆盖业务结果”只监控 CPU、内存和 JVM 堆使用量,无法判断系统是否正确完成业务。
建议建立三层指标:
- CPU、内存、磁盘和网络
- Pod 重启、线程池、连接池
- JVM GC、堆和类加载
- 请求量、错误率和延迟分位数
- 超时、熔断、重试和限流次数
- 消息积压、消费延迟和失败次数
- 数据库连接、慢查询和锁等待
- 创建、支付、核销等流程成功率
- 长时间停留在中间状态的单据数
- 金额或数量对账差异
- 补偿任务和人工处理数量
每条日志和消息应携带统一的 traceId、业务单号和关键状态。技术调用链与业务状态关联后,问题定位才不需要依赖人工逐库查询。
发布和配置同样需要治理
Section titled “发布和配置同样需要治理”服务可以独立部署,不代表应该随意发布。稳定的交付流程至少包含:
代码检查 ↓单元与集成测试 ↓制品构建 ↓配置和迁移检查 ↓灰度发布 ↓指标观察 ↓全量或回滚数据库变更应采用向前兼容顺序:
- 先增加新字段或新表。
- 发布同时兼容新旧结构的代码。
- 完成历史数据迁移。
- 切换读取和写入逻辑。
- 确认无旧版本使用后再清理。
直接在一次发布中重命名字段、修改类型并删除旧列,会让应用发布和数据库变更无法独立回滚。
配置需要具备:
- 类型和取值校验
- 环境差异说明
- 敏感信息独立管理
- 修改记录与审核
- 错误配置的快速回滚
动态配置并不意味着所有参数都应该在线修改。线程池、金额规则和安全策略需要不同的权限与发布要求。
渐进式改造单体系统
Section titled “渐进式改造单体系统”大型系统不适合一次性重写。更可靠的方式是选择边界清晰、价值明确的能力逐步迁移。
一个可执行的顺序是:
- 识别高变化或高故障的业务能力。
- 在单体内部先整理模块与数据边界。
- 建立明确接口和防腐层。
- 将新需求优先实现到目标服务。
- 同步或迁移存量数据。
- 对比新旧链路的业务结果。
- 分批切换流量并保留回退路径。
- 稳定后删除旧逻辑。
先做模块化再做进程拆分,可以降低分布式改造的不确定性。如果单体内部边界仍然混乱,直接拆成服务只会把方法调用变成网络调用。
架构治理需要持续执行
Section titled “架构治理需要持续执行”架构规则如果只存在于文档中,很快会与代码脱节。应尽可能转化为可以执行或审查的机制:
- 使用依赖检查阻止跨层或跨域调用
- 通过 Schema 管理 API 和事件契约
- 在 CI 中执行测试、静态检查和迁移检查
- 为核心服务定义 SLO 和告警
- 在 Code Review 中检查边界与数据所有权
- 定期回顾服务依赖、故障和容量变化
Code Review 不只检查代码风格,还应关注:
- 新依赖是否符合服务方向
- 是否绕过数据所有者直接访问存储
- 重试是否可能产生重复副作用
- 异常是否携带足够的业务上下文
- 新事件是否有版本和幂等设计
- 发布是否具备兼容与回退路径
服务必须按固定顺序一起发布,任一服务不可用都会导致完整流程失败。
共享领域模型
Section titled “共享领域模型”多个服务依赖同一个包含大量业务对象的公共包。一次字段变化会触发全系统升级。
共享库更适合放置日志、追踪和基础协议,不应承载跨领域业务模型。
无限制同步调用
Section titled “无限制同步调用”服务为了获取少量字段不断增加 RPC,最终形成深层调用链。应重新评估数据副本、读模型和职责归属。
消息驱动但没有状态
Section titled “消息驱动但没有状态”系统发送大量消息,却无法查询一个流程当前执行到哪里。消息必须与持久化状态、幂等和补偿机制组合使用。
用基础设施替代设计
Section titled “用基础设施替代设计”引入 Kubernetes、Service Mesh 或消息队列不能自动解决边界、数据和一致性问题。基础设施只能执行已经明确的架构决策。
治理检查清单
Section titled “治理检查清单”- 服务是否对应明确的业务能力
- 核心状态和业务不变量是否内聚
- 是否存在需要多个服务同时发布的修改
- 是否有明确的服务所有者
数据与一致性
Section titled “数据与一致性”- 每份核心数据是否有唯一写入方
- 是否存在跨服务直接写库
- 强一致与最终一致范围是否明确
- 消息发布和业务事务之间是否有可靠机制
- 消费者是否具备业务幂等性
- 同步调用是否设置超时和资源上限
- 重试是否有次数、退避和幂等保护
- 是否能够发现消息积压和流程卡住
- 核心业务是否有结果指标和告警
- API、事件和数据库变更是否向前兼容
- 发布是否支持灰度和快速回滚
- CI 是否执行核心检查
- Code Review 是否覆盖架构和数据边界
微服务架构治理的价值不在于把系统拆得更细,而在于让业务变化、数据一致性和运行故障都有明确边界。
服务边界决定变化范围,数据所有权决定自治能力,一致性和幂等决定业务可靠性,可观测性和发布治理决定系统能否长期运行。只有这些机制同时成立,微服务才是降低复杂度的工具,而不是复杂度的来源。