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(高性能、可控)