一、为什么选择TCC
我们的服务调用使用的Dubbo,并且是大额资金的敏感操作,再不改变原有调用方式的情况下,TCC是最合适的。
二、业界主流的TCC框架
我们在使用时主要调研了TCC-Trancsactional,后续又调研了开源的Seata(Fescar)。最终结合实际情况实现了自己的TCC框架。
Seata TCC原理
- 开去全局事务,TM向TC申请开启全局事务,TC创建一个全局事务ID并返回给TM,全局事务开启,将全局事务id纳入RootContext(ThreadLocal)。
- 调用到要执行的业务方法时,解析TCC Bean,创建TCC Resource,注册到TC。
- 调用到要执行的业务方法时,向TC注册分支事务,并将分支事务纳入到全局事务的管理中。
- TM向TC发起全局提交或者回滚。
Seata TCC的不足之处
- 参与者和TC之间多次RPC调用,网络开销会影响一定的性能。
- 目前只支持File存储事务,事务恢复机制不完整。
- 异常控制需要业务方自己控制。
TCC Tracnsactional
TCC Transactional主要是两个切面,它没有单独的事务协调器,而是把事务协调器和应用绑定在一起,这样的好处是只要保证应用的可用性就能保证事务协调器的可用,而且减少了业务方和TC间的RPC的调用。提供了多种事务存储方式,拥有事务恢复能力和运维平台,并且已经有很多公司在使用。
TCC Transactional也有不足之处,它的异常控制机制不完善,在发生网络抖动、被调用方宕机等情况的时候依旧会出现问题。
三、数据库模型设计
交易服务至少要调用三个服务,每个服务都涉及到了资金的变化。原有的数据库模型在使用TCC之后并不能很好的适应TCC的设计思想,在参考了蚂蚁金服的设计之后,我们重新优化了持仓服务、费用服务的数据模型。
-
持仓服务原来的数据库模型
CREATE TABLE IF NOT EXISTS `position`( `id` INT UNSIGNED AUTO_INCREMENT, `amount` DECIMAL(20,8), -- 持仓金额 PRIMARY KEY ( `id` ) )ENGINE=InnoDB DEFAULT CHARSET=utf8; 复制代码
-
改造后的持仓数据库模型
CREATE TABLE IF NOT EXISTS `position`( `id` INT UNSIGNED AUTO_INCREMENT, `amount` DECIMAL(20,8), -- 持仓金额 `frozen` DECIMAL(20,8), -- 冻结资金 PRIMARY KEY ( `id` ) )ENGINE=InnoDB DEFAULT CHARSET=utf8; 复制代码
三、并发控制
-
Try阶段
update position set amount=amount - #{money},frozen=frozen + #{money} where id = #{id} and amount > #{money}; 复制代码
扣减持仓,增加冻结金额。
-
Confirm阶段
update position set frozen= frozen - #{money} where id = #{id}; 复制代码
释放冻结资金。
-
Cancel阶段
update position set amount=amount + #{money} ,frozen= frozen - #{money} where id =#{id} and frozen > #{money}; 复制代码
增加持仓,释放冻结资金。
四、异常
1. 滚-try未执行cancel执行
- Try调用超时
- 被调用方短时间内假死
2. 幂等问题
- 网络抖动
- 事务恢复多次重试
3. 悬挂
- try接口超时,执行cancel,try接口恢复
五、异常控制
create table tcc_transaction_xxx(
`id` INT UNSIGNED AUTO_INCREMENT,
`app` char(20) comment '业务模块',
`tx_id` varchar(100) not null comment '全局事务id',
`branch_id` varchar(100) not null comment '分支事务id',
`create_time` datetime not null comment '创建时间',
`update_time` datetime not null comment '更新时间',
`content` varbinary(8000) default null comment '事务内容',
`status` tinyint not null comment '状态<>try=0=已发送&confirm=1=已提交&cancel=已回滚',
`version` int(11) default 0 comment '版本',
`retried_content` int(11) default 0 comment '重试次数',
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
复制代码
- Try阶段
执行业务一阶段得时候先插事务控制表,判断status是否为二阶段,二阶段执行则直接抛出异常,终止一阶段。
- confirm阶段
- cancel阶段
参考文章
分布式事务 Seata TCC 模式深度解析 | SOFAChannel#4 直播整理 - 金融级分布式架构的文章 - 知乎 zhuanlan.zhihu.com/p/63552935
Seata TCC 分布式事务源码分析 - Young Chen的文章 - 知乎 zhuanlan.zhihu.com/p/64484721
Seata TCC 分布式事务源码分析 - Young Chen的文章 - 知乎 zhuanlan.zhihu.com/p/64484721