跨行转账如何保证数据一致性?TCC分布式事务从入门到实战
引言
在当今互联网架构中,分布式系统已成为主流。当一笔转账需要跨多个银行、跨多个账户、甚至跨多个微服务时,如何保证数据的一致性就成了一个极具挑战性的问题。
想象这样一个场景:用户要从A银行转账1000元到B银行。这看似简单的操作,在分布式系统中需要完成以下步骤:
- 从A银行账户扣减1000元
- 通过银行间清算网络进行清算
- 向B银行账户增加1000元
如果在这个过程中任何一步出现故障------网络中断、服务器宕机、数据库崩溃------就会导致数据不一致,这是金融系统绝对不能容忍的。
本文将从ACID基础讲起,逐步深入到分布式事务的解决方案,重点讲解TCC模式的实现原理,并提供一个完整的可运行项目。
一、什么是ACID?
在学习分布式事务之前,我们首先需要理解什么是ACID。ACID是数据库事务的四个基本特性,是保证数据一致性的基石。 
1.1 原子性(Atomicity)
原子性是指事务中的所有操作要么全部成功,要么全部失败回滚。就像转账操作,扣款和入账必须同时成功或同时失败,不能出现扣款成功但入账失败的情况。
1.2 一致性(Consistency)
一致性是指事务执行前后,数据库必须处于一致的状态。对于转账来说,无论发生什么,两账户的总金额必须保持不变。
1.3 隔离性(Isolation)
隔离性是指多个事务并发执行时,每个事务都感觉不到其他事务的存在。隔离性可以防止脏读、不可重复读、幻读等问题。
1.4 持久性(Durability)
持久性是指事务一旦提交,对数据的修改就是永久性的,即使系统崩溃也不会丢失。
二、分布式事务面临的挑战
在单体应用中,我们可以通过数据库的本地事务(如MySQL的InnoDB引擎)轻松实现ACID。但在分布式系统中,情况变得复杂得多。 
2.1 网络不可靠
在分布式系统中,网络是不可靠的。请求可能丢失、延迟、重复,节点之间的通信随时可能失败。
2.2 节点独立性
每个服务有独立的数据库,本地事务只能保证单个服务的数据一致性,无法跨服务保证。
2.3 部分失败
分布式操作中,可能出现部分成功、部分失败的情况。例如:A银行扣款成功,但B银行入账失败。
2.4 数据库隔离
不同服务的数据库之间无法直接建立事务关系,传统的两阶段提交(2PC)在互联网高并发场景下性能较差。
三、TCC分布式事务解决方案
TCC(Try-Confirm-Cancel)是一种应用层的分布式事务解决方案,它将业务逻辑分成三个阶段,通过补偿机制来保证最终一致性。
3.1 TCC的核心思想
TCC模式的核心思想是将每个业务操作拆分成三个独立的步骤:
- Try阶段:资源检查和预留
- Confirm阶段:确认执行业务操作
- Cancel阶段:取消执行,释放资源
3.2 TCC三阶段详解

