:::tips 这里通过一个案例来进行实现TCC模式 :::
创建数据表
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;
:::tips
- xid:是全局事务id
- freeze_money:用来记录用户冻结金额
- state:用来记录事务状态
业务流程
- 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业务 :::
声明TCC接口
:::tips TCC模式的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,在微服务工程中的service包中新建一个接口,用来声明TCC模式的三个接口 :::
/**
* @version 1.0
* @description 说明
* @package cn.itcast.account.service
*/
@LocalTCC
public interface AccountTCCService {
/**
* 用户账户扣款
* 1 扣减account表可用金额
* 2 记录冻结金额和事务状态到account_freeze表
* @param userId
* @param money
*
*/
@TwoPhaseBusinessAction(name = "deduct",
commitMethod = "confirm",rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
/**
* 确认业务,根据xid删除account_freeze表的冻结记录
* @param ctx
* @return
*/
boolean confirm(BusinessActionContext ctx);
/**
* 撤消回滚,需要把冻结金额还原
* 1 修改account_freeze表,冻结金额为0,state为2
* 2 修改account表,恢复可用金额
* @param ctx
* @return
*/
boolean cancel(BusinessActionContext ctx);
}
实现接口
:::tips 在impl包中新建一个类,实现刚刚编写的接口 :::
/**
* @version 1.0
* @description 说明
* @package cn.itcast.account.service.impl
*/
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
/**
* 用户账户扣款
* 1 扣减account表可用金额
* 2 记录冻结金额和事务状态到account_freeze表
* @param userId
* @param money
*
*/
@Override
@Transactional
public void deduct(String userId, int money) {
String xid = RootContext.getXID();
//1. 扣减金额
accountMapper.deduct(userId,money);
log.debug("扣减金额成功: {},userId={}", xid, userId);
//2. 添加冻结记录
AccountFreeze freeze = new AccountFreeze();
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreezeMapper.insert(freeze);
log.debug("冻结金额成功! {},userId={}", xid, userId);
//3. 添加本地事务控制
}
/**
* 确认业务,根据xid删除account_freeze表的冻结记录
* @param ctx
* @return
*/
@Override
public boolean confirm(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
log.debug("进入confirm: {}", xid);
//2. 通过事务Id删除数据
int count = accountFreezeMapper.deleteById(xid);
log.debug("删除冻结记录 {},count={}",xid,count);
return 1==count;
}
/**
* 撤消回滚,需要把冻结金额还原
* 1 修改account_freeze表,冻结金额为0,state为2
* 2 修改account表,恢复可用金额
* @param ctx
* @return
*/
@Override
@Transactional
public boolean cancel(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
//2. 查询冻结记录
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
//3. 回滚金额
String userId = ctx.getActionContext("userId").toString();
log.debug("开始回滚金额: {},userId={}", xid, userId);
accountMapper.refund(userId, freeze.getFreezeMoney());
//4. 更新冻结金额为0,且更新状态为cancel
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
int count = accountFreezeMapper.updateById(freeze);
log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
//5. 本地事务控制
return count==1;
}
}
解决空回滚
:::tips 在Cancel方法中,根据xid查询account_freeze,如果为null则说明Try还没做,需要空回滚 :::
/**
* @version 1.0
* @description 说明
* @package cn.itcast.account.service.impl
*/
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
/**
* 用户账户扣款
* 1 扣减account表可用金额
* 2 记录冻结金额和事务状态到account_freeze表
* @param userId
* @param money
*
*/
@Override
@Transactional
public void deduct(String userId, int money) {
String xid = RootContext.getXID();
//1. 扣减金额
accountMapper.deduct(userId,money);
log.debug("扣减金额成功: {},userId={}", xid, userId);
//2. 添加冻结记录
AccountFreeze freeze = new AccountFreeze();
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreezeMapper.insert(freeze);
log.debug("冻结金额成功! {},userId={}", xid, userId);
//3. 添加本地事务控制
}
/**
* 确认业务,根据xid删除account_freeze表的冻结记录
* @param ctx
* @return
*/
@Override
public boolean confirm(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
log.debug("进入confirm: {}", xid);
//2. 通过事务Id删除数据
int count = accountFreezeMapper.deleteById(xid);
log.debug("删除冻结记录 {},count={}",xid,count);
return 1==count;
}
/**
* 撤消回滚,需要把冻结金额还原
* 1 修改account_freeze表,冻结金额为0,state为2
* 2 修改account表,恢复可用金额
* @param ctx
* @return
*/
@Override
@Transactional
public boolean cancel(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
//2. 查询冻结记录
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
//判断是否空回滚
if(freeze == null){
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreeMapper.insert(freeze);
return true;
}
//3. 回滚金额
String userId = ctx.getActionContext("userId").toString();
log.debug("开始回滚金额: {},userId={}", xid, userId);
accountMapper.refund(userId, freeze.getFreezeMoney());
//4. 更新冻结金额为0,且更新状态为cancel
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
int count = accountFreezeMapper.updateById(freeze);
log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
//5. 本地事务控制
return count==1;
}
}
解决业务悬挂
:::tips 在Try方法中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行Try业务 :::
/**
* @version 1.0
* @description 说明
* @package cn.itcast.account.service.impl
*/
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
/**
* 用户账户扣款
* 1 扣减account表可用金额
* 2 记录冻结金额和事务状态到account_freeze表
* @param userId
* @param money
*
*/
@Override
@Transactional
public void deduct(String userId, int money) {
String xid = RootContext.getXID();
//判断Cancel是否执行过,避免业务悬挂
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
if(freeze != null){
//Cancel已经执行过,拒绝业务
return;
}
//1. 扣减金额
accountMapper.deduct(userId,money);
log.debug("扣减金额成功: {},userId={}", xid, userId);
//2. 添加冻结记录
AccountFreeze freeze = new AccountFreeze();
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreezeMapper.insert(freeze);
log.debug("冻结金额成功! {},userId={}", xid, userId);
//3. 添加本地事务控制
}
/**
* 确认业务,根据xid删除account_freeze表的冻结记录
* @param ctx
* @return
*/
@Override
public boolean confirm(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
log.debug("进入confirm: {}", xid);
//2. 通过事务Id删除数据
int count = accountFreezeMapper.deleteById(xid);
log.debug("删除冻结记录 {},count={}",xid,count);
return 1==count;
}
/**
* 撤消回滚,需要把冻结金额还原
* 1 修改account_freeze表,冻结金额为0,state为2
* 2 修改account表,恢复可用金额
* @param ctx
* @return
*/
@Override
@Transactional
public boolean cancel(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
//2. 查询冻结记录
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
//判断是否空回滚
if(freeze == null){
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreeMapper.insert(freeze);
return true;
}
//3. 回滚金额
String userId = ctx.getActionContext("userId").toString();
log.debug("开始回滚金额: {},userId={}", xid, userId);
accountMapper.refund(userId, freeze.getFreezeMoney());
//4. 更新冻结金额为0,且更新状态为cancel
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
int count = accountFreezeMapper.updateById(freeze);
log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
//5. 本地事务控制
return count==1;
}
}
幂等处理
:::tips 如果冻结记录,则先判断一下这条冻结记录的状态,如果状态为2(Cancel),说明已经执行过了,不能再重复执行业务 :::
/**
* @version 1.0
* @description 说明
* @package cn.itcast.account.service.impl
*/
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
/**
* 用户账户扣款
* 1 扣减account表可用金额
* 2 记录冻结金额和事务状态到account_freeze表
* @param userId
* @param money
*
*/
@Override
@Transactional
public void deduct(String userId, int money) {
String xid = RootContext.getXID();
//判断Cancel是否执行过,避免业务悬挂
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
if(freeze != null){
//Cancel已经执行过,拒绝业务
return;
}
//1. 扣减金额
accountMapper.deduct(userId,money);
log.debug("扣减金额成功: {},userId={}", xid, userId);
//2. 添加冻结记录
AccountFreeze freeze = new AccountFreeze();
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreezeMapper.insert(freeze);
log.debug("冻结金额成功! {},userId={}", xid, userId);
//3. 添加本地事务控制
}
/**
* 确认业务,根据xid删除account_freeze表的冻结记录
* @param ctx
* @return
*/
@Override
public boolean confirm(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
log.debug("进入confirm: {}", xid);
//2. 通过事务Id删除数据
int count = accountFreezeMapper.deleteById(xid);
log.debug("删除冻结记录 {},count={}",xid,count);
return 1==count;
}
/**
* 撤消回滚,需要把冻结金额还原
* 1 修改account_freeze表,冻结金额为0,state为2
* 2 修改account表,恢复可用金额
* @param ctx
* @return
*/
@Override
@Transactional
public boolean cancel(BusinessActionContext ctx) {
//1. 获取事务Id
String xid = ctx.getXid();
//2. 查询冻结记录
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
//判断是否空回滚
if(freeze == null){
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
freeze.setUserId(userId);
accountFreeMapper.insert(freeze);
return true;
}
//幂等处理
if(AccountFreeze.State.CANCEL == freeze.getState().intValue()){
//状态为Cancel,说明已经执行过Cancel,不能再重复处理
return true;
}
//3. 回滚金额
String userId = ctx.getActionContext("userId").toString();
log.debug("开始回滚金额: {},userId={}", xid, userId);
accountMapper.refund(userId, freeze.getFreezeMoney());
//4. 更新冻结金额为0,且更新状态为cancel
freeze = new AccountFreeze();
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
int count = accountFreezeMapper.updateById(freeze);
log.debug("回滚-更新冻结状态:{}, userId={},count={}", xid, userId,count);
//5. 本地事务控制
return count==1;
}
}