SpringCloudAlibaba Seata分布式事务 TCC模式

TCC事务介绍

在大厂中常用,因为性能最高,但代码复杂度高

简单来说,Seata TCC 模式是一个业务侵入性强,但性能高、控制细粒度的分布式事务解决方案。它不依赖底层数据库,而是通过你业务代码中实现的 Try-Confirm-Cancel 三个方法来管理事务。

核心原理:一个"手动挡"的二阶段提交

TCC 模式本质是两阶段提交(2PC)的一种业务层实现。它把事务拆分为三个阶段,全部由你的代码控制:

  1. Try 阶段(一阶段:资源预留) :执行业务检查,并预留 需要使用的资源。比如,在"下单扣库存"场景中,不是直接扣减库存,而是先将库存冻结起来,进入一个中间状态。

  2. Confirm 阶段(二阶段:确认提交):如果所有参与者的 Try 阶段都成功了,TC(事务协调器)就会调用每个参与者的 Confirm 方法,将 Try 阶段预留的资源真正提交,完成业务操作(比如将冻结的库存真正扣减掉)。

  3. Cancel 阶段(二阶段:回滚):如果任何一个参与者的 Try 阶段失败了,TC 就会调用所有参与者的 Cancel 方法,释放 Try 阶段预留的资源,让数据回到初始状态(比如解冻之前冻结的库存)。

TCC事务模式执行机制

注意:这里实际上是多了一个冻结字段,或者说多了一个冻结的概念

流程分析

阶段一Try经历后,阶段二只会经过一个Confirm 或者 Canncel

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

  • 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30

初始余额:

余额充足,可以冻结:

此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。

  • 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30

确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元

  • 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

需要回滚,那么就要释放冻结金额,恢复可用金额:

Seata的TCC模型

Seata中的TCC模型依然延续之前的事务架构,如图:

优缺点

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

事务悬挂和空回滚

1)空回滚

当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不应该做回滚。

Try 执行缓慢或被延迟,导致全局事务超时,TC 下发 Cancel。

如图:

执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

业务悬挂

对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。

执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

实现TCC模式

解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel?

解决方案

解决这个问题的方案是在每个微服务创建一张表来判断每个微服务的状态,来解决幂等、事务悬挂和空回滚的问题。

Seata 1.5.0以前这张表是需要我们来创建并在业务逻辑中维护的,之后的版本我们只需要创建这张表,seata会帮我们维护。

