分布式事务实战:基于本地消息表与Saga模式实现服务协同与最终一致性
最近在开发一个分布式任务调度平台时,遇到了一个典型的多服务协同问题:一个核心任务需要按严格顺序调用A、B、C三个服务,任何一个环节失败都需要整体回滚。最初采用简单的同步链式调用,结果在服务B超时时,整个流程卡死,服务A已执行的操作也无法撤销,造成了数据不一致的“烂摊子”。这种“一次对接有点不熟练”的窘境,在微服务架构下屡见不鲜。
本文将系统性地拆解如何设计一个可靠的服务协同与事务补偿机制,实现类似“空间站对接”的精准、容错的服务调用。我们将从最基础的同步调用改造开始,逐步演进到基于状态机、消息队列的最终一致性方案,并提供可运行的核心代码示例。无论你是正在构建分布式系统的新手,还是希望优化现有服务间通信的开发者,这套从理论到实战的闭环方案都能直接复用。
1. 背景与核心概念:分布式事务的挑战与常见模式
在单体应用时代,我们依靠数据库事务(ACID)来保证数据一致性。但在微服务架构下,数据和服务被拆分到不同的进程中,传统的数据库事务无法跨网络边界生效。这就引出了分布式事务问题。
核心挑战:
- 网络不确定性:服务间调用可能成功、失败或超时,调用方无法准确知道被执行方的真实状态。
- 服务自治性:每个服务拥有独立的数据库,不允许其他服务直接操作其数据。
- 性能与可用性:强一致性方案(如两阶段提交2PC)通常会牺牲可用性和性能,不适合高并发场景。
常见解决方案模式:
- 同步事务(不推荐):如开篇提到的链式调用,耦合严重,可用性差。
- 两阶段提交(2PC):强一致性方案,但存在协调者单点故障、资源锁定时间长的问题,在互联网应用中较少使用。
- 补偿事务(TCC):Try-Confirm-Cancel模式,需要业务实现三个接口,侵入性强,但一致性较好。
- 本地消息表:基于数据库本地事务和消息队列,实现最终一致性,业务侵入性较低。
- Saga模式:将一个分布式事务拆分为一系列本地事务,每个本地事务都有对应的补偿操作,通过协调器或事件驱动来执行或回滚。
本文将重点介绍 Saga模式 和 基于本地消息表的最终一致性 这两种更贴合互联网场景的方案,并给出具体实现。
2. 环境准备与版本说明
为了演示完整流程,我们将构建一个简化的“订单创建”场景,涉及订单服务、库存服务和账户服务。我们将使用Spring Boot框架。
基础环境:
- JDK: 17 或 11
- Maven: 3.6+
- Spring Boot: 2.7.x (本文示例基于2.7.18)
- 数据库: MySQL 5.7+ (用于本地消息表)
- 消息队列: RabbitMQ 3.8+ (或 Kafka,本文以RabbitMQ为例)
- IDE: IntelliJ IDEA 或 Eclipse
项目结构:
每个服务都是独立的Spring Boot应用。
3. 核心原理拆解:Saga与本地消息表
3.1 Saga模式:事件/编排与协调/编排
Saga模式的核心思想是“事后补救”。它有两种主要实现方式:
- 编排(Choreography):每个服务产生并监听事件,自行决定后续操作。服务间通过事件总线(如MQ)通信,没有中心协调者。优点是去中心化、简单;缺点是流程逻辑分散,难以监控和调试。
- 协调(Orchestration):引入一个Saga协调器(一个独立的服务),它负责按顺序调用参与者服务,并在失败时调用补偿操作。优点是流程逻辑集中,易于管理和监控;缺点是引入了单点(协调器需高可用)。
一个典型的Saga协调事务序列:
- 协调器调用 服务A 的
execute。 - 若成功,则调用 服务B 的
execute。 - 若服务B失败,则协调器依次调用 服务B 的
compensate,然后调用 服务A 的compensate。 - 所有补偿完成后,事务回滚。
3.2 本地消息表方案
该方案利用数据库的本地事务原子性,将消息生产和业务操作绑定在同一个事务中。 核心步骤:
- 业务服务在本地事务中,完成业务数据操作,并同时向“本地消息表”插入一条状态为“待发送”的消息。
- 一个独立的“消息发送者”定时轮询本地消息表,抓取“待发送”的消息。
- “消息发送者”将消息投递到消息队列(如RabbitMQ)。如果投递成功,将本地消息状态更新为“已发送”;如果失败,则重试。
- 下游服务消费消息,处理业务。处理成功后,可以发送一条确认消息,或者由上游服务提供状态查询接口供下游回调。
优势:保证了业务操作和消息发出的原子性,只要消息成功发出,最终一定能被消费,从而实现最终一致性。
4. 完整实战案例:基于本地消息表的订单创建
我们以实现“下单扣库存扣余额”为例,采用本地消息表方案。
4.1 数据库与消息表设计
首先,在订单服务的数据库中创建业务表和本地消息表。