跨行转账如何保证数据一致性?TCC分布式事务从入门到实战

跨行转账如何保证数据一致性?TCC分布式事务从入门到实战

引言

在当今互联网架构中,分布式系统已成为主流。当一笔转账需要跨多个银行、跨多个账户、甚至跨多个微服务时,如何保证数据的一致性就成了一个极具挑战性的问题。

想象这样一个场景:用户要从A银行转账1000元到B银行。这看似简单的操作,在分布式系统中需要完成以下步骤:

  1. 从A银行账户扣减1000元
  2. 通过银行间清算网络进行清算
  3. 向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模式的核心思想是将每个业务操作拆分成三个独立的步骤:

  1. Try阶段:资源检查和预留
  2. Confirm阶段:确认执行业务操作
  3. 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 架构分层

  1. 展示层:提供用户界面和API接口
  2. 服务层:实现业务逻辑和TCC协调
  3. 数据层:持久化事务状态和业务数据
  4. 定时任务层:处理超时事务和重试

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 流程说明

  1. 发起转账

    • 用户发起转账请求
    • 生成全局唯一的事务ID
    • 创建TCC事务上下文
  2. Try阶段

    • 转出账户:冻结1000元
    • 转入账户:预留1000元额度
    • 两个Try都成功后进入Confirm阶段
  3. Confirm阶段

    • 转出账户:扣减1000元余额,释放冻结
    • 转入账户:增加1000元余额
    • 更新事务状态为CONFIRMED
  4. 异常处理

    • 如果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失败后:

  • 记录失败日志
  • 增加重试计数
  • 定时任务扫描并重试
  • 超过最大重试次数后转为人工处理
相关推荐
用户685453759776920 分钟前
同步成本换并行度:多线程、协程、分片、MapReduce 怎么选才不踩坑
后端
javaTodo28 分钟前
Claude Code 记忆机制详解:从 CLAUDE.md 到 Auto Memory,六层体系全拆解
后端
LSTM971 小时前
使用 C# 和 Spire.PDF 从 HTML 模板生成 PDF 的实用指南
后端
JaguarJack1 小时前
为什么 PHP 闭包要加 static?
后端·php·服务端
BingoGo1 小时前
为什么 PHP 闭包要加 static?
后端
是糖糖啊1 小时前
OpenClaw 从零到一实战指南(飞书接入)
前端·人工智能·后端
百度Geek说2 小时前
基于Spark的配置化离线反作弊系统
后端
Java编程爱好者2 小时前
虚拟线程深度解析:轻量并发编程的未来趋势
后端
苏三说技术2 小时前
Spring AI 和 LangChain4j ,哪个更好?
后端