sql 复制代码
CREATE TABLE IF NOT EXISTS `tcc_fence_log` (
  `xid` VARCHAR(128) NOT NULL COMMENT '全局事务id',
  `branch_id` BIGINT NOT NULL COMMENT '分支事务id',
  `action_name` VARCHAR(64) NOT NULL COMMENT 'TCC 动作名称',
  `status` TINYINT NOT NULL COMMENT '事务状态: 1-tried, 2-committed, 3-rollbacked, 4-suspended(悬挂)',
  `gmt_create` DATETIME(3) NOT NULL COMMENT '创建时间',
  `gmt_modified` DATETIME(3) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`xid`, `branch_id`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
  • 联合主键 (xid + branch_id) :用来确保同一个全局事务的分支不会被重复执行,这是实现 Try 阶段幂等 的基础。
  • status 状态字段:记录分支事务当前所处的阶段,分别对应:1=已尝试(Tried)、2=已提交(Committed)、3=已回滚(Rollbacked)、4=已挂起(Suspended)。
  • action_name 字段:记录 TCC 动作名称,用于框架识别和回调对应的 Confirm/Cancel 方法。

建表后,在 @TwoPhaseBusinessAction 注解中开启防护功能:useTCCFence = true。开启后,框架会自动利用这张表来处理幂等、空回滚和防悬挂问题,你就不需要再手动编写这部分控制代码了

代码实现

阶段一:Try(资源预留)

java 复制代码
@TwoPhaseBusinessAction(
    name = "reduceBalance", 
    commitMethod = "confirm", 
    rollbackMethod = "cancel",
    useTCCFence = true  // 开启防护机制
)
@Transactional
public boolean try(BusinessActionContext context, String userId, int money) {
    // 1. 扣减可用余额
    accountMapper.deductAvailable(userId, money);
    
    // 2. 插入冻结记录(或直接在可用余额表中增加冻结字段)
    // 这里注意:useTCCFence 会自动记录状态,你不需要自己维护冻结表
    
    return true;
}

思路分析

sql 复制代码
CREATE TABLE `account_freeze_tbl` (
  `xid` varchar(128) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

阶段二:Confirm(确认提交)

java 复制代码
@Transactional
public boolean confirm(BusinessActionContext context) {
    // 不需要再操作余额(Try 阶段已经扣了)
    // 只需要清理冻结记录(如果有的话)
    // 或者什么都不做(空确认)
    log.info("Confirm 成功,xid={}", context.getXid());
    return true;
}

阶段三:Cancel(回滚)

java 复制代码
@Transactional
public boolean cancel(BusinessActionContext context) {
    // 从上下文获取 Try 阶段的参数
    String userId = (String) context.getActionContext("userId");
    Integer money = (Integer) context.getActionContext("money");
    
    // 恢复可用余额
    accountMapper.increaseAvailable(userId, money);
    
    // 清理冻结记录(如果有)
    return true;
}

所以,在你 1.5.2 版本中,只要开启了 useTCCFence = true你不需要自己维护 account_freeze_tbl也不需要手动写任何状态判断逻辑,框架全帮你做了。

业务代码 账户表加冻结字段(推荐)

java 复制代码
CREATE TABLE `account` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(64) NOT NULL COMMENT '用户ID',
  `available_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '可用余额',
  `frozen_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '冻结金额',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
java 复制代码
// try阶段
@Transactional
public boolean try(String userId, int money) {
    // 1. 先查当前版本号
    Account account = accountMapper.selectByUserId(userId);
    if (account.getAvailableAmount() < money) {
        throw new RuntimeException("余额不足");
    }
    
    // 2. 乐观锁更新
    int affected = accountMapper.update(
        "UPDATE account SET " +
        "  available_amount = available_amount - #{money}, " +
        "  frozen_amount = frozen_amount + #{money}, " +
        "  version = version + 1 " +
        "WHERE user_id = #{userId} " +
        "  AND available_amount >= #{money} " +
        "  AND version = #{version}"
    );
    
    // 3. 冲突处理
    if (affected == 0) {
        throw new RuntimeException("并发冲突,请重试");
    }
    
    // tcc_fence_log 插入由 Seata 框架自动完成(useTCCFence=true)
    return true;
}



// Confirm 阶段
@Transactional
public boolean confirm(BusinessActionContext ctx) {
    String xid = ctx.getXid();
    String userId = (String) ctx.getActionContext("userId");
    Integer money = (Integer) ctx.getActionContext("money");
    
    // 1. 先查当前版本号和冻结金额
    Account account = accountMapper.selectByUserId(userId);
    
    // 2. 幂等:如果冻结金额已经是 0,说明 Confirm 已完成
    if (account.getFrozenAmount() == 0) {
        log.info("Confirm 幂等:冻结金额已为0,xid={}", xid);
        return true;
    }
    
    // 3. 乐观锁更新:只清冻结金额
    int affected = accountMapper.update(
        "UPDATE account SET " +
        "  frozen_amount = frozen_amount - #{money}, " +
        "  version = version + 1 " +
        "WHERE user_id = #{userId} " +
        "  AND frozen_amount >= #{money} " +
        "  AND version = #{version}"
    );
    
    if (affected == 0) {
        log.warn("Confirm 被乐观锁拦截,xid={}", xid);
        // 重试或返回 true(让框架重试)
        return false;
    }
    
    return true;
}





// cancel阶段
@Transactional
public boolean cancel(BusinessActionContext ctx) {
    String xid = ctx.getXid();
    String userId = (String) ctx.getActionContext("userId");
    Integer money = (Integer) ctx.getActionContext("money");
    
    // 1. 先查当前版本号和冻结金额
    Account account = accountMapper.selectByUserId(userId);
    
    // 2. 幂等:如果冻结金额已经是 0,说明 Cancel 已完成或已 Confirm
    if (account.getFrozenAmount() == 0) {
        log.info("Cancel 幂等:冻结金额已为0,xid={}", xid);
        return true;
    }
    
    // 3. 乐观锁更新:恢复可用余额,清冻结金额
    int affected = accountMapper.update(
        "UPDATE account SET " +
        "  available_amount = available_amount + #{money}, " +
        "  frozen_amount = frozen_amount - #{money}, " +
        "  version = version + 1 " +
        "WHERE user_id = #{userId} " +
        "  AND frozen_amount >= #{money} " +
        "  AND version = #{version}"
    );
    
    if (affected == 0) {
        log.warn("Cancel 被乐观锁拦截,xid={}", xid);
        return false;  // 让框架重试
    }
    
    log.info("Cancel 成功:xid={}, userId={}, 恢复金额={}", xid, userId, money);
    return true;
}

声明TCC接口

TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,

我们在account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:

java 复制代码
package cn.itcast.account.service;

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

@LocalTCC
public interface AccountTCCService {

    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money")int money);

    boolean confirm(BusinessActionContext ctx);

    boolean cancel(BusinessActionContext ctx);
}

编写实现类

在account-service服务中的cn.itcast.account.service.impl包下新建一个类,实现TCC业务:

记得要在Try资源预留的方法前加 事务@Transactional

todo 这儿到底用@Transactional还是@GlobalTransactional,两个实例中不一致了,先说是@GlobalTransactional吧

java 复制代码
package cn.itcast.account.service.impl;

import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 0.获取事务id
        String xid = RootContext.getXID();
        // 1.扣减可用余额
        accountMapper.deduct(userId, money);
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务id
        String xid = ctx.getXid();
        // 2.根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        // 1.恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}

---------------------------另一种说法--------------------------------------------------------

AT模式基本上能满足我们使用分布式事务大部分需求,但涉及非关系型数据库与中间件的操作、跨公司服务的调用、跨语言的应用调用就需要结合TCC模式;

一个分布式的全局事务,整体是两阶段提交(Try - Comfirm/Cancel)的模型,在Seata中,AT模式与TCC模式事实上都是基于两阶段提交,它们的区别在于:

  • AT模式基于支持本地ACID事务的关系型数据库:
  1. 一阶段prepare行为:在本地事务中,一并提交"业务数据更新"和"相应回滚日志记录";
  2. 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志;
  3. 阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚;
  • 而TCC 模式,需要我们人为编写代码实现提交和回滚:
  1. 一阶段 prepare 行为:调用自定义的 prepare 逻辑;(真正要做的事情,比如插入订单,更新库存,更新余额)
  2. 二阶段 commit 行为:调用自定义的 commit 逻辑;(自己写代码实现)
  3. 二阶段 rollback 行为:调用自定义的 rollback 逻辑;(自己写代码实现)

所以TCC模式,就是把自定义的分支事务的提交和回滚并纳入到全局事务管理中;

通俗来说,Seata的TCC模式就是手工版本的AT模式,它允许你自定义两阶段的处理逻辑而不需要依赖AT模式的undo_log回滚表;

基于SpringBoot单体应用的TCC事务

  • @LocalTCC

该注解标识此TCC为本地模式,即该事务是本地调用,非RPC调用,@LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法即可;

  • @TwoPhaseBusinessAction

该注解标识为TCC模式,注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指提交方法,rollbackMethod指事务回滚方法,指定好三个方法之后,Seata会根据事务的成功或失败,通过动态代理去帮我们自动调用提交或者回滚;

  • @BusinessActionContextParameter

该注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法;BusinessActionContext 是指TCC事务上下文,携带了业务方法的参数;

  • commitMethod可以是空确认,返回true即可
java 复制代码
package com.bjpowernode.service;

public interface OrderService {

    /**
     * 下订单
     *
     * @param userId 用户id
     * @param productId 产品id
     * @return 订单id
     * @throws Exception 创建订单失败,抛出异常
     */
    Integer createOrder(Integer userId, Integer productId) throws Exception;

}








package com.bjpowernode.service;

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

import java.math.BigDecimal;

@LocalTCC
public interface AccountService {

    /**
     * 扣除余额
     * 定义两阶段提交
     * name = reduceStock为一阶段try方法
     * commitMethod = commitTcc 为二阶段确认方法
     * rollbackMethod = cancel 为二阶段取消方法
     * BusinessActionContextParameter注解 可传递参数到二阶段方法
     *
     * @param userId 用户ID
     * @param money  扣减金额
     * @throws Exception 失败时抛出异常
     */
    @TwoPhaseBusinessAction(name = "reduceBalance", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    void reduceBalance(@BusinessActionContextParameter(paramName = "userId") Integer userId,
                       @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    /**
     * 确认方法、可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param context 上下文
     * @return boolean
     */
    boolean commitTcc(BusinessActionContext context);

    /**
     * 二阶段取消方法
     *
     * @param context 上下文
     * @return boolean
     */
    boolean cancelTcc(BusinessActionContext context);

}






package com.bjpowernode.service;

import com.bjpowernode.model.Product;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface ProductService {

    /**
     * 减库存
     *
     * 定义两阶段提交
     * name = reduceStock为一阶段try方法
     * commitMethod = commitTcc 为二阶段确认方法
     * rollbackMethod = cancel 为二阶段取消方法
     * BusinessActionContextParameter注解 可传递参数到二阶段方法
     *
     * @param productId 商品ID
     * @param amount    扣减数量
     * @throws Exception 扣减失败时抛出异常
     */
    @TwoPhaseBusinessAction(name = "reduceStock", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    Product reduceStock(@BusinessActionContextParameter(paramName = "productId") Integer productId,
                        @BusinessActionContextParameter(paramName = "amount") Integer amount);

    /**
     * 二阶段提交方法
     *
     * 确认方法、可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param context 上下文
     * @return boolean
     */
    boolean commitTcc(BusinessActionContext context);

    /**
     * 二阶段回滚方法
     *
     * @param context 上下文
     * @return boolean
     */
    boolean cancelTcc(BusinessActionContext context);

}
java 复制代码
package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.OrdersMapper;
import com.bjpowernode.model.Orders;
import com.bjpowernode.model.Product;
import com.bjpowernode.service.AccountService;
import com.bjpowernode.service.OrderService;
import com.bjpowernode.service.ProductService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductService productService;

    @Override
    @DS(value = "order-ds")
    @GlobalTransactional //seata全局事务注解, TM 事务发起方
    public Integer createOrder(Integer userId, Integer productId) throws Exception {
        Integer amount = 1; // 购买数量暂时设置为 1

        log.info("当前 XID: {}", RootContext.getXID());

        // 减库存
        Product product = productService.reduceStock(productId, amount);

        // 减余额
        accountService.reduceBalance(userId, product.getPrice());

        // 下订单
        Orders order = new Orders();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));

        ordersMapper.insertSelective(order);

        log.info("下订单: {}", order.getId());

        int a = 10 / 0;

        // 返回订单编号
        return order.getId();
    }
}







package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.ProductMapper;
import com.bjpowernode.model.Product;
import com.bjpowernode.service.AccountService;
import com.bjpowernode.service.ProductService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductMapper productMapper;

    @DS(value = "product-ds")
    @Override
    public Product reduceStock(Integer productId, Integer amount) {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        Product product = productMapper.selectByPrimaryKey(productId);
        if (product.getStock() < amount) {
            throw new RuntimeException("库存不足");
        }

        // 扣减库存
        int updateCount = productMapper.reduceStock(productId, amount);
        // 扣除成功
        if (updateCount == 0) {
            throw new RuntimeException("库存不足");
        }

        // 扣除成功
        log.info("扣除 {} 库存成功", productId);

        return product;
    }

    /**
     * tcc服务(confirm)方法
     * 可以空确认
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "product-ds")
    @Override
    public boolean commitTcc(BusinessActionContext context) {
        log.info("Confirm阶段,ProductServiceImpl, commitTcc --> xid = " + context.getXid() + ", commitTcc提交成功");
        return true;
    }

    /**
     * tcc服务(cancel)方法
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "product-ds")
    @Override
    public boolean cancelTcc(BusinessActionContext context) {
        log.info("Cancel阶段,ProductServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");

        //TODO 这里可以实现中间件、非关系型数据库的回滚操作
        log.info("Cancel阶段,ProductServiceImpl, cancelTcc this data: {}, {}", context.getActionContext("productId"), context.getActionContext("amount"));

        //进行数据库回滚处理
        Integer productId = (Integer)context.getActionContext("productId");
        Integer amount = (Integer)context.getActionContext("amount");
        //把库存再加回去 (避免数据出问题,加个锁,分布式环境下就需要分布式锁)
        productMapper.increaseStock(productId, amount);

        return true;
    }
}





package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.AccountMapper;
import com.bjpowernode.model.Account;
import com.bjpowernode.service.AccountService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @DS(value = "account-ds")
    @Override
    public void reduceBalance(Integer userId, BigDecimal money) {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查余额
        Account account = accountMapper.selectAccountByUserId(userId);
        if (account.getBalance().doubleValue() < money.doubleValue()) {
            throw new RuntimeException("余额不足");
        }

        // 扣除余额
        int updateCount = accountMapper.reduceBalance(userId, money);
        // 扣除成功
        if (updateCount == 0) {
            throw new RuntimeException("余额不足");
        }
        log.info("扣除用户 {} 余额成功", userId);

        //int a = 10 / 0;
    }

    /**
     * tcc服务(confirm)方法
     * 可以空确认
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "account-ds")
    @Override
    public boolean commitTcc(BusinessActionContext context) {
        log.info("Confirm阶段,AccountServiceImpl, commitTcc --> xid = {}", context.getXid() + ", commitTcc提交成功");
        return true;
    }

    /**
     * tcc服务(cancel)方法
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "account-ds")
    @Override
    public boolean cancelTcc(BusinessActionContext context) {
        log.info("Cancel阶段,AccountServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");
        //TODO 这里可以实现中间件、非关系型数据库的回滚操作
        log.info("Cancel阶段,AccountServiceImpl, cancelTcc this data: userId= {}, money = {}", context.getActionContext("userId"), context.getActionContext("money"));

        //进行数据库回滚处理
        Integer userId = (Integer)context.getActionContext("userId");
        BigDecimal money = (BigDecimal)context.getActionContext("money");

        //把余额再加回去
        accountMapper.increaseBalance(userId, money);

        return true;
    }
}

基于Spring Cloud Alibaba的TCC分布式事务

具体代码实现和springboot单体应用的代码实现几乎没有区别,具体参考Git上提交的代码;

分布式事务回滚日志排查与统计

在分布式系统中实现事务回滚日志可以遵循以下步骤:

  1. 设计日志结构
  • 事务信息记录:

每个事务在开始时,生成一个唯一的事务 ID。这个 ID 将贯穿整个事务的生命周期,并用于关联事务的各个操作和日志记录。

记录事务的开始时间、发起者、涉及的服务或资源等元信息。这些信息对于后续的故障排查和事务恢复非常重要。

  • 操作记录:

对于事务中的每个操作,记录操作的类型(如插入、更新、删除)、操作的对象(表名、对象 ID 等)、操作的参数(如更新的值、删除的条件等)。

记录操作的顺序,以便在回滚时能够按照正确的顺序撤销操作。

  1. 事务执行与日志记录
  • 同步记录:

在事务执行过程中,每个操作在实际执行之前,先将操作的日志记录写入回滚日志。确保日志记录的写入是同步的,即操作只有在日志记录成功写入后才真正执行。

如果日志记录写入失败,应该立即中止事务,并进行错误处理。

  • 事务状态更新:

随着事务的执行,不断更新事务的状态信息在日志中。例如,记录事务的当前阶段(如开始、进行中、提交、回滚等)、已执行的操作数量、是否出现错误等。

这些状态信息对于判断事务的进展和进行故障恢复非常重要。

  1. 定期清理和归档
  • 日志清理:

为了防止回滚日志无限增长,需要定期清理已完成且不需要回滚的事务日志。可以根据时间、事务状态或其他策略来确定哪些日志可以被清理。

在清理日志之前,确保事务已经完全提交或回滚,并且不会再有需要回滚的情况。

  • 日志归档:

对于一些重要的事务或需要长期保留的日志,可以将其归档到其他存储介质中,以便进行审计或历史分析。归档可以按照一定的时间周期或事务类型进行。

相关推荐
独行侠影a13 小时前
SpringBoot 分布式锁实战:Redisson 解决订单超卖并发问题
spring boot·分布式·后端
运维行者_18 小时前
网络监控与ITSM集成:从告警到工单,实现运维自动化闭环
开发语言·网络·分布式·后端·架构·flask·php
猿长大人21 小时前
C# | MediatR 入门指南:后端架构解耦
分布式·后端·架构·c#·.net
蓝胖的四次元口袋1 天前
分布式知识梳理(2)
分布式
小罗水1 天前
附录D 常见问题排查指南
分布式
华章酱2 天前
基于 RabbitMQ 与 Hyperf 的消息顺序性保障方案
分布式·rabbitmq
ACP广源盛139246256732 天前
2026 PCIe互连芯片@ACP#国产替代格局解析:芯动科技领跑高端交换芯片赛道
大数据·网络·数据库·人工智能·分布式·嵌入式硬件
creator_Li2 天前
Kafka深入刨析-Consumer
分布式·kafka
国科安芯2 天前
四通道集成降压稳压器在低轨卫星星座分布式供电架构中的应用研究
分布式·架构·电源管理系统·低轨卫星星座·分布式供电·dc-dc降压稳压器·抗辐射加固