什么是TCC事务
TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作: 预处理Try、确认Confirm、撤销Cancel。 Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,
TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试
数据库
CREATE DATABASE `hmily` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
use `bank1`;
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户
主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行
卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT
'帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT =
Dynamic;
INSERT INTO `account_info` VALUES (2, '张三的账户', '1', '', 10000);
CREATE DATABASE `bank2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
use `bank2`;
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户
主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行
卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT
'帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT =
Dynamic;
INSERT INTO `account_info` VALUES (3, '李四的账户', '2', NULL, 0);
每个数据库都创建try、confirm、cancel三张日志表:
CREATE TABLE `local_try_log` (
`tx_no` varchar(64) NOT NULL COMMENT '事务id',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`tx_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE `local_confirm_log` (
`tx_no` varchar(64) NOT NULL COMMENT '事务id',
`create_time` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE `local_cancel_log` (
`tx_no` varchar(64) NOT NULL COMMENT '事务id',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`tx_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
示例代码
@Autowired
AccountInfoDao accountInfoDao;
@Autowired
Bank2Client bank2Client;
@Transactional(rollbackFor = Exception.class)
@HmilyTCC(confirmMethod = "confirm", cancelMethod = "rollback")
public void updateAccountBalance(AccountChangeEvent accountChangeEvent) {
String transId = accountChangeEvent.getTxNo();
log.info("bank1 try begin 开始执行...xid:{}", transId);
if (accountInfoDao.isExistTry(transId) > 0) {
log.info("bank1 try 已经执行,无需重复执行,xid:{}", transId);
return;
}
if (accountInfoDao.isExistConfirm(transId) > 0 || accountInfoDao.isExistCancel(transId) > 0) {
log.info("bank1 try悬挂处理 cancel或confirm已经执行,不允许执行try,xid:{}", transId);
return;
}
if (accountInfoDao.subtractAccountBalance(accountChangeEvent) <= 0) {
log.info("bank1 try 扣减金额失败,xid:{}", transId);
return;
}
accountInfoDao.addTry(transId);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (accountChangeEvent.getAmount() == -1) throw new RuntimeException();
bank2Client.transfer(accountChangeEvent);
if (accountChangeEvent.getAmount() == -2) throw new RuntimeException();
log.info("bank1 try end 结束执行...xid:{}", transId);
}
@Transactional
public void confirm(AccountChangeEvent accountChangeEvent) {
String transId = accountChangeEvent.getTxNo();
log.info("bank1 confirm begin 开始执行...xid:{},accountNo:{},amount:{}", transId, accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount());
if (accountInfoDao.isExistConfirm(transId) > 0) {
log.info("bank1 confirm 已经执行,无需重复执行...xid:{}", transId);
return;
}
accountInfoDao.addConfirm(transId);
log.info("bank1 confirm end 结束执行...xid:{}", transId);
}
@Transactional
public void rollback(AccountChangeEvent accountChangeEvent) {
String transId = accountChangeEvent.getTxNo();
log.info("bank1 cancel begin 开始执行...xid:{}", transId);
if (accountInfoDao.isExistCancel(transId) > 0) {
log.info("bank1 cancel 已经执行,无需重复执行,xid:{}", transId);
return;
}
if (accountInfoDao.isExistTry(transId) <= 0) {
log.info("bank1 空回滚处理,try没有执行,不允许cancel执行,xid:{}", transId);
return;
}
accountInfoDao.addAccountBalance(accountChangeEvent);
accountInfoDao.addCancel(transId);
log.info("bank1 cancel end 结束执行...xid:{}", transId);
}
解决方案分析
生成还款计划是一个执行时长较长的业务,不建议阻塞主业务流程,此业务对一致性要求较低。 根据上述需求进行解决方案分析: 1、采用Seata实现2PC Seata在事务执行过程会进行数据库资源锁定,由于事务执行时长较长会将资源锁定较长时间,所以不适用。 2、采用Hmily实现TCC 本需求对业务一致性要求较低,因为生成还款计划的时长较长,所以不要求交易中心修改标的状态为“还款中”就立 即生成还款计划 ,所以本方案不适用。 3、基于MQ的可靠消息一致性 满标审批通过后由交易中心修改标的状态为“还款中”并且向还款服务发送消息,还款服务接收到消息开始生成还款 计划,基本于MQ的可靠消息一致性方案适用此场景 。 4、最大努力通知方案 满标审批通过后由交易中心向还款服务发送通知要求生成还款计划,还款服务并且对外提供还款计划生成结果校对 接口供其它服务查询,最大努力 通知方案也适用本场景 。
|