Try阶段:资源预留
Try阶段是整个TCC事务的第一步,其主要职责是:
- 检查业务规则是否满足(如账户余额是否充足)
- 预留所需资源(冻结转账金额)
- 记录事务状态
Try阶段执行成功后,资源被锁定,但实际业务尚未执行。
示例代码:
java
@Override
public boolean tryExecute(TCCContext context) {
String accountId = context.getParam("accountId");
Long amount = context.getParam("amount");
// 1. 检查账户余额
Account account = accountMapper.selectById(accountId);
if (account.getBalance() < amount + account.getFrozenAmount()) {
return false; // 余额不足
}
// 2. 冻结金额(乐观锁更新)
int rows = accountMapper.freezeAmount(accountId, amount, account.getVersion());
if (rows > 0) {
// 3. 创建冻结记录
FreezeRecord record = new FreezeRecord();
record.setTransactionId(context.getTransactionId());
record.setAccountId(accountId);
record.setAmount(amount);
record.setStatus(0); // 冻结中
freezeRecordMapper.insert(record);
return true;
}
return false;
}
Confirm阶段:确认执行
Confirm阶段在所有分支的Try都成功后执行,主要职责是:
- 执行实际业务操作(扣减余额、增加余额)
- 释放预留资源
- 更新事务状态为CONFIRMED
Confirm阶段必须支持幂等性,因为网络问题可能导致重复调用。
示例代码:
java
@Override
public boolean confirmExecute(TCCContext context) {
String accountId = context.getParam("accountId");
Long amount = context.getParam("amount");
// 1. 幂等性检查
FreezeRecord record = freezeRecordMapper.selectValidRecord(context.getTransactionId(), accountId);
if (record == null) {
// 已确认过,直接返回成功
return true;
}
// 2. 扣减余额和冻结金额
int rows = accountMapper.confirmDeduct(accountId, amount, account.getVersion());
if (rows > 0) {
// 3. 更新冻结记录状态
freezeRecordMapper.updateStatus(context.getTransactionId(), accountId, 1);
return true;
}
return false;
}
Cancel阶段:取消执行
Cancel阶段在任意分支Try失败时执行,主要职责是:
- 释放已预留的资源
- 回滚业务操作
- 更新事务状态为CANCELLED
Cancel阶段同样需要支持幂等性,并支持空回滚(Try未执行时Cancel也能成功)。
示例代码:
java
@Override
public boolean cancelExecute(TCCContext context) {
String accountId = context.getParam("accountId");
// 1. 幂等性检查
FreezeRecord record = freezeRecordMapper.selectValidRecord(context.getTransactionId(), accountId);
if (record == null) {
return true; // 空回滚:没有冻结记录也视为成功
}
// 2. 释放冻结金额
int rows = accountMapper.releaseFreeze(accountId, record.getAmount(), account.getVersion());
if (rows > 0) {
// 3. 更新冻结记录状态
freezeRecordMapper.updateStatus(context.getTransactionId(), accountId, 2);
return true;
}
return false;
}
四、TCC状态机设计
TCC事务有明确的状态转换流程,通过状态机可以清晰地管理事务生命周期。 
4.1 状态定义
| 状态 | 说明 | 是否终态 |
|---|---|---|
| IDLE | 空闲状态,事务初始状态 | 否 |
| TRYING | Try阶段执行中 | 否 |
| CONFIRMING | Confirm阶段执行中 | 否 |
| CANCELLING | Cancel阶段执行中 | 否 |
| CONFIRMED | 事务已确认(成功) | 是 |
| CANCELLED | 事务已取消(回滚) | 是 |
| TIMEOUT | 事务超时 | 是 |
4.2 状态转换规则
objectivec
IDLE → TRYING:开始执行Try阶段
TRYING → CONFIRMING:所有分支Try成功
TRYING → CANCELLING:任一分支Try失败
TRYING → TIMEOUT:Try阶段超时
CONFIRMING → CONFIRMED:Confirm阶段成功
CANCELLING → CANCELLED:Cancel阶段完成
TIMEOUT → CANCELLING:超时后触发回滚
五、系统架构设计
一个完整的TCC分布式事务系统需要合理的架构设计。

