跨行转账如何保证数据一致性?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失败后:

  • 记录失败日志
  • 增加重试计数
  • 定时任务扫描并重试
  • 超过最大重试次数后转为人工处理
相关推荐
青云计划10 小时前
知光项目知文发布模块
java·后端·spring·mybatis
你这个代码我看不懂10 小时前
@RefreshScope刷新Kafka实例
分布式·kafka·linq
Victor35610 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor35610 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
yeyeye11111 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai12 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
+VX:Fegn089512 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟12 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
小小张说故事13 小时前
SQLAlchemy 技术入门指南
后端·python