基于 XA 协议的两阶段提交 (2PC)。这是一种分布式事务协议,旨在保证在多个参与者(通常是不同的数据库或资源管理器)共同参与的事务中,所有参与者要么都提交事务,要么都回滚事务,从而维护数据的一致性。
你可以把 2PC 想象成一个需要多个部门共同签字才能生效的合同审批流程。每个部门都有同意或拒绝的权利,最终结果取决于所有部门的意见。
核心思想:协调者与参与者
在 2PC 中,有两个关键的角色:
- 协调者 (Coordinator): 负责整个事务的协调和管理。它向所有参与者发出指令,并收集它们的响应,最终决定事务是提交还是回滚。
- 参与者 (Participants): 参与到分布式事务中的各个资源管理器(例如,数据库)。它们接收协调者的指令,执行本地事务,并告知协调者自己的状态。
两阶段提交的过程
2PC 的过程分为两个关键阶段:
第一阶段(准备阶段 - Prepare Phase)
-
协调者询问 (Prepare Request): 协调者向所有参与者发送一个“准备请求”,询问它们是否准备好提交事务。这个请求包含了要执行的事务操作信息。
-
参与者执行本地事务 (Local Transaction Execution): 收到准备请求后,每个参与者执行本地事务操作,并在本地保存相关的 undo/redo 日志等信息,以便在后续需要回滚或提交时使用。注意:这里参与者只是执行操作,但并不真正提交。
-
参与者响应 (Vote): 每个参与者根据本地事务的执行情况,向协调者发送一个“投票”:
- “同意 (Vote-Commit)”: 表示本地事务执行成功,参与者已经准备好提交。
- “拒绝 (Vote-Abort)”: 表示本地事务执行失败,参与者不能提交,希望回滚。
第二阶段(提交/回滚阶段 - Commit/Rollback Phase)
协调者在收集到所有参与者的投票后,会根据投票结果决定下一步的操作:
情况一:所有参与者都投了“同意”票
-
协调者发送提交请求 (Commit Request): 协调者向所有参与者发送“提交请求”。
-
参与者提交本地事务 (Local Transaction Commit): 收到提交请求后,每个参与者正式提交之前在本地执行的事务。
-
参与者发送确认 (Acknowledgement): 参与者完成提交后,向协调者发送“提交确认”。
-
协调者完成事务 (Transaction Completion): 协调者收到所有参与者的提交确认后,认为整个分布式事务成功完成。
情况二:任何一个或多个参与者投了“拒绝”票,或者协调者在规定时间内没有收到所有参与者的投票
-
协调者发送回滚请求 (Rollback Request): 协调者向所有投了“同意”票的参与者(如果存在)以及投了“拒绝”票的参与者发送“回滚请求”。
-
参与者回滚本地事务 (Local Transaction Rollback): 收到回滚请求后,每个参与者利用之前保存的 undo 日志撤销之前执行的本地事务操作。
-
参与者发送确认 (Acknowledgement): 参与者完成回滚后,向协调者发送“回滚确认”。
-
协调者完成事务 (Transaction Completion): 协调者收到所有参与者的回滚确认后,认为整个分布式事务已回滚。
一个简单的转账例子
假设我们有两个不同的银行数据库参与一个跨行转账事务:
- 参与者 A: 存储用户 A 的账户信息(在 Bank A 的数据库中)。
- 参与者 B: 存储用户 B 的账户信息(在 Bank B 的数据库中)。
- 协调者: 一个专门的事务管理器。
事务: 用户 A 向用户 B 转账 100 元。
第一阶段(准备阶段)
-
协调者发送准备请求: “请参与者 A 准备从用户 A 账户扣除 100 元,参与者 B 准备向用户 B 账户增加 100 元。”
-
参与者 A 执行本地事务: Bank A 的数据库执行了扣除操作,但未提交,记录了 undo 日志(如果需要回滚,就加回 100 元)。参与者 A 投票“同意”。
-
参与者 B 执行本地事务: Bank B 的数据库执行了增加操作,但未提交,记录了 redo 日志(如果需要提交,就确认增加 100 元)。参与者 B 投票“同意”。
-
协调者收到所有“同意”票。
第二阶段(提交阶段)
-
协调者发送提交请求: “请参与者 A 和参与者 B 提交事务。”
-
参与者 A 提交本地事务: Bank A 的数据库正式提交了扣除 100 元的操作。参与者 A 发送“提交确认”。
-
参与者 B 提交本地事务: Bank B 的数据库正式提交了增加 100 元的操作。参与者 B 发送“提交确认”。
-
协调者收到所有“提交确认”,事务成功完成。
如果第一阶段有参与者投了“拒绝”票(例如,用户 A 账户余额不足):
第一阶段(准备阶段)
- 参与者 A 执行本地事务,发现余额不足,投票“拒绝”。
- 参与者 B 可能仍然投票“同意”(因为它不知道 A 的情况)。
- 协调者收到至少一个“拒绝”票。
第二阶段(回滚阶段)
-
协调者发送回滚请求: “请参与者 A 和参与者 B 回滚事务。”
-
参与者 A 回滚本地事务: Bank A 的数据库利用 undo 日志撤销了之前的扣除操作(实际上由于余额不足,可能并没有真正扣除)。参与者 A 发送“回滚确认”。
-
参与者 B 回滚本地事务: Bank B 的数据库利用 undo 日志撤销了之前的增加操作(即使已经执行,也会被撤销)。参与者 B 发送“回滚确认”。
-
协调者收到所有“回滚确认”,事务回滚成功,保证了两个银行账户数据的一致性。
2PC 的缺点
虽然 2PC 能够保证数据的一致性,但也存在一些明显的缺点:
- 同步阻塞 (Blocking): 在准备阶段之后,如果协调者或某个参与者发生故障,其他参与者可能会一直处于阻塞状态,等待协调者的指令或故障参与者的恢复,这会降低系统的可用性。
- 单点故障 (Single Point of Failure): 协调者是整个事务的关键,如果协调者发生故障,整个分布式事务将无法继续进行。
- 数据不一致的风险 (Data Inconsistency Window): 在准备阶段和提交/回滚阶段之间,如果协调者在发送提交/回滚指令前崩溃,可能会导致部分参与者提交了事务,而另一部分参与者没有,从而造成数据不一致。
TCC (Try-Confirm-Cancel) 分布式事务协议,并对比它与两阶段提交 (2PC) 的不同。
TCC:一种柔性事务
TCC 是一种基于补偿机制的分布式事务解决方案。它将一个分布式事务的生命周期划分为三个阶段:
-
Try 阶段(尝试执行):
- 尝试执行业务操作,完成所有业务检查(一致性)。
- 预留必要的业务资源(准隔离性)。
- 这个阶段的目标是尽量为后续的 Confirm 阶段准备好执行条件。
-
Confirm 阶段(确认执行):
- 在 Try 阶段所有参与者都成功的情况下,Confirm 阶段会被执行。
- 真正执行业务操作,不进行任何业务检查。
- Confirm 阶段只需要使用 Try 阶段预留的资源,因此成功率要高。
- Confirm 操作需要保证幂等性,因为可能会重试。
-
Cancel 阶段(取消执行):
- 如果在 Try 阶段有任何参与者失败,或者在后续阶段发生异常,Cancel 阶段会被执行。
- 释放 Try 阶段预留的业务资源,撤销之前 Try 阶段所做的操作,进行补偿。
- Cancel 操作同样需要保证幂等性,因为可能会重试。
TCC 的核心思想是“先尝试,成功则确认,失败则补偿”。 它不依赖于资源管理器(如数据库)的 XA 协议,而是将事务的控制权交还给业务层面,通过业务逻辑来实现事务的提交和回滚。
一个跨银行转账的 TCC 例子
我们继续使用跨银行转账的场景:用户 A 从 Bank A 转账 100 元给用户 B 在 Bank B。
- 参与者 A: Bank A 的转出服务。
- 参与者 B: Bank B 的转入服务。
- 协调者: 仍然需要一个协调者来记录事务状态和驱动流程。
Try 阶段:
- 协调者通知参与者 A (Try): “尝试从用户 A 账户冻结 100 元。”
- 参与者 A 执行 (Try): Bank A 的转出服务检查用户 A 的余额是否足够,如果足够则冻结 100 元(例如,在一个中间状态或单独的冻结金额字段中),并记录冻结日志。Try 阶段成功,返回成功。
- 协调者通知参与者 B (Try): “尝试在用户 B 账户预增加 100 元的额度(但尚未真正增加)。”
- 参与者 B 执行 (Try): Bank B 的转入服务在用户 B 的账户上预留 100 元的额度(例如,在一个中间状态或单独的待入账金额字段中),并记录预留日志。Try 阶段成功,返回成功。
Confirm 阶段(如果 Try 阶段都成功):
- 协调者通知参与者 A (Confirm): “确认转出操作,真正扣减用户 A 账户的 100 元。”
- 参与者 A 执行 (Confirm): Bank A 的转出服务从用户 A 账户的实际余额中扣除 100 元,并清除冻结记录。Confirm 成功,返回成功。
- 协调者通知参与者 B (Confirm): “确认转入操作,真正增加用户 B 账户的 100 元。”
- 参与者 B 执行 (Confirm): Bank B 的转入服务将预留的 100 元额度增加到用户 B 的实际余额中,并清除预留记录。Confirm 成功,返回成功。
Cancel 阶段(如果在 Try 阶段有失败,或后续阶段失败):
假设在 Try 阶段,Bank A 发现用户 A 余额不足,Try 失败。
- 协调者通知参与者 A (Cancel): “取消转出操作,释放之前冻结的金额。”
- 参与者 A 执行 (Cancel): Bank A 的转出服务释放之前冻结的 100 元(如果已经冻结),并清除冻结记录。Cancel 成功,返回成功。
- 协调者通知参与者 B (Cancel): “取消转入操作,撤销之前预增加的额度。”
- 参与者 B 执行 (Cancel): Bank B 的转入服务撤销之前预留的 100 元额度,并清除预留记录。Cancel 成功,返回成功。
TCC 与 2PC 的不同
特性 | 两阶段提交 (2PC) | Try-Confirm-Cancel (TCC) |
---|---|---|
协议层面 | 依赖资源管理器(如数据库)的 XA 协议 | 基于业务逻辑实现 |
资源锁定 | 在准备阶段锁定资源,直到提交或回滚 | Try 阶段预留资源,Confirm/Cancel 阶段释放资源 |
一致性 | 强一致性(在理想情况下) | 最终一致性(通过补偿机制保证) |
性能 | 可能存在长时间的资源锁定,性能相对较低 | 资源锁定时间短,性能通常更高 |
可用性 | 协调者或参与者故障可能导致长时间阻塞 | 业务层面实现补偿,可用性相对较高 |
开发复杂度 | 对业务代码侵入性较小,依赖中间件实现 | 对业务代码侵入性较大,需要实现 Try/Confirm/Cancel 逻辑 |
适用场景 | 资源支持 XA 协议的场景,对强一致性要求高的场景 | 跨数据库、跨服务等复杂场景,对性能和可用性要求高的场景 |
导出到 Google 表格
总结
- 2PC 追求强一致性,但在某些故障情况下可能导致长时间的阻塞,影响系统可用性。它依赖于底层数据库等资源管理器对 XA 协议的支持。
- TCC 追求最终一致性,通过业务逻辑实现补偿,减少了资源锁定的时间,提高了系统的并发能力和可用性。但它对业务代码的侵入性较大,需要开发人员实现 Try、Confirm 和 Cancel 三个操作,并且要考虑幂等性、空回滚、悬挂等问题,开发复杂度较高。
XA 和 TCC 的核心区别
1. 资源层面 vs. 业务层面
-
XA (资源层面): 你可以把它想象成是由数据库(资源管理器,RM)自身提供的“官方”分布式事务解决方案。 它依赖于数据库实现了 XA 协议,这个协议定义了协调者(Transaction Manager,TM)和参与者(RM)之间如何进行两阶段提交的通信。
- 类比: 这就像是不同银行之间有统一的国际清算系统(XA 协议),大家都遵循这个标准流程来处理跨行交易,银行内部的事务机制直接参与到这个流程中。
-
TCC (业务层面): 它不依赖于数据库的 XA 协议。 而是将分布式事务的控制权放在了业务代码层面。你需要自己设计每个参与服务的 Try、Confirm 和 Cancel 操作,通过编写业务逻辑来模拟两阶段提交。
- 类比: 这就像是不同公司之间没有统一的清算系统,它们需要自己协商一套跨公司交易的流程。每个公司都需要定义“预申请”、“确认”和“撤销”这些操作的具体步骤。
2. 强一致性 vs. 最终一致性
-
XA (强一致性): 在理想情况下(没有故障),XA 能够保证严格的原子性。要么所有参与者都提交成功,要么都回滚成功,数据在事务结束后必须保持一致。
- 理解: 就像国际清算系统保证,只要交易开始,最终要么双方账户都更新成功,要么都不更新,中间状态对用户来说是不可见的。
-
TCC (最终一致性): TCC 并不能保证在事务执行过程中所有参与者都处于完全一致的状态。如果在 Confirm 或 Cancel 阶段发生故障,可能需要进行重试或人工干预来保证最终的数据一致性。它的目标是经过一段时间后,数据能够达到一致的状态。
- 理解: 就像公司间的协商交易,如果在“确认”阶段出了问题,可能需要重新发起确认或者执行“撤销”操作,最终目标是让双方的账目对得上,但中间可能会有短暂的不一致。
3. 资源锁的处理
-
XA (长时间持有锁): 这是 XA 的一个关键特点,也是其性能瓶颈所在。在准备阶段 (Prepare),参与者(数据库)通常会锁定相关的资源(例如,数据库行、表等)。这些锁会一直保持到提交或回滚阶段结束才会释放。
- 影响: 长时间的资源锁定会降低数据库的并发处理能力,因为其他事务可能需要等待这些锁被释放才能继续执行。
-
TCC (不长时间持有锁): TCC 的设计目标之一就是尽量缩短资源锁定的时间。
- Try 阶段: Try 阶段通常只是预留资源(例如,冻结金额、记录中间状态),而不是直接锁定数据库的业务资源。
- Confirm/Cancel 阶段: 这两个阶段执行真正的业务操作或补偿操作,通常只需要很短的时间,并且依赖于 Try 阶段预留的信息,不再需要长时间锁定核心业务资源。
- 优势: 通过减少锁的持有时间,TCC 可以提高系统的并发性和吞吐量。
更形象的比喻
想象你和你的朋友合伙买一件东西,需要分别从你们的银行账户扣款:
-
XA 就像银行的联名账户: 银行提供了一个“两阶段扣款”的功能。当发起扣款时,银行会先冻结你们账户上的钱(准备阶段,持有锁),确认双方账户都冻结成功后,才会真正扣款(提交阶段,释放锁)。如果任何一方账户有问题,银行会取消冻结(回滚阶段)。这个过程由银行的系统严格控制,保证要么都扣款成功,要么都不扣款。但如果银行系统繁忙,冻结的时间可能会比较长,影响你们其他交易。
-
TCC 就像你们私下协商:
- Try: 你先告诉银行 A:“我要给朋友转 100 元,请先帮我记录一下,这笔钱暂时不能动。”你的朋友也告诉银行 B:“我可能会收到朋友的 100 元,请先预留一个收款的记录。”(业务层面预留资源,不一定锁定核心账户余额)
- Confirm: 如果你们都通知说“可以了,确认转账!”,银行 A 才真正扣除你的钱,银行 B 才真正增加你朋友的钱。
- Cancel: 如果任何一方出了问题(例如,你的余额不足),你们会通知银行 A 取消之前的记录,通知银行 B 也取消之前的预留。
- 关键: 这个过程是你们自己(业务代码)控制的,银行只是根据你们的指令操作。银行不会一直锁定你的钱,但在“确认”或“取消”失败时,可能需要你们重试或手动协调来保证最终账目正确。
总结理解的关键点
- 谁来控制事务: XA 是资源管理器(数据库)控制,TCC 是业务代码控制。
- 如何保证一致性: XA 依赖数据库的原子性能力,TCC 依赖业务逻辑的补偿机制。
- 锁的持有时间: XA 持有锁的时间较长,TCC 尽量缩短锁的持有时间。
- 适用场景: XA 适用于支持 XA 协议的同构数据源,对强一致性要求高的场景;TCC 适用于跨异构系统、需要更高并发和可用性的场景。
xa 相当于人家给我做了这个功能 而tcc则是自己实现功能?
没错
XA 就像是数据库(或资源管理器)自身提供了“现成的”分布式事务功能。 你只需要按照 XA 协议的规范来使用,协调者会和参与者(数据库)进行通信,由数据库内部的机制来保证事务的原子性和一致性,并负责资源的锁定和释放。你更多的是在配置和使用这个“内置”的功能。
TCC 则完全是“自己动手,丰衣足食”。 你需要基于业务逻辑,自己设计和实现 Try、Confirm 和 Cancel 这三个阶段的操作。你需要考虑如何在 Try 阶段预留资源,如何在 Confirm 阶段真正执行业务操作,以及如何在 Cancel 阶段进行补偿,撤销之前的操作。TCC 将分布式事务的控制权从资源管理器转移到了业务代码层面,你需要自己编写代码来模拟两阶段提交的流程。
更进一步理解:
-
XA: 就像使用支持分布式事务的中间件(例如,某些 JMS 实现、一些分布式数据库)提供的事务管理能力。这些中间件已经帮你实现了两阶段提交的协议细节,你只需要配置和调用相应的 API。
-
TCC: 就像你需要自己编写一个复杂的业务流程,这个流程包含“预处理”、“确认执行”和“撤销处理”三个步骤,并且你需要自己保证这些步骤在分布式环境下的正确执行和数据一致性。
所以,你的总结非常精辟:
- XA: “人家(资源管理器)给我做了这个功能,我直接用。”
- TCC: “人家没做,我自己想办法实现这个功能。”
这种“自己实现”的灵活性使得 TCC 可以应用于 XA 无法很好解决的场景,例如跨异构数据源、不直接支持 XA 协议的服务调用等。但同时,这也带来了更高的开发复杂度和维护成本。