跳到主要内容

TCC分布式事务详解

TCC事务模式概述

TCC是Try-Confirm-Cancel的缩写,是一种基于业务层补偿机制的分布式事务解决方案。TCC模式将一个完整的业务操作拆分为三个独立的事务阶段,通过这三个阶段的协同配合来实现分布式环境下的事务一致性。

TCC事务包括以下三个核心步骤:

Try阶段:在Try阶段,各个参与者尝试执行业务操作,并对全局事务预留必要的业务资源。如果Try阶段执行成功,参与者返回成功标识;如果执行失败,则返回失败标识。需要注意的是,Try阶段执行的是完整的业务检查和资源预留,但不是最终提交。

Confirm阶段:如果所有参与者的Try阶段都执行成功,协调者通知所有参与者执行Confirm操作。在Confirm阶段,参与者使用Try阶段预留的资源完成业务操作,并释放全局事务占用的资源。Confirm操作必须保证幂等性,因为可能会被重复调用。

Cancel阶段:如果任何一个参与者在Try阶段执行失败,或者Confirm阶段出现异常,协调者会通知所有参与者执行Cancel操作。Cancel阶段释放Try阶段预留的业务资源,回滚已执行的操作。Cancel操作同样必须保证幂等性。

举个实际的业务场景。假设用户在电商平台购物,需要从钱包账户扣款200元,同时给用户赠送100积分:

Try阶段

  • 钱包服务检查账户余额是否充足,如果充足则冻结200元
  • 积分服务检查积分账户状态正常,预留100积分的配额

Confirm阶段

  • 如果Try阶段都成功,钱包服务执行扣款,将冻结的200元真正扣除
  • 积分服务执行增加积分操作,将100积分加到用户账户

Cancel阶段

  • 如果Try阶段钱包冻结失败,积分服务需要释放预留的积分配额
  • 如果Confirm阶段钱包扣款成功但积分增加失败,需要执行补偿操作,将钱包的200元退回(执行一次反向充值操作)

TCC与2PC的核心区别

虽然TCC和2PC都是两阶段类型的协议,但它们有本质的不同:

事务范围不同

  • 2PC是一个事务:在2PC中,Prepare阶段事务并不提交,只有在第二阶段收到Commit指令后才真正提交。整个过程是一个完整的数据库事务,事务会一直持续到第二阶段结束,事务持有的锁也会一直保持。

  • TCC是三个独立事务:TCC把一个业务操作拆分成Try、Confirm、Cancel三个步骤,每个步骤操作数据库时都是一个独立的事务,各自独立开启、独立提交。Try阶段执行完成后事务就提交了,不会等到Confirm阶段。

一致性保证不同

  • 2PC保证强一致性:因为2PC是一个完整的数据库事务,严格遵循ACID特性,能够保证强一致性。在事务提交之前,其他事务看不到中间状态的数据。

  • TCC保证最终一致性:TCC通过全局锁和重试机制保证最终一致性。在Try和Confirm之间存在时间间隔,数据处于中间状态(如资金已冻结但未扣款),用户可能看到这种中间状态,因此TCC是最终一致性而非强一致性。

性能差异

  • 2PC性能较差:由于2PC事务时间长,从Prepare到Commit期间一直持有锁,阻塞时间长,并发性能差。

  • TCC性能较好:TCC的每个阶段都是独立的短事务,锁持有时间短,可以支持更高的并发。

实现层次不同

  • 2PC在数据库资源层实现:2PC依赖数据库对XA协议的支持,在数据库资源层面实现分布式事务。

  • TCC在应用层实现:TCC在应用层通过业务代码实现,不依赖特定的数据库特性,可以支持非关系型数据库。

下表总结了两者的主要区别:

维度XA 2PCTCC
实现层数据库资源层应用层
事务机制一个事务三个独立事务
性能低(阻塞时间长)高(异步提交)
一致性强一致性最终一致性
隔离性高(数据库隔离级别)弱(需全局锁辅助)
数据库要求需支持XA协议无特殊要求
回滚方式数据库原生回滚业务补偿
适用场景强一致、低并发高并发、最终一致

因此,2PC适用于对事务一致性要求极高的场景,如银行核心转账系统。而TCC适用于对性能要求高但可以接受最终一致性的场景,如电商订单处理、库存扣减等业务。

TCC的优缺点

优点

灵活性高:TCC适用于各种业务场景,如订单处理、库存管理、资金结算等,可以根据业务逻辑实现精细的事务控制。开发者可以在业务层面灵活设计补偿逻辑。

高可用性:TCC使用分布式锁和资源预留机制保证分布式事务一致性,即使某个节点出现故障,也不会影响整个系统的运行。可以通过重试机制保证事务最终完成。

可扩展性好:TCC采用分阶段提交方式,各个阶段相对独立,支持横向扩展,可以适应更多的并发访问和复杂的业务场景。

性能优秀:相比2PC,TCC的每个阶段都是独立的短事务,不会长时间持有数据库锁,具有更好的并发性能,适合高并发场景。

缺点

实现复杂度高:TCC需要为每个业务操作实现Try、Confirm和Cancel三个方法,每个方法都需要实现正确的业务逻辑和补偿机制,代码实现复杂度较高。

业务侵入性强:TCC需要将事务操作拆分为三个步骤,对原有业务代码有较大的侵入性,需要针对不同的业务场景进行专门实现,增加了开发和维护成本。

悬挂事务问题:在调用TCC服务的Try操作时,可能因网络拥堵导致超时,此时事务协调器触发Cancel操作。之后,延迟在网络上的Try请求被服务接收,出现了Cancel请求比Try请求先执行的情况。这会导致Try阶段占用的资源无法释放,形成事务悬挂。

