跳到主内容

从资损率0.1%到0:TCC分布式事务跨服务资金一致性实战 ​

问题背景:跨支付/账户/订单场景的资损痛点 ​

电商下单核心链路中,用户提交订单后需要同步完成三个强关联操作:订单服务生成待支付订单、支付服务预生成冻结流水、账户服务锁定对应金额的用户余额,任意环节出现网络抖动、服务宕机、超时重试,都可能出现部分提交部分失败的状态不一致问题。 传统单体架构下的本地事务完全无法覆盖跨服务场景,我们早期未做特殊处理时线上曾出现0.1%的资损率,也就是每1000笔订单就有1笔出现资金和订单状态不匹配:要么用户余额被扣减但订单未生成,要么订单状态标记为已支付但用户资金未扣,不仅违反支付合规要求,还会引发大量客诉,业务侧对此零容忍。

方案对比:为什么TCC是资金场景最优选型 ​

我们前期先后测试过三种主流分布式事务方案,最终选定TCC模式作为资金场景的核心一致性保障方案:

  1. 可靠消息最终一致性方案:异步补偿延迟最高达15s,资金场景下用户等待支付结果的体验极差,极端场景下还可能出现重复扣减风险,无法满足实时性要求;
  2. XA两阶段提交方案:资源锁持有时间长达数秒,高并发下性能瓶颈明显,单集群QPS超过200就会触发大量数据库锁超时,完全无法支撑大促场景的流量峰值;
  3. TCC模式:分为Try(资源预留)、Confirm(确认提交)、Cancel(回滚释放)三个阶段,全程数据库行锁持有时间不超过200ms,性能损耗仅在5%以内,还可以自定义异常兜底逻辑,完全适配资金场景的强一致性要求,是金融级资金链路的最优选型。

实操步骤与核心Java代码实现 ​

我们基于Spring Cloud + Seata TCC生态实现跨三个服务的分布式事务,核心流程分为三层:第一是Try阶段三个服务分别完成资源预留,第二是全局事务管理器校验所有Try都成功后触发Confirm,任意一个Try失败就立刻触发Cancel,第三是异步兜底任务补偿长时间处于中间态的异常事务,避免服务宕机导致的状态卡住问题。 核心可运行代码示例如下,首先定义订单服务的TCC接口:

java
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;

@LocalTCC
public interface OrderTccService {
    // Try阶段:生成待支付订单,状态标记为INIT,预留订单资源
    @TwoPhaseBusinessAction(name = "orderTccTx", commitMethod = "orderConfirm", rollbackMethod = "orderCancel")
    boolean tryCreateOrder(@BusinessActionContextParameter(paramName = "orderId") String orderId,
                           @BusinessActionContextParameter(paramName = "userId") String userId,
                           @BusinessActionContextParameter(paramName = "amount") Long amount);
    // Confirm阶段:修改订单状态为已支付,确认资源提交
    boolean orderConfirm(BusinessActionContext context);
    // Cancel阶段:标记订单为已取消,释放预留资源
    boolean orderCancel(BusinessActionContext context);
}

支付服务、账户服务的TCC接口逻辑和订单服务对齐:Try阶段分别生成冻结支付流水、锁定用户账户余额,Confirm阶段将冻结状态的资金正式扣减,Cancel阶段直接解冻冻结的资金。 全局事务发起入口代码如下,通过Seata的@GlobalTransactional注解自动开启分布式事务管理:

java
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;

@Service
public class OrderPayServiceImpl implements OrderPayService {
    @GlobalTransactional(name = "pay-order-global-tx", rollbackFor = Exception.class)
    public void createOrderAndDeduct(String orderId, String userId, Long amount) {
        // 调用订单服务Try阶段接口
        orderTccService.tryCreateOrder(orderId, userId, amount);
        // 调用支付服务Try阶段接口
        payTccService.tryFreezePay(orderId, userId, amount);
        // 调用账户服务Try阶段接口
        accountTccService.tryLockBalance(orderId, userId, amount);
    }
}

最后新增异步兜底定时任务,每30s扫描一次事务控制表中超过5分钟还处于中间态的TCC事务,主动触发重试Confirm/Cancel,这里引入分布式锁避免多服务实例并发重复执行兜底逻辑:

java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class TccCompensateTask {
    @Resource
    private StringRedisTemplate redisTemplate;
    @Resource
    private TxLogMapper txMapper;
    @Resource
    private TccTxHandler tccHandler;

    // 异常兜底定时任务,每30s执行一次
    @Scheduled(cron = "0/30 * * * * ?")
    public void tccExceptionCompensate() {
        // 抢占分布式锁,避免多实例并发执行兜底导致重复处理
        String lockKey = "tcc:compensate:global:lock";
        Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.MINUTES);
        if (!Boolean.TRUE.equals(lockResult)) {
            return;
        }
        try {
            // 查询超过5分钟未处理的中间态事务记录
            List<TxLogDO> unProcessedTxList = txMapper.selectUnprocessedTx(LocalDateTime.now().minusMinutes(5));
            for (TxLogDO tx : unProcessedTxList) {
                // 根据事务状态触发对应Confirm或Cancel补偿逻辑
                tccHandler.processCompensateTx(tx);
                // 标记事务为已补偿,避免重复扫描
                txMapper.updateTxProcessed(tx.getId());
            }
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
}

这套方案上线后我们的跨服务事务成功率从99.2%提升到99.999%,资损率直接降到0,大促场景下峰值1万QPS也没有出现性能瓶颈。

避坑总结:3个高频异常场景的修正方案 ​

我们在落地过程中踩过多个线上故障,总结出3个必须严格遵守的实践规则:

  1. 空回滚问题修正:常见错误现象是当账户服务的Try请求还没到达,全局事务管理器已经触发Cancel,此时直接执行Cancel逻辑会尝试删除不存在的冻结记录,引发数据异常。正确处理方式是在Cancel阶段先查询本地事务控制表的Try执行记录是否存在,如果不存在直接返回成功,不执行任何回滚逻辑。
  2. 幂等性问题修正:常见错误现象是网络重试导致Confirm/Cancel方法被多次调用,出现重复扣减/解冻资金的问题。我们需要为每个TCC事务生成唯一的XID,在Confirm/Cancel执行前先查询本地事务执行记录表,如果该XID已经处理过直接返回成功,完全避免重复执行。
  3. 悬挂问题修正:常见错误现象是因为网络抖动,Cancel请求比Try请求先到达服务,Cancel执行完所有解冻逻辑后,后面Try请求才到达,会预留出本来不该存在的资源,导致用户资金被错误锁定。正确处理方式是在Try阶段先查询事务控制表是否已经存在对应XID的Cancel记录,如果存在直接拒绝执行Try逻辑,返回失败即可。 这套TCC异常兜底方案已经在我们的电商支付场景稳定运行18个月,累计处理超过2000万笔交易,没有出现过一笔资金不一致的问题,完全满足金融级的资金一致性要求。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。