一 TCC模式
1.1 TCC的逻辑
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC需要人工干预编写代码。需要实现三个方法:
1.Try:资源的检测和预留;
2.Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
3.Cancel:预留资源释放,可以理解为try的反向操作。
代码地址: https://gitee.com/jurf-liu/seata-demo.git
1.2 流程图
TCC的执行流程可以分为两个阶段,分别如下:
1.第一阶段:Try,业务系统做检测并预留资源 (加锁,锁住资源),比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库存给锁定住。
2.第二阶段:根据第一阶段的结果决定是执行confirm还是cancel
Confirm:执行真正的业务(执行业务,释放锁)
Cancle:是对Try阶段预留资源的释放(出问题,释放锁)
原文链接:https://blog.csdn.net/a745233700/article/details/122402303
1.3 优缺点
TCC的优点:
1.一阶段完成直接提交事务,释放数据库资源,性能好
2.相比AT模型,无需生成快照,无需使用全局锁,性能最强
3.不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么
1.有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
2.软状态,事务是最终一致
3.需要考虑Confirm和Cancel的失败情况,做好幂等处理
1.4 实现空回滚与悬挂
1.4.1 空回滚(允许空回滚)
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作(后发先至),这时cancel进行回滚操作,就是空回滚。
解决办法:在cancel操作时,应当判断try操作是否已执行,如果未执行,则cacel操作就是空回滚。
1.4.2 空悬挂(防止空悬挂)
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
二 利用TCC实现分布式事务的案例操作
2.1 工程结构
代码地址:https://gitee.com/jurf-liu/seata-demo.git
2.2 需求的描述
1.修改account-service,编写try,confirm,cancel逻辑;
2.try业务: 添加冻结金额,扣减可用金额;
3.confirm业务:删除冻结金额
4.cancel业务:修改冻结金额为0,恢复可用金额
2.3 附件数据库
2.4 编写try,confirm,cancel方法
1.在account-service模块,新增接口
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 = "deductInfo", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
2.定义实现类
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;
}
}
2.5 解决空回滚和悬挂
2.5.1 理论思想
为了防止空回滚,业务悬挂,保证幂等性要求,需要新增一张表冻结表,在数据库记录冻结金额的同时,记录当前事务id和执行状态。
try业务:记录冻结金额和事务状态到account_freeze表;扣减account表可用金额;
confirm业务:根据xid删除account_freeze表的冻结记录;
cancel业务: 修改account_freeze表,冻结金额为0,state为2;修改account表,恢复可用金额
如何判断是否空回滚:
在cancel业务中,根据xid查询account_freeze表,如果为null,则说明try还没做,需要空回滚。
如何判断业务空悬挂:在try业务中,根据xid查询account_freeze,如果已经存在,则证明cancel已经执行,拒绝执行try业务。
如下图所示:
2.5.2 代码
1.try方法
2.concel方法
3.代码
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();
//判断freeze是否有详情记录,如果有,则一定是cancel执行过,需要拒绝业务
AccountFreeze oldFreeze=freezeMapper.selectById(xid);
if(oldFreeze!=null){
//是cancel执行过,需要拒绝业务
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 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();
String userId= ctx.getActionContext().get("userId").toString();
AccountFreeze freeze = freezeMapper.selectById(xid);
//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);
}
//2.保持幂等性
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;
}
}
2.6 controller的调用修改
修改controller调用
2.8 启动nacos,seata
1.启动nacos
2.启动seata
2.7 验证
2.7.1 初始态数据表
2.7.2 正常下单
2.各表数据
在表account_freeze中冻结记录进行删除,此时查看所以还是空记录。因为confirm业务:根据xid删除account_freeze表的冻结记录;
2.7.3 订单数大于库存数访问
存库量为:2; 请求量为:10,库存扣减发生异常,触发try异常执行cancel方法,进行回滚
2.查看控制台
1.account:
2.order
3.storage: 发生扣减异常
3.查看数据表:回滚执行了cancel方法,冻结表新增一条冻结数据,其他表均进行了回滚。
cancel业务:修改account_freeze表,冻结金额为0,state为2;修改account表,恢复可用金额。