分布式事务
背景
如单体应用中,商品订单和物流订单在一个数据库里,创建一个商品订单和物流订单在一个数据库事务里,数据库自己就能保证原子性一致性。 在分布式服务中,商品服务和物流服务拆分开来,商品服务创建了个订单,需要通知物流服务下物流订单,显然已经不是一个数据库连接的事务。(下文举例也会用这个例子)
在分布式系统中,分布式事务就是解决跨服务、跨数据源数据一致性问题的关键技术。
这里将介绍主流的分布式事务方案,设计思路、实现方式、优缺点。
一、分布式事务定义与CAP理论约束
比较学术的定义: 分布式事务是指事务的参与者、资源服务器、事务协调者位于不同的分布式节点上,需要保证跨节点操作的原子性、一致性、隔离性和持久性(ACID)。
在分布式环境中,由于网络延迟、节点故障等问题,ACID特性难以完全满足,需在CAP理论(一致性、可用性、分区容错性)中寻找平衡——多数分布式系统优先保证分区容错性(P),再根据业务需求在一致性(C)和可用性(A)之间取舍。
二、主流分布式事务方案
(一)2PC(Two-Phase Commit,两阶段提交)
1. 设计思路
2PC是分布式事务的经典方案,思路是将事务分为“准备阶段”和“提交阶段”,通过协调者(Coordinator)统一管理所有参与者(Participant)的事务状态,确保所有节点要么同时提交,要么同时回滚,实现强一致性。
2. 实现概要
-
准备阶段:协调者向所有参与者发送准备请求,参与者执行本地事务(不提交),记录事务日志,若执行成功则返回“就绪”响应,若失败则返回“中止”响应。
-
提交阶段:协调者汇总所有参与者响应,若全部为“就绪”,则向所有参与者发送“提交”指令,参与者执行提交操作并返回确认;若存在“中止”响应,或超时未收到响应,则发送“回滚”指令,参与者执行回滚操作。
实现代码
public void createOrder() {
// 1. 获取MySQL连接
Connection conn = orderDataSource.getConnection();
// 关闭自动提交,开启本地事务 重点
conn.setAutoCommit(false);
// 2. 执行业务SQL:插入订单
conn.prepareStatement("INSERT INTO t_order VALUES (?,?,?)").executeUpdate();
// ====== 重点:耗时大头 ======
// 3. 手动执行【prepareCommit 预提交】
// 2PC 非常重要的一步操作
// 完成了「业务 SQL + 预提交」,此时事务进入Prepared 状态,数据已落地、日志已持久化、锁已持有;
((JDBC4Connection)conn).prepareCommit();
// 4. 【长耗时步骤】:发起RPC网络请求,给协调器发送「我预提交成功了,请指示」,然后【阻塞等待】
// 拿着这个MySQL连接不放、拿着订单的行锁不放,一直等协调器的回复
boolean commitFlag = coordinatorRpcClient.waitCommitOrRollback();
// 5. 收到协调器指令后,执行最终提交/回滚
if(commitFlag) {
conn.commit();
} else {
conn.rollback();
}
// 6. 释放连接
conn.close();
}
注: prepareCommit、commit 这是两个核心API,通常我们写事务时不需要手动写 prepareCommit,因为数据库帮我们自动调用了。 但在分阶段提交时,需要这么一个关键步骤,让数据库占用锁、写日志。
3. 优缺点
优点:实现简单,逻辑清晰,能保证强一致性,适合对数据一致性要求极高的场景。
缺点:
-
阻塞问题:准备阶段后,参与者需等待协调者指令,若协调者故障,参与者会长期阻塞,占用连接资源,发生长事务、锁占用,并发低,且容易死锁。
-
单点故障:协调者是核心瓶颈,若协调者宕机,整个事务无法推进。
-
一致性风险:提交阶段若部分参与者未收到指令,会导致数据不一致。
4. 适用场景
2PC适用于中心化架构、低并发、强一致性需求场景,性能问题较差。 微服务架构通常不用这个。
(二)3PC(Three-Phase Commit,三阶段提交)
1. 设计思路
3PC是对2PC的改进,关键是引入“预提交阶段”,将2PC的准备阶段拆分为“CanCommit”和“PreCommit”,并引入超时机制,解决2PC的阻塞问题和单点故障带来的一致性风险,本质仍是强一致性方案。
2. 实现概要
-
CanCommit阶段:协调者询问参与者是否可执行事务,参与者仅做资源检查,不执行实际事务,返回“同意”或“拒绝”。
-
PreCommit阶段:协调者若收到所有“同意”,则向参与者发送预提交指令,参与者执行本地事务(不提交),返回“预提交成功”;若有“拒绝”,则发送中止指令。
-
DoCommit阶段:协调者汇总预提交结果,若全部成功则发送“提交”指令,参与者提交;若失败或超时,则发送“回滚”指令。参与者超时未收到指令时,默认执行提交(而非阻塞)。
3. 优缺点
优点:解决了2PC的阻塞问题,引入超时机制,降低单点故障对事务的影响,仍能保证强一致性。
缺点:
-
复杂度提升:多增加一个阶段,协议逻辑更复杂,运维成本高。
-
一致性仍有风险:若预提交后协调者宕机,部分参与者超时提交,部分未收到指令提交,可能导致数据不一致。
-
性能开销:仍需多轮网络通信,性能优于2PC但仍较低。
4. 适用场景
3PC因复杂度和一致性残留问题,实际应用较少,目前微服务架构中基本不采用。
(三)TCC(Try-Confirm-Cancel,补偿事务)
1. 设计思路
TCC是基于业务层补偿的分布式事务方案,思路是将分布式事务拆分为三个业务操作:Try(资源检查与预留)、Confirm(确认执行)、Cancel(补偿回滚),通过业务代码手动实现一致性,无需依赖数据库事务,适合微服务架构。
2. 实现概要
-
Try阶段:检查业务资源是否充足,预留资源(如电商下单时锁定库存、物流订单初始锁定),确保后续Confirm/Cancel操作可执行。
-
Confirm阶段:无异常时执行实际业务逻辑,释放预留资源,该阶段必须保证幂等性(避免重复执行)。
-
Cancel阶段:出现异常时执行补偿操作,回滚Try阶段的预留资源(如撤销订单),同样需保证幂等性。
TCC需引入事务协调者,负责协调各服务执行Try、Confirm或Cancel操作,处理超时重试、失败恢复等问题。
3. 优缺点
优点:
-
性能优异:基于业务层操作,无数据库锁阻塞,适合高并发场景。
-
灵活性高:可根据业务需求定制补偿逻辑,支持非数据库资源(如缓存、消息队列)。
-
最终一致性:通过重试机制保证事务最终完成。
缺点:
-
侵入性强:需修改业务代码,实现Try/Confirm/Cancel接口,开发成本高。
-
补偿逻辑复杂:有些业务的Cancel操作难以设计,需考虑各种异常场景。
-
幂等性要求:需手动保证Confirm/Cancel操作的幂等性,避免重复执行导致数据错误。
4. 适用场景
TCC适合高并发、强业务定制化的微服务场景。
TCC 相比3PC 的关键是「业务状态拆分」,而这恰恰是它比 3PC 更适配分布式场景的原因,它不像3PC强依赖本地事务,而是每种业务状态一个事务,直接提交。
(四)Saga模式
1. 设计思路
Saga模式是基于长事务拆分的补偿方案,思路是将分布式事务拆分为多个本地事务(子事务),每个子事务对应一个补偿事务,若某个子事务执行失败,则反向执行之前所有子事务的补偿事务,实现最终一致性。与TCC不同,Saga无需资源预留,适合长事务、业务流程复杂的场景。
2. 实现概要
Saga模式分为两种实现方式:
-
编排式Saga:引入中央协调器,由协调器按顺序调用各子事务服务,若某子事务失败,协调器反向调用补偿服务。例如:订单创建→库存扣减→支付扣减,失败时执行支付补偿→库存补偿→订单取消。
-
协同式Saga:无中央协调器,各服务通过消息通信触发下一个子事务,失败时由当前服务通知上一个服务执行补偿,依赖服务间的耦合通信。
3. 优缺点
优点:
-
低侵入性:无需修改现有本地事务逻辑,仅需实现补偿事务,开发成本低于TCC。
-
适合长事务:支持跨多个服务、多步骤的复杂业务流程(如供应链、物流调度)。
-
最终一致性:通过补偿机制保证事务最终达成一致,性能优于2PC/3PC。
缺点:
-
无资源预留:可能出现中间状态不一致(如订单创建成功但库存扣减失败,需补偿回滚),对业务容错性要求高。
-
补偿顺序复杂:多子事务场景下,补偿顺序需严格反向,协调逻辑易出错。
-
不支持并发:子事务需按顺序执行,并发性能低于TCC。
4. 适用场景
Saga模式适合业务流程长、子事务多的场景,在信贷等领域应用广泛:
- 如用信放款流程(风控→额度中心→客账放款),利用Saga模式拆分长事务,每个步骤对应补偿逻辑,避免因某环节失败导致数据混乱。
(五)本地消息表(最终一致性)
1. 设计思路
基于“消息可靠性投递”和“消息消费幂等性”实现最终一致性,思路是:本地事务与记录重试任务送作为一个原子操作,通过本地消息表记录消息状态,确保消息必被投递;消费者接收消息后执行本地事务,失败则重试,直至成功或者到了最大重试次数。
2. 实现概要
-
生产者服务:执行本地事务,同时向本地消息表插入一条“待发送”状态的消息(本地事务保证原子性)。
-
消息重试线程:轮询本地消息表,将“待发送”消息投递到消息队列(或者RPC调用待重试任务方法),投递成功后更新消息状态为“已发送”。
-
消费者服务:监听消息队列(或者等待被重试调用),接收消息后执行本地事务,执行成功则确认消息消费(更新重试表状态),失败则不确认(记录下一次重试时间)。
-
生产者侧定时检查“待发送”消息,重新投递超时未发送的消息;消费者侧处理重复消息(保证幂等性)。
swan组件
比如美团的Swan组件,最大努力通知型最终一致性,就是类似的思路:
- 每个业务系统自建一张本地重试表(也叫事务日志表),和业务表在同一个数据库实例;
- 业务操作时,同时写「业务主表」+「重试表」,重试表记录「待通知 / 待重试的任务、业务 ID、重试次数、状态、重试周期等」;
- 业务系统起独立的扫表线程(定时任务,比如每 10s 扫一次),扫描重试表里「待重试、未超时、重试次数未达上限」的任务;
- 扫表线程把任务丢给执行线程池,执行线程调用下游接口完成通知 / 回调;
- 执行成功→更新重试表状态为「成功」;执行失败→更新重试次数 + 下次重试时间,等待下一轮扫表;
如何解决多机扫描 并发执行的问题?
如果多台机器同时扫描到一堆待执行任务,然后并发去执行任务。即使消费者有分布式锁、幂等机制,也会导致资源的浪费。
这完全可以利用mysql本身的锁机制,如增加重试任务的状态(执行中)、执行ip。 当更新成功后,再去查出来待执行任务。
- sql1 where status=0 待执行,更新到status=3
-- 1.查询 待重试任务 + 同时更新状态为【执行中(3)】+ 绑定执行机器IP
UPDATE swan_local_retry
SET status = 3, executor_ip = '192.168.1.100', update_time = now()
WHERE status = 0
AND next_retry_time <= NOW()
AND retry_count < max_retry_count
LIMIT 50; -- 每次抢占50条,可配
- sql2 1执行成功后,再查询拿到任务去执行:
SELECT * FROM swan_local_retry WHERE status =3 AND executor_ip = '192.168.1.100';
3. 优缺点
优点:
-
低侵入性:仅需增加本地消息表,对原有业务逻辑改动小,开发成本低。
-
高可用性:基于消息队列异步通信,无阻塞问题,适合高并发场景。
-
最终一致性:通过重试机制和消息表补偿,保证事务最终完成。
缺点:
-
一致性延迟:消息投递和消费存在延迟,中间状态数据不一致,需业务容忍。
-
幂等性与去重:需手动实现消费者幂等性,处理重复消息,增加业务复杂度。
-
消息表冗余:每个服务需维护本地消息表,增加数据库存储和运维成本。
4. 适用场景
该方案适合异步通信、对一致性延迟容忍、高并发的场景,也是分布式事务的主流方案。
三、总结
| 方案 | 一致性级别 | 性能 | 开发成本 | 适用场景 |
|---|---|---|---|---|
| 2PC | 强一致性 | 低 | 低 | 传统分布式数据库、低并发、强一致需求 |
| 3PC | 强一致性 | 中低 | 中 | 传统系统、对阻塞敏感的强一致需求 |
| TCC | 最终一致性 | 高 | 高 | 微服务、高并发、强业务定制化(如支付、订单) |
| Saga | 最终一致性 | 中高 | 中 | 微服务、长事务、复杂业务流程 |
| 本地消息表(+MQ) | 最终一致性 | 高 | 中低 | 微服务、异步通信、高并发 |
-
微服务架构:优先选择最终一致性方案,长事务复杂流程选Saga,异步场景选本地消息表+MQ。
-
若为非核心业务:选择最大努力一次交付,以最低成本满足业务需求。
-
优先复用现有中间件:如已使用RocketMQ/Kafka,可优先考虑本地消息表+MQ方案;如使用Seata框架,可快速落地TCC/Saga。