:::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*/@LocalTCCpublic 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@Slf4jpublic class AccountTCCServiceImpl implements AccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountFreezeMapper accountFreezeMapper;/*** 用户账户扣款* 1 扣减account表可用金额* 2 记录冻结金额和事务状态到account_freeze表* @param userId* @param money**/@Override@Transactionalpublic 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*/@Overridepublic boolean confirm(BusinessActionContext ctx) {//1. 获取事务IdString 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@Transactionalpublic boolean cancel(BusinessActionContext ctx) {//1. 获取事务IdString 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,且更新状态为cancelfreeze = 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@Slf4jpublic class AccountTCCServiceImpl implements AccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountFreezeMapper accountFreezeMapper;/*** 用户账户扣款* 1 扣减account表可用金额* 2 记录冻结金额和事务状态到account_freeze表* @param userId* @param money**/@Override@Transactionalpublic 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*/@Overridepublic boolean confirm(BusinessActionContext ctx) {//1. 获取事务IdString 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@Transactionalpublic boolean cancel(BusinessActionContext ctx) {//1. 获取事务IdString 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,且更新状态为cancelfreeze = 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@Slf4jpublic class AccountTCCServiceImpl implements AccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountFreezeMapper accountFreezeMapper;/*** 用户账户扣款* 1 扣减account表可用金额* 2 记录冻结金额和事务状态到account_freeze表* @param userId* @param money**/@Override@Transactionalpublic 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*/@Overridepublic boolean confirm(BusinessActionContext ctx) {//1. 获取事务IdString 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@Transactionalpublic boolean cancel(BusinessActionContext ctx) {//1. 获取事务IdString 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,且更新状态为cancelfreeze = 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@Slf4jpublic class AccountTCCServiceImpl implements AccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountFreezeMapper accountFreezeMapper;/*** 用户账户扣款* 1 扣减account表可用金额* 2 记录冻结金额和事务状态到account_freeze表* @param userId* @param money**/@Override@Transactionalpublic 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*/@Overridepublic boolean confirm(BusinessActionContext ctx) {//1. 获取事务IdString 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@Transactionalpublic boolean cancel(BusinessActionContext ctx) {//1. 获取事务IdString 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,且更新状态为cancelfreeze = 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;}}
