分布式事务简述
在单体架构时代,我们依靠数据库的 ACID 特性就能高枕无忧。在微服务架构下,由于数据库被拆分(分库分表)或服务化,原本单机的 ACID 保证失效了。一个业务操作经常需要跨多个服务完成,例如一个简单的“下单”操作可能涉及订单、库存、支付等多个独立的服务和数据库。如何保证这些操作“要么都成功,要么都失败”?这就是分布式事务要解决的核心问题。
分布式事务为什么这么难?
在深入方案之前,我们必须掌握两个核心理论。
1. CAP 定理
在一个分布式系统中,以下三个要素最多只能同时实现两点:
- Consistency(一致性): 所有节点在同一时间的数据完全一致。
- Availability(可用性): 服务一直可用,而且响应时间正常。
- Partition Tolerance(分区容错性): 遇到网络分区(节点间通信故障)时,系统仍能继续服务。
分区容错性是什么
分区是常态,而非例外:分布式系统由多台通过网络连接的计算机构成,而网络是不可靠的。硬件故障、带宽拥塞、配置错误等都可能导致网络延迟或中断,使得系统中的部分节点无法正常通信,这就是网络分区。既然无法避免,系统就必须具备容忍分区错误的能力,这就是分区容错性的含义。分区容错性(P)是分布式系统必须保障的属性。你无法通过“不选择P”来换取CA。分布式事务为这么难,本质就是因为网络是不可靠的。
P 是基础,决策在 C 和 A:正因为分区错误必然会发生,设计分布式系统时,分区容错性(P)是必须满足的前提。这引出了CAP定理的核心:当分区错误发生时,你无法同时保证强一致性和高可用性,必须在 C(一致性) 和 A(可用性) 之间做出选择。
分区错误发生时的现实抉择
当网络出现分区错误,系统某些节点间无法通信时,你面临一个两难选择:
选择 CP(一致性优先)
- 做法:如果无法保证数据是最新的,系统将拒绝服务(例如返回一个错误或超时),直到数据同步完成。
- 场景:适用于对数据准确性要求极高的场景。例如,银行转账系统或金融交易系统,宁可暂时停止服务,也绝不能出现数据不一致。
选择 AP(可用性优先)
- 做法:系统始终响应请求,即使返回的数据可能是过时的。系统会接受暂时的数据不一致,并在网络恢复后通过同步机制逐步达到最终一致。
- 场景:适用于对用户体验和系统可用性要求更高的场景。例如,电商网站的商品详情页或社交媒体的动态信息,让用户先看到可能略旧的信息,远比长时间等待或报错要好。
2. BASE 理论
在实际应用中,纯粹的强一致性(CP)(往往会导致性能瓶颈)或高可用性(AP)可能过于绝对。因此,互联网大厂更多采用 BASE 理论作为AP系统的实践指南,它强调基本可用、软状态和最终一致性,在保证高可用的前提下,尽可能地接近数据一致。例如,你在电商平台下单时,库存可能不会立即锁定,而是通过异步方式处理,这背后就是BASE思想的体现。
- Basically Available(基本可用)
- Soft state(软状态): 允许数据存在中间状态。
- Eventually consistent(最终一致性): 不要求实时一致,但经过一段时间后,数据必须达成一致。
分布式事务方案
经典方案:强一致性模型 (2PC/3PC)
如果是传统企业级应用,追求数据的绝对准确(如金融转账),对强一致性有严格要求,且能接受其性能瓶颈的场景,那么会考虑这些方案。
2PC (Two-Phase Commit) 两阶段提交
这是最经典的分布式事务协议,它引入事务协调器(Transaction Coordinator),分准备和提交两个阶段,所有参与者都同意后才提交:
- 准备阶段 (Prepare): 协调者询问所有参与者是否准备好提交。
- 提交阶段 (Commit): 如果大家都 OK,则下令提交;只要有一个不行,则下令回滚。
缺点是同步阻塞(性能差)、单点故障(协调者挂了就瘫痪)、数据不一致(第二阶段如果部分节点没收到 Commit 指令)。
3PC (Three-Phase Commit)
为了解决 2PC 的阻塞问题,引入了 CanCommit、PreCommit、DoCommit 三个阶段,并加入了超时机制。虽然缓解了阻塞,但依然无法完全解决网络分区导致的数据不一致。
现代主流:最终一致性模型 (TCC/Saga/MQ)
在高性能要求的互联网场景中,这些方案才是主角。
1. TCC (Try-Confirm-Cancel)
TCC(Try-Confirm-Cancel)是一种基于业务补偿的分布式事务解决方案,它通过将业务操作拆分为三个核心阶段(尝试、确认、取消)来保证分布式系统下数据的最终一致性,属于应用层面的二阶段提交。
- Try: 检测及预留业务资源(如:冻结余额)。
- Confirm: 确认执行业务操作(不进行任何业务检查,只使用 Try 预留的资源)。
- Cancel: 取消执行,释放预留资源(回滚)。
实现一个健壮的 TCC 事务,必须处理好以下三个核心问题:
- 空回滚问题:当 Cancel 阶段执行时,可能会发现 Try 阶段根本没有成功执行(例如网络异常导致 Try 请求未到达)。此时,Cancel 阶段不应执行任何操作,以避免对业务数据造成错误影响。解决方案通常是在 Cancel 阶段检查 Try 是否成功执行,如果没有成功,则直接返回,不进行任何操作。
- 幂等控制:由于网络问题,Confirm 或 Cancel 阶段可能会被多次调用。为了防止重复执行导致的数据错误,必须确保 Confirm 和 Cancel 操作是幂等的。常见的做法是使用唯一事务 ID 来标识每个事务,并在 Confirm 和 Cancel 阶段检查该 ID 是否已经处理过。
- 悬挂问题:如果在 Try 阶段成功后,Confirm 阶段由于网络问题未能及时执行,可能会导致资源长时间被锁定,影响系统性能。为了解决这个问题,可以设置超时机制,在一定时间内未执行 Confirm,则自动触发 Cancel 操作,释放资源。
2. Saga 模式
Saga(Sequentially Consistent Autonomous Global Transactions)将一个长事务(Long-running-transaction)拆分为多个本地短事务,按顺序执行。每个短事务都有一个对应的补偿事务。
- 流程: T1 → T2 → T3。
- 异常处理: 如果 T2 失败,则执行 C2 → C1 的补偿事务进行回滚。
- 特点: 适用于业务流程长、参与者多的场景,如电商下单(创建订单→扣库存→支付)。
3. 可靠消息最终一致性 (MQ)
可靠消息最终一致性是分布式系统中保证数据最终一致性的重要方案,特别适用于跨服务的异步场景。其核心思想是将本地事务和消息发送绑定在一起,确保两者要么都成功,要么都失败。常见的实现方式有两种:
利用消息中间件(如 RocketMQ)的事务消息功能。
- 发送半消息 (Half Message): 生产者先发一条“暂不可投递”的消息给 MQ。
- MQ 返回确认: MQ 收到并持久化消息,返回 ACK。此时消费者还看不见这条消息。
- 执行本地事务: 生产者在收到确认后,才开始执行本地事务。
- 提交/回滚: 如果本地事务成功,发送
Commit给 MQ,消息对消费者可见。如果本地事务失败,发送Rollback给 MQ,消息被删除。 - 回查机制(Transaction Status Check)(核心): 如果第 4 步的确认信号因为网络原因丢了,MQ 会主动询问生产者:“你那个 ID 为 XXX 的本地事务到底成没成功?”
- 下游消费消息并执行。
如果 MQ 不支持事务消息,通常会采用 本地消息表 (Outbox Pattern)。
- 在一个本地事务里: 既保存订单,又在同一数据库的一张
message_outbox表里插入一条消息记录。 - 原子性保证: 利用关系型数据库的事务特性,保证了“订单”和“待发送消息”要么一起成功,要么一起失败。
- 分布式投递: 启动一个后台线程不断扫描
message_outbox表,把消息发给 MQ,直到发送成功再删除记录。
在分布式事务的 可靠消息最终一致性 方案中,处理下游失败的核心思想是:“只许成功,不许失败;如果失败,重试到底;实在不行,人工接入。”因为在这一步,上游事务已经提交,无法回滚。
MQ 模式之所以是分布式事务,是因为它面临着本地数据库更新与远程消息发送之间的原子性挑战。
仅仅在代码里写‘发消息’是不够的,因为网络或宕机可能导致‘数据库变了但消息没发’。我们必须通过 RocketMQ 的事务消息(半消息+回查) 或者 本地消息表(Outbox Pattern),将这两个跨系统的操作‘绑定’在一起,从而实现上游和下游的最终一致性。
工业级利器:Seata
Seata (Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,支持多种分布式事务模型(如 TCC、AT、Saga、XA)。它通过引入全局事务管理器(TC)和资源管理器(RM),实现了对分布式事务的统一协调和管理。其中的AT 模式最常用,它基于无侵入的自动补偿机制。通过拦截 SQL,自动生成反向 SQL,并记录更新前后的快照(Undo Log),在回滚时自动还原,代码侵入极小。
总结
处理微服务分布式事务的核心思想是从传统的强一致性转向最终一致性,并通过补偿机制来实现回滚。选择时需要在一致性、性能、系统复杂度和开发成本之间做出权衡。