在分布式系统中,跨服务的事务一致性一直是个难题。本文将结合具体业务场景,详细解析 TCC 分布式事务的实现原理、流程以及优缺点,帮助你彻底搞懂这一常用的分布式事务解决方案。
一、业务场景:为什么需要 TCC?
我们以一个常见的电商订单流程为例,用户支付订单后,系统需要完成以下操作:
- 订单服务:修改订单状态为 “已支付”
- 库存服务:扣减对应商品的库存
- 积分服务:为会员增加相应积分
- 仓储服务:创建销售出库单
编辑
这些操作必须作为一个整体事务存在 ——要么全部成功,要么全部失败。否则就会出现数据不一致的问题,比如:
- 订单状态已改为 “已支付”,但库存服务扣减库存失败,导致库存数量未变
- 积分成功增加,但仓储服务未创建出库单,导致商品无法发货
编辑
这种情况下,就需要 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。当任何一个服务的操作失败时,所有已完成的操作都需要回滚。
编辑
二、TCC 的三个阶段
TCC 是 Try-Confirm-Cancel 的缩写,它将分布式事务的执行过程分为三个阶段,通过这三个阶段的协调来保证事务的一致性。
(一)阶段一:Try(尝试阶段)
在 Try 阶段,各个服务并不会直接完成业务操作,而是进行一些预备操作,主要目的是:
- 检查业务所需的资源是否可用
- 对业务所需的资源进行锁定或预留
以我们的订单流程为例,各个服务在 Try 阶段的操作如下:
订单服务:
不直接将订单状态修改为 “已支付”,而是先修改为 “UPDATING”(中间状态),表示正在处理中。
库存服务:
不直接扣减库存,而是将可销售库存减少,同时在冻结库存字段中记录相应数量。例如,库存 100 件,需要扣减 2 件,则可销售库存变为 98,冻结库存变为 2。
积分服务:
不直接增加会员积分,而是在预增加积分字段中记录相应数值。例如,会员原有积分 1190,需增加 10 分,则保持积分 1190 不变,预增加积分字段设为 10。
仓储服务:
创建销售出库单,但将状态设置为 “UNKNOWN”(未知状态)。
以下是订单服务在 Try 阶段的代码示例:
class OrderService:
def __init__(self, inv_srv, credit_srv, wms_srv):
# 库存服务
self.inv_srv = inv_srv
# 积分服务
self.credit_srv = credit_srv
# 仓储服务
self.wms_srv = wms_srv
def try_pay(self):
# Try阶段:将订单状态修改为中间状态UPDATING
orm.update_status("UPDATING")
# 调用库存服务的Try接口
self.inv_srv.try_reduce_stock()
# 调用积分服务的Try接口
self.credit_srv.try_add_credit()
# 调用仓储服务的Try接口
self.wms_srv.try_sale_delivery()
编辑
(二)阶段二:Confirm(确认阶段)
当 TCC 分布式事务框架感知到所有服务的 Try 操作都成功执行后,就会进入 Confirm 阶段。在这个阶段,各个服务会正式执行业务操作,完成最终的数据修改。
各个服务在 Confirm 阶段的操作如下:
订单服务:
将订单状态从 “UPDATING” 正式修改为 “TRADE_SUCCESS”(已支付)。
库存服务:
将冻结库存字段中的数值清零,完成库存扣减。例如,将冻结的 2 件库存清零,最终可销售库存为 98。
积分服务:
将预增加积分字段中的数值添加到实际积分中,并将预增加积分字段清零。例如,将预增加的 10 分添加到原有 1190 分中,变为 1200 分,预增加积分字段变为 0。
仓储服务:
将销售出库单的状态从 “UNKNOWN” 修改为 “CREATED”(已创建)。
以下是订单服务在 Confirm 阶段的代码示例:
class OrderServiceConfirm:
def confirm_pay(self):
# Confirm阶段:正式将订单状态修改为已支付
orm.update_status("TRADE_SUCCESS")
编辑
(三)阶段三:Cancel(取消阶段)
如果在 Try 阶段,任何一个服务的操作失败,TCC 分布式事务框架就会进入 Cancel 阶段。在这个阶段,各个服务会撤销在 Try 阶段所做的预备操作,恢复到事务执行前的状态。
各个服务在 Cancel 阶段的操作如下:
订单服务:
将订单状态从 “UPDATING” 修改为 “CANCELED”(已取消)。
库存服务:
将冻结库存字段中的数值加回到可销售库存中,并将冻结库存字段清零。例如,将冻结的 2 件库存加回到可销售库存中,可销售库存恢复为 100,冻结库存变为 0。
积分服务:
将预增加积分字段中的数值清零,保持原有积分不变。例如,将预增加的 10 分清零,积分仍为 1190。
仓储服务:
将销售出库单的状态从 “UNKNOWN” 修改为 “CANCELED”(已取消)。
以下是订单服务在 Cancel 阶段的代码示例:
class OrderServiceCancel:
def cancel_pay(self):
# Cancel阶段:将订单状态修改为已取消
orm.update_status("CANCELED")
编辑
三、TCC 分布式事务框架的作用
要实现 TCC 分布式事务,必须依赖专门的 TCC 分布式事务框架,如 Java 的 Seata、ByteTCC、Himly、TCC-transaction,Go 语言的 go-seata 等。这些框架主要起到以下作用:
- 协调事务流程:感知各个服务 Try 阶段的执行结果,决定进入 Confirm 阶段还是 Cancel 阶段。
- 跨服务通信:在不同服务之间传递事务状态信息,调用相应的 Confirm 或 Cancel 接口。
- 记录事务日志:将分布式事务的执行过程和状态记录到日志中(可以是磁盘文件或数据库),用于故障恢复。
- 重试机制:当 Confirm 或 Cancel 操作执行失败时,框架会根据事务日志进行重试,确保最终执行成功。
四、TCC 的优缺点
优点:
- 解决了跨服务的业务操作原子性问题,对于组合支付、订单减库存等场景非常实用。
- TCC 将数据库的二阶段提交上升到微服务层面实现,避免了数据库 2 阶段提交中锁冲突导致的长事务低性能问题。
- TCC 具有异步高性能的特点,它采用 Try 先检查,然后异步实现 Confirm,真正的提交在 Confirm 方法中完成。
缺点:
- 对微服务的侵入性强,每个事务都必须实现 Try、Confirm、Cancel 三个方法,开发成本高,维护和改造的成本也高。
- 为了保证事务一致性,Try、Confirm、Cancel 接口必须实现幂等性操作(通常通过定时器 + 重试机制实现)。
- 事务管理器需要记录事务日志,会损耗一定性能,同时也会使整个 TCC 事务的执行时间拉长(建议采用 redis 记录事务日志)。
- TCC 需要通过锁来确保数据一致性,加锁会导致一定的性能损耗。
五、应对极端情况
- 服务重启后的事务恢复:
TCC 事务框架会记录分布式事务的活动日志(可以是磁盘日志文件或数据库记录),保存事务运行的各个阶段和状态。当服务重启后,框架可以根据这些日志恢复未完成的分布式事务。 - Confirm 或 Cancel 逻辑执行失败的处理:
TCC 事务框架会通过活动日志记录各个服务的状态。如果发现某个服务的 Cancel 或 Confirm 逻辑执行失败,会不断重试调用,直到成功。 - 无法自动解决的问题:
对于一些极小概率的异常情况,如果自动重试无法解决,框架会发送邮件通知工作人员进行人工处理。
六、总结
TCC 分布式事务通过 Try、Confirm、Cancel 三个阶段的协调,实现了跨服务业务操作的原子性。它的核心思想是:
- 先通过 Try 操作检查业务可行性并锁定资源
- 如果所有 Try 操作都成功,通过 Confirm 操作完成最终业务
- 如果任何 Try 操作失败,通过 Cancel 操作回滚所有预备操作
TCC 特别适合那些对数据一致性要求高,且需要跨多个服务协同完成的业务场景。虽然它增加了开发成本和复杂度,但相比其他分布式事务解决方案,它在性能和灵活性方面具有明显优势。
目前,主流的 TCC 框架有 Seata、go-seata 等,在实际项目中可以根据技术栈选择合适的框架来实现 TCC 分布式事务。
如果这篇文章对大家有帮助可以点赞关注,你的支持就是我的动力😊!