前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分布式事务TCC模式的空回滚和业务悬挂问题

分布式事务TCC模式的空回滚和业务悬挂问题

作者头像
benym
发布2022-07-14 17:02:22
5.4K0
发布2022-07-14 17:02:22
举报
文章被收录于专栏:后端知识体系后端知识体系

# TCC模式的空回滚和业务悬挂问题

首先回顾一下TCC模式

# TCC模式原理

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求Try成功Confirm一定要能成功。
  • Cancel:预留资源释放,可以理解为Try的反向操作。 举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
  • 阶段一(Try): 检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
TCC1
TCC1
  • 阶段二:假如要提交(Confirm),则冻结金额扣减30
TCC2
TCC2
  • 阶段三:如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
TCC3
TCC3

TCC工作模型图:

TCCALL
TCCALL
# 空回滚和业务悬挂问题

以代码中的account—service服务为例,利用TCC实现分布式事务需要完成以下逻辑:

  • 修改account-service,编写try、confirm、cancel逻辑
  • try业务:添加冻结金额,扣减可用金额
  • confirm业务:删除冻结金额
  • cancel业务:删除冻结金额,恢复可用金额
  • 保证confirm、cancel接口的幂等性
  • 允许空回滚
  • 拒绝业务悬挂

幂等性就是无论接口调用多少次,返回的结果应该具有一致性。那么什么是控回滚和业务悬挂呢? 空回滚:当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。 业务悬挂:对于已经空回滚的业务,如果以后继续执行try,就永远不可能confirm或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂。 如下图所示

TCC111
TCC111

空回滚情况: 上方调用分支按照TCC流程正常执行,此时下方调用分支因为某种原因而阻塞了,由于长时间没有执行,这个分支发生了超时错误,由TM经过2.1步骤发送超时错误,回滚全局事务的指令给TC,TC检查分支状态2.2,发现确实有一只分支超时,发送2.3回滚指令到各分支的RM,由RM执行2.4cancel操作。 此时对于第一个分支而言,执行cancel没有问题,因为流程正常。但对于第二个分支而言,他并没有执行第一步的try,所以此时第二个分支不能真正的执行cancel,需要执行空回滚,也就是说返回一个正常状态,且不报错。需要在cancel之前查看是否有前置的try,如果没有执行try则需要空回滚。

TCC222
TCC222

业务悬挂情况: 假设在上方的基础上,下方分支的阻塞畅通了,此时他执行1.4去锁定资源(try),但整个事务都已经回滚结束了,所以他不会执行第二阶段,但冻结了资源,这种情况应该进行避免。需要在try操作之前查看当前分支是否已经回滚过,如果已经回滚过则不能在执行try命令。

# 实现方法

为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务id和执行状态,为此我们设计了一张表:

代码语言:javascript
复制
CREATE TABLE `account_freeze_tbl` (
  `xid` varchar(128) NOT NULL COMMENT '事务id',
  `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;

  1. Try业务
  • 记录冻结金额和事务状态0到account_freeze表
  • 扣减account表可用金额
  1. Confirm业务
  • 根据xid删除account_freeze表的冻结记录(因为如果一个事务confirm那么记录就没有意义了)
  1. Cancel业务
  • 修改account_freeze表,冻结金额为0,state为2
  • 修改account表,恢复可用金额
  1. 如何判断是否空回滚
  • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
  1. 如何避免业务悬挂
  • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务
# TCC标准接口声明

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

代码语言:javascript
复制
@LocalTCC
public interface TCCService {
    /**
     * Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
     */
    @TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")
    /**
     * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致
     *
     * @param context 上下文,可以传递try方法的参数
     * @return boolean 执行是否成功 
     */
    void prepare(@BusinessActionContextParameter(paramName = "param") String param);
    /**
     * 二阶段回滚方法,要保证与rollbackMethod一致
     */
    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

# 在account-service中的具体实现

AccountTCCService.java

代码语言:javascript
复制
@LocalTCC
public interface AccountTCCService {

    // try阶段
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money") int money);
    // confirm阶段
    boolean confirm(BusinessActionContext context);
    // cancel阶段
    boolean cancel(BusinessActionContext context);
    
}

AccountTCCServiceImpl.java

代码语言:javascript
复制
@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {

    @Resource
    private AccountMapper accountMapper;

    @Resource
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 数据库的money是unsigned字段,不可能为负数,所以这里不用检测余额
        // 直接扣减为负数会抛出异常,这里的事务注解回滚
        // 0. 获取事务id
        String xid = RootContext.getXID();
        // 业务悬挂处理,防止已经发起回滚操作后,阻塞的try恢复,进行扣减
        // 导致无法confirm也无法cancel
        // 1. 判断freeze中是否有冻结记录,如果有,一定是CANCEL执行过,需要拒绝业务
        AccountFreeze oldFreeze = freezeMapper.selectById(xid);
        if (oldFreeze != null) {
            // 拒绝
            return;
        }
        // 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 context) {
        // 因为try获取成功后进入confirm,意味着分支状态检查通过
        // 发起了事务提交指令,free表的数据就没有意义了,直接删除即可
        // 1. 获取事务id
        String xid = context.getXid();
        // 2. 根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        String xid = context.getXid();
        // 0. 查询冻结记录,可以走数据库,也可以走上下文
        AccountFreeze freeze = freezeMapper.selectById(xid);
        String userId = context.getActionContext("userId").toString();
        // 1. 空回滚判断,判断freeze是否为null,为null证明try没执行,需要空回滚
        if (freeze == null) {
            // 证明try没执行,需要空回滚,记录一下这个回滚的信息
            freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return true;
        }
        // 2. 幂等判断,只要cancel执行了,这个状态一定是CANCEL
        // 所以判断这个值就可以知道是否幂等,防止上一轮cancel超时后重复执行cancel
        if (freeze.getState() == AccountFreeze.State.CANCEL) {
            // 已经处理过一次CANCEL了,无需重复处理
            return true;
        }
        // 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;
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-02-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # TCC模式的空回滚和业务悬挂问题
    • # TCC模式原理
      • # 空回滚和业务悬挂问题
        • # 实现方法
          • # TCC标准接口声明
            • # 在account-service中的具体实现
            相关产品与服务
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档