5.1 架构分层
- 展示层:提供用户界面和API接口
- 服务层:实现业务逻辑和TCC协调
- 数据层:持久化事务状态和业务数据
- 定时任务层:处理超时事务和重试
5.2 核心组件
TCCTransactionManager(事务管理器)
负责协调整个TCC事务的执行流程:
java
@Slf4j
@Component
public class TCCTransactionManager {
public boolean executeTransaction(TCCContext context, List<TCCTransactionService> participants) {
// 1. Try阶段
if (!tryPhase(context, participants)) {
cancelPhase(context, participants); // Try失败,执行Cancel
return false;
}
// 2. Confirm阶段
return confirmPhase(context, participants);
}
}
AccountTCCService(账户TCC服务)
实现账户相关的TCC操作,包括冻结、扣款、释放等。
TCCTransactionScheduledTask(定时任务)
处理超时事务和重试逻辑:
java
@Scheduled(fixedDelay = 5000)
public void scanTimeoutTransactions() {
// 扫描超时事务并处理
List<TCCTransaction> expired = tccTransactionMapper.selectExpiredTransactions(
System.currentTimeMillis(), 100);
for (TCCTransaction record : expired) {
handleTimeoutTransaction(record);
}
}
六、转账业务完整流程
下面以跨行转账为例,展示完整的TCC执行流程。 
6.1 流程说明
-
发起转账
- 用户发起转账请求
- 生成全局唯一的事务ID
- 创建TCC事务上下文
-
Try阶段
- 转出账户:冻结1000元
- 转入账户:预留1000元额度
- 两个Try都成功后进入Confirm阶段
-
Confirm阶段
- 转出账户:扣减1000元余额,释放冻结
- 转入账户:增加1000元余额
- 更新事务状态为CONFIRMED
-
异常处理
- 如果Try失败,执行Cancel释放资源
- 如果Confirm失败,记录日志并重试
- 如果超时未完成,定时任务介入处理
6.2 事务管理器执行代码
java
public boolean executeTransaction(TCCContext context, List<TCCTransactionService> participants) {
String txId = context.getTransactionId();
log.info("【TCC】开始执行事务: {}, 参与者数量: {}", txId, participants.size());
// 设置超时时间(默认5分钟)
if (context.getExpireTime() == null) {
context.setExpireTime(System.currentTimeMillis() + 5 * 60 * 1000);
}
// 持久化事务记录
saveTransactionRecord(context);
try {
// 1. Try阶段:资源预留
if (!tryPhase(context, participants)) {
log.warn("【TCC】Try阶段失败,开始Cancel: {}", txId);
cancelPhase(context, participants);
return false;
}
// 2. Confirm阶段:确认执行
log.info("【TCC】Try阶段成功,开始Confirm: {}", txId);
return confirmPhase(context, participants);
} catch (Exception e) {
log.error("【TCC】事务执行异常: {}", txId, e);
cancelPhase(context, participants);
throw e;
}
}
七、高性能设计要点
为了保证系统在高并发场景下的性能,我们采用了以下优化策略:
7.1 乐观锁控制并发
使用数据库version字段实现乐观锁,避免长事务阻塞:
sql
UPDATE account SET frozen_amount = frozen_amount + #{amount},
version = version + 1
WHERE account_id = #{accountId} AND version = #{version}
AND balance >= #{amount}
7.2 幂等性设计
所有TCC操作都支持幂等,通过唯一的事务ID和状态判断:
java
// 幂等性检查
FreezeRecord existRecord = freezeRecordMapper.selectValidRecord(txId, accountId);
if (existRecord != null) {
return true; // 已执行过,直接返回成功
}
7.3 本地缓存+数据库持久化
使用ConcurrentHashMap作为L1缓存,减少数据库查询:
java
private final ConcurrentHashMap<String, TCCContext> localCache = new ConcurrentHashMap<>();
public TCCContext getContext(String transactionId) {
// 先查本地缓存
TCCContext context = localCache.get(transactionId);
if (context != null) {
return context;
}
// 查数据库
TCCTransaction record = tccTransactionMapper.selectById(transactionId);
// ...
}
7.4 随机退避避免重试风暴
乐观锁冲突时使用随机退避策略:
java
private void randomBackoff(int retry) {
if (retry > 0) {
long sleepMs = ThreadLocalRandom.current().nextLong(10, 50 * (retry + 1));
Thread.sleep(sleepMs);
}
}
八、项目实战
8.1 数据库设计
账户表(account)
sql
CREATE TABLE account (
account_id VARCHAR(64) PRIMARY KEY,
account_name VARCHAR(128) NOT NULL,
balance BIGINT NOT NULL DEFAULT 0, -- 余额(分)
frozen_amount BIGINT NOT NULL DEFAULT 0, -- 冻结金额(分)
status TINYINT NOT NULL DEFAULT 0, -- 状态
version INT NOT NULL DEFAULT 0, -- 乐观锁版本
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
TCC事务表(tcc_transaction)
sql
CREATE TABLE tcc_transaction (
transaction_id VARCHAR(64) PRIMARY KEY,
business_type VARCHAR(32) NOT NULL,
status TINYINT NOT NULL DEFAULT 0, -- 0-IDLE 1-TRYING 2-CONFIRMING...
expire_time BIGINT NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
max_retry_count INT NOT NULL DEFAULT 5,
business_params TEXT,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_expire (status, expire_time)
);
冻结记录表(freeze_record)
sql
CREATE TABLE freeze_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
transaction_id VARCHAR(64) NOT NULL,
account_id VARCHAR(64) NOT NULL,
amount BIGINT NOT NULL,
operation_type VARCHAR(16) NOT NULL, -- FREEZE或RESERVE
status TINYINT NOT NULL DEFAULT 0, -- 0-冻结中 1-已确认 2-已取消
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tx_account (transaction_id, account_id)
);
8.2 前端页面展示(1)

8.2 前端页面展示(2)

九、常见问题与解决方案
Q1:TCC与2PC有什么区别?
| 特性 | TCC | 2PC |
|---|---|---|
| 锁定时间 | Try阶段短时间锁定 | 直到提交才释放 |
| 性能 | 较高,无长时间阻塞 | 较低,需要长时间锁资源 |
| 一致性 | 最终一致 | 强一致 |
| 实现复杂度 | 高,需要业务支持 | 低,数据库支持 |
Q2:如何处理空回滚?
空回滚是指Try还没执行,Cancel就先到了。解决方案:
- Cancel执行时先检查是否有冻结记录
- 如果没有记录,直接返回成功(幂等)
Q3:如何处理悬挂?
悬挂是指Cancel先于Try执行,导致Try执行后资源无法释放。解决方案:
- Cancel执行时创建一条"已取消"记录
- Try执行时检查是否存在"已取消"记录,如果存在则拒绝执行
Q4:Confirm失败怎么办?
Confirm失败后:
- 记录失败日志
- 增加重试计数
- 定时任务扫描并重试
- 超过最大重试次数后转为人工处理