空回滚问题:当Try操作因网络原因未执行成功,但事务协调器认为超时而触发Cancel操作时,对于那些Try未成功的参与者来说,执行Cancel就是一次空回滚。需要在业务中正确识别和处理空回滚,否则可能导致Cancel失败,最终导致整个分布式事务失败。

TCC关键问题及解决方案

空回滚和悬挂问题

空回滚:当Try阶段某些参与者失败了,协调器通知所有参与者执行Cancel。对于那些Try未成功的参与者来说,本次Cancel就是空回滚。如果不正确处理空回滚,可能会出现异常报错,甚至导致Cancel一直失败。

事务悬挂:网络延迟可能导致Cancel请求比Try请求先到达。参与者先执行了空回滚,之后才收到Try请求并执行,这会导致Try占用的资源无法释放,没有后续操作来处理,形成悬挂。

解决方案:引入事务记录表

可以通过引入分布式事务记录表来统一解决这两个问题。每个参与者在本地数据库创建事务记录表,记录每次分布式事务的操作状态。

表结构示例:

CREATE TABLE `distributed_transaction_log` (
`tx_id` varchar(128) NOT NULL COMMENT '全局事务ID',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`tx_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式事务日志表';

事务状态流转:

处理空回滚

public boolean cancel(String txId) {
// 查询事务记录
TransactionLog log = transactionLogMapper.selectByTxId(txId);

if (log == null) {
// Try阶段未执行,这是空回滚
// 记录Cancel状态,防止后续Try执行
TransactionLog cancelLog = new TransactionLog();
cancelLog.setTxId(txId);
cancelLog.setState(TransactionState.CANCEL);
transactionLogMapper.insert(cancelLog);
return true;
}

// 正常Cancel逻辑
if (log.getState() == TransactionState.TRY) {
// 执行业务回滚
doBusinessCancel(txId);
// 更新状态
log.setState(TransactionState.CANCEL);
transactionLogMapper.updateById(log);
return true;
}

// 已经Cancel过了,幂等返回
return log.getState() == TransactionState.CANCEL;
}

防止事务悬挂

public boolean try(String txId, TryRequest request) {
// 查询事务记录
TransactionLog log = transactionLogMapper.selectByTxId(txId);

if (log != null && log.getState() == TransactionState.CANCEL) {
// 已经执行过Cancel(空回滚),拒绝Try
return false; // 或直接返回成功,避免重试
}

if (log != null && log.getState() == TransactionState.TRY) {
// 幂等处理,已经Try过了
return true;
}

// 执行Try业务逻辑
doBusinessTry(request);

// 记录Try状态
TransactionLog tryLog = new TransactionLog();
tryLog.setTxId(txId);
tryLog.setState(TransactionState.TRY);
transactionLogMapper.insert(tryLog);

return true;
}

需要注意的是,以上操作需要做好并发控制,可以通过数据库的唯一索引、乐观锁或分布式锁来保证并发安全。

有了事务记录表,还可以基于它实现幂等控制。每次Try、Confirm、Cancel请求到达时,都先查询事务记录表,根据当前状态决定是执行业务逻辑还是幂等返回。

Confirm或Cancel失败的处理

在TCC模式中,Confirm或Cancel阶段可能因为网络故障、服务异常等原因执行失败,需要有合适的处理策略。

Confirm失败的处理

由于Try阶段已经锁定了资源,Confirm阶段理论上成功概率很高。如果Confirm失败,通常采用以下策略:

  1. 重试机制(最常用):这是最常见的处理方式。由于Try阶段已经预留了资源,Confirm失败大概率是暂时性问题(如网络延迟、服务短暂不可用)。可以设置重试策略,包括重试次数、重试间隔等。通常会设置重试上限,避免无限重试。
public void confirmWithRetry(String txId) {
int maxRetries = 3;
int retryCount = 0;

while (retryCount < maxRetries) {
try {
confirm(txId);
return; // 成功则返回
} catch (Exception e) {
retryCount++;
if (retryCount >= maxRetries) {
// 记录日志并告警
log.error("Confirm failed after {} retries, txId: {}", maxRetries, txId);
alertService.sendAlert("TCC Confirm failed", txId);
throw e;
}
// 等待后重试
Thread.sleep(1000 * retryCount); // 递增延迟
}
}
}
  1. 异步补偿:如果同步重试仍然失败,可以将失败的事务记录下来,通过异步任务或定时任务继续重试,直到成功为止。

  2. 人工干预:对于重要的业务事务,如果自动重试多次仍然失败,可以触发告警,由运维人员或系统管理员人工介入处理。

Cancel失败的处理

Cancel失败的处理策略与Confirm类似:

  1. 自动重试:通过自动重试机制多次尝试执行Cancel操作,直到成功。

  2. 记录日志和告警:将失败信息详细记录到日志中,并发送告警通知相关人员。

  3. 人工干预:如果自动重试多次仍然失败,需要人工介入,手动执行Cancel操作或进行数据修复。

无论是Confirm还是Cancel失败,关键是要保证操作的幂等性,这样即使重复执行也不会产生副作用。同时要有完善的监控和告警机制,及时发现和处理异常情况。

TCC框架选择

目前业界有多个成熟的TCC框架可以选择:

Seata TCC模式:阿里开源的分布式事务解决方案Seata提供了完整的TCC模式支持,集成简单,社区活跃。

Hmily:高性能的TCC分布式事务框架,支持SpringBoot、Dubbo、SpringCloud等主流框架。

ByteTCC:支持XA和TCC两种模式的分布式事务框架,性能优秀。

选择TCC框架时,需要考虑与现有技术栈的兼容性、性能要求、社区支持等因素。对于已经使用Seata的项目,可以直接使用Seata的TCC模式;对于追求极致性能的场景,可以考虑Hmily或ByteTCC。