Seata TCC 生产级(空回滚+悬挂+幂等)+ AT/TCC 混合使用

AT 模式基于 undo_log 实现自动回滚,无代码侵入,适合常规业务;

TCC 模式通过业务层 Try-Confirm-Cancel 手动实现分布式事务,性能更高、粒度更细,适合库存、秒杀等核心交易场景。

TCC三大问题

空回滚:Try 没执行,Cancel 先执行

悬挂:Cancel 执行完,Try 才来

幂等:Confirm/Cancel 重复调用

一、项目标准结构(清晰可维护)

复制代码
your-project/
├── src/main/java/com/example/
│   ├── config/             # Seata 配置
│   ├── entity/             # 实体类
│   ├── mapper/             # DAO 层
│   ├── service/            # 业务层
│   │   ├── tcc/                # TCC 专用包
│   │   │   ├── StockTccService.java      # TCC 接口
│   │   │   └── StockTccServiceImpl.java  # TCC 实现(含三大问题解决)
│   │   ├── OrderService.java       # 分布式事务发起方(AT+TCC混合)
│   │   └── AccountService.java     # AT 模式服务
│   └── controller/         # 接口入口
└── resources/
    └── mapper/             # MyBatis XML

二、必须执行的 SQL(2张表)

1. TCC 事务状态表(解决:空回滚、悬挂、幂等

sql 复制代码
CREATE TABLE `tcc_transaction` (
  `xid` varchar(100) NOT NULL,
  `branch_id` bigint NOT NULL,
  `status` tinyint NOT NULL COMMENT '0=Try中 1=Confirm完成 2=Cancel已回滚',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 库存表(冻结字段实现 TCC 资源预留

sql 复制代码
CREATE TABLE `stock` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `goods_id` bigint NOT NULL,
  `stock` int NOT NULL DEFAULT 0,
  `frozen` int NOT NULL DEFAULT 0 COMMENT '冻结库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三、POM 依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

四、核心 TCC 代码

4.1 TCC 接口(固定写法)

java 复制代码
package com.example.service.tcc;

import io.seata.rm.tcc.api.*;

@LocalTCC
public interface StockTccService {

    @TwoPhaseBusinessAction(
            name = "deductStockTcc",
            commitMethod = "confirm",
            rollbackMethod = "cancel"
    )
    boolean tryDeductStock(
            BusinessActionContext context,
            @BusinessActionContextParameter("goodsId") Long goodsId,
            @BusinessActionContextParameter("num") Integer num
    );

    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

4.2 TCC 实现类(空回滚 + 悬挂 + 幂等

java 复制代码
package com.example.service.tcc;

import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.example.mapper.StockMapper;
import com.example.mapper.TccTransactionMapper;
import com.example.entity.TccTransaction;

import javax.annotation.Resource;

@Slf4j
@Service
public class StockTccServiceImpl implements StockTccService {

    @Resource
    private StockMapper stockMapper;

    @Resource
    private TccTransactionMapper tccMapper;

    // ==================== Try:检查 + 冻结 ====================
    @Override
    public boolean tryDeductStock(BusinessActionContext context, Long goodsId, Integer num) {
        String xid = context.getXid();
        long branchId = context.getBranchId();

        // 1. 幂等校验:已执行则直接返回
        TccTransaction t = tccMapper.get(xid, branchId);
        if (t != null) {
            return true;
        }

        // 2. 防悬挂:Cancel 已执行,拒绝 Try
        if (t != null && t.getStatus() == 2) {
            throw new RuntimeException("交易已回滚,禁止操作");
        }

        // 3. 记录事务状态
        tccMapper.add(xid, branchId, 0);

        // 4. 业务校验 + 冻结库存
        Integer available = stockMapper.getAvailable(goodsId);
        if (available == null || available < num) {
            throw new RuntimeException("库存不足");
        }
        stockMapper.freeze(goodsId, num);
        log.info("✅ TCC Try 完成:goodsId={},冻结={}", goodsId, num);
        return true;
    }

    // ==================== Confirm:确认扣减 ====================
    @Override
    public boolean confirm(BusinessActionContext context) {
        String xid = context.getXid();
        long branchId = context.getBranchId();
        Long goodsId = (Long) context.getActionContext("goodsId");
        Integer num = (Integer) context.getActionContext("num");

        // 幂等
        TccTransaction t = tccMapper.get(xid, branchId);
        if (t == null || t.getStatus() == 1) return true;

        // 扣减
        stockMapper.deduct(goodsId, num);
        tccMapper.update(xid, branchId, 1);
        log.info("✅ TCC Confirm 完成:最终扣减");
        return true;
    }

    // ==================== Cancel:回滚解冻 ====================
    @Override
    public boolean cancel(BusinessActionContext context) {
        String xid = context.getXid();
        long branchId = context.getBranchId();
        Long goodsId = (Long) context.getActionContext("goodsId");
        Integer num = (Integer) context.getActionContext("num");

        // 1. 幂等
        TccTransaction t = tccMapper.get(xid, branchId);
        if (t != null && t.getStatus() == 2) return true;

        // 2. 空回滚:Try 未执行,直接标记为已回滚
        if (t == null) {
            tccMapper.add(xid, branchId, 2);
            return true;
        }

        // 3. 解冻
        if (t.getStatus() == 0) {
            stockMapper.unfreeze(goodsId, num);
        }

        tccMapper.update(xid, branchId, 2);
        log.info("✅ TCC Cancel 完成:解冻回滚");
        return true;
    }
}

五、Mapper 接口(完整)

5.1 TccTransactionMapper

java 复制代码
package com.example.mapper;

import com.example.entity.TccTransaction;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface TccTransactionMapper {
    TccTransaction get(@Param("xid") String xid, @Param("branchId") Long branchId);
    int add(@Param("xid") String xid, @Param("branchId") Long branchId, @Param("status") Integer status);
    int update(@Param("xid") String xid, @Param("branchId") Long branchId, @Param("status") Integer status);
}

5.2 StockMapper

java 复制代码
package com.example.mapper;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface StockMapper {
    Integer getAvailable(@Param("goodsId") Long goodsId);
    int freeze(@Param("goodsId") Long goodsId, @Param("num") Integer num);
    int deduct(@Param("goodsId") Long goodsId, @Param("num") Integer num);
    int unfreeze(@Param("goodsId") Long goodsId, @Param("num") Integer num);
}

六、XML SQL

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper">

    <!-- TCC 事务 -->
    <select id="get" resultType="com.example.entity.TccTransaction">
        select * from tcc_transaction where xid=#{xid} and branch_id=#{branchId}
    </select>
    <insert id="add">
        insert into tcc_transaction(xid,branch_id,status) values(#{xid},#{branchId},#{status})
    </insert>
    <update id="update">
        update tcc_transaction set status=#{status} where xid=#{xid} and branch_id=#{branchId}
    </update>

    <!-- 库存 -->
    <select id="getAvailable" resultType="integer">
        select stock - frozen from stock where goods_id=#{goodsId}
    </select>
    <update id="freeze">
        update stock set frozen = frozen + #{num} where goods_id=#{goodsId}
    </update>
    <update id="deduct">
        update stock set stock=stock-#{num}, frozen=frozen-#{num} where goods_id=#{goodsId}
    </update>
    <update id="unfreeze">
        update stock set frozen = frozen - #{num} where goods_id=#{goodsId}
    </update>

</mapper>

七、分布式事务发起方(AT + TCC 混合使用

java 复制代码
package com.example.service;

import com.example.service.tcc.StockTccService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import com.example.mapper.OrderMapper;

import javax.annotation.Resource;

@Service
public class OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private AccountService accountService;

    @Resource
    private StockTccService stockTccService;

    /**
     * 全流程分布式事务:
     * 订单(AT) + 账户(AT) + 库存(TCC)
     */
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 1. 创建订单(AT)
        orderMapper.insert(order);

        // 2. 扣减余额(AT)
        accountService.deduct(order.getUserId(), order.getPrice());

        // 3. 扣减库存(TCC)
        stockTccService.tryDeductStock(null, order.getGoodsId(), order.getNum());

        // 模拟异常 → 全部回滚
        // int i = 1 / 0;
    }
}

八、Seata 配置(application.yml)

yaml 复制代码
seata:
  enabled: true
  tx-service-group: my_group
  service:
    vgroup-mapping:
      my_group: default
    grouplist:
      default: 127.0.0.1:8091

九、这套代码 解决了哪些生产问题?

1. 空回滚问题

  • 现象:Try 没执行,Cancel 先执行
  • 解决:Cancel 中判断无事务记录 → 直接标记回滚,不做业务操作

2. 悬挂问题

  • 现象:Cancel 执行完,Try 才执行 → 资源永久锁住
  • 解决:Try 执行前判断事务状态 → 已回滚则直接拒绝

3. 幂等问题

  • 现象:Confirm/Cancel 被重复调用 → 重复扣减/重复解冻
  • 解决:执行前判断状态 → 已执行则直接返回

4. 超卖问题

  • 解决:Try 冻结 + Confirm 最终扣减

5. 分布式事务一致性

  • 订单、账户、库存任意失败 → 全部自动回滚

6. AT + TCC 混合使用

  • 简单业务用 AT(零编码)
  • 核心业务用 TCC(高性能、可控)
相关推荐
小旭95272 小时前
SpringBoot 整合 MyBatis 与自动配置原理详解
java·spring boot·后端·spring·intellij-idea·mybatis
超级无敌大好人2 小时前
程序运行卡住排查
java·spring ai·qdrant
NGC_66112 小时前
深入解析 ConcurrentHashMap 设计思想:高并发下的线程安全哈希表
java·开发语言
无极低码2 小时前
纯Java、无任何第三方依赖、直接可用的 SQLite 工具类
java·jvm·sqlite
weixin_425023002 小时前
Spring Boot 2.7 + JDK 8 实现 WebSocket 集群分布式部署(基于 Redis Pub/Sub 方案)
java·spring boot·websocket
高级盘丝洞2 小时前
Spring Boot 使用 WebServiceTemplate 调用 WebService 完整教程
java·spring boot·后端
人道领域4 小时前
Day | 11 【苍穹外卖统计业务的实现:含详细思路分析】
java·数据库·后端·苍穹外卖
xiaoye37089 小时前
Java 自动装箱 / 拆箱 原理详解
java·开发语言
YDS82910 小时前
黑马点评 —— 分布式锁详解加源码剖析
java·spring boot·redis·分布式