TCC Demo 代码实现
简介
设计实现一个 TCC 分布式事务框架的简单 Demo,实现事务管理器,不需要实现全局事务的持久化和恢复、高可用等
工程运行
- 工程地址:TCCDemo
需要MySQL数据库,保存全局事务信息,相关TCC步骤都会打印在控制台上
- 1:启动MySQL,创建一个数据库 test
- 2.运行当前工程的:TccDemoApplication,启动以后自动创建数据库的表
- 3.访问:http://localhost:8080/transaction/commit,confirm示例
- 4.访问:http://localhost:8080/transaction/cancel,cancel示例
大致实现思路
- 1.初始化:想事务管理器注册新事务,生成全局事务唯一ID
- 2.try阶段执行:try相关的代码执行,期间注册相应的调用记录,发送try执行结果到事务管理器,执行成功由事务管理器执行confirm或者cancel步骤
- 3.confirm阶段:事务管理器收到try执行成功信息,根据事务ID,进入事务confirm阶段执行,confirm失败进入cancel,成功则结束
- 4.cancel阶段:事务管理器收到try执行失败或者confirm执行失败,根据事务ID,进入cancel阶段执行后结束,如果失败了,打印日志或者告警,让人工参与处理
前置知识
TCC 原理
TCC分布式事务主要的三个阶段:
- 1.Try:主要是对业务系统做检测及资源预留
- 2.Confirm:确认执行业务操作
- 3.Cancel:取消执行业务操作
下面以一个例子来说明三个阶段需要做的事:比如现在有两个数据库,一个用户账户数据库、一个商品库存数据库,现在提供一个买货的接口,当买卖成功时,扣除用户账户和商品库存,大致伪代码如下:
public void buy() {
// 用户账户操作
userAccount();
// 商品账户操作
StoreAccount();
}
在上面这个操作做,两个函数的操作必须同时成功,不然就会出现数据不一致问题,也就是需要保证事务原子性。
因为设定的场景是数据在两个不同的数据库,所有没有办法利用单个数据库的事务机制,它是跨数据库的,所以需要分布式事务的机制。
下面简单模拟下,在不使用TCC事务管理器,按照TCC的思想,在代码中如何保证事务原子性
TCC 无事务管理器 Demo 伪代码
使用上面的场景,代码大致如下:
class Demo {
public void buy() {
// try 阶段:比如去判断用户和商品的余额和存款是否充足,进行预扣款和预减库存
if (!userServer.tryDeductAccount()) {
// 用户预扣款失败,相关数据没有改变,返回错误即可
}
if (!storeService.tryDeductAccount()) {
// cancel 阶段: 商品预减库存失败,因为前面进行了用户预扣款,所以需要进入cancel阶段,恢复用户账户
userService.cancelDeductAccount();
}
// Confirm 阶段:try 成功就进行confirm阶段,这部分操作比如是将扣款成功状态和减库存状态设置为完成
if (!userService.confirmDeductAccount() || !storeService.confirmDeductAccount()) {
// cancel 阶段:confirm的任意阶段失败了,需要进行数据恢复(回滚)
userService.cancelDeductAccount();
storeService.cancelDeductAccount();
}
}
}
上面就是一个TCC事务大致代码,可以看到:之前的每个函数操作都需要分为三个子函数,try、confirm、cancel。将其细化,在代码中判断执行,保证其事务原子性。
上面是两个服务,用户账户和商品存储操作,看着写起来不是太多,但如果是多个服务呢?try阶段就会多很多的if,还有相应的cancel的动态增加,confirm也是,大致如下:
class Demo {
public void buy() {
// try 阶段:比如去判断用户和商品的余额和存款是否充足,进行预扣款和预减库存
if (!userServer.tryDeductAccount()) {
// 用户预扣款失败,相关数据没有改变,返回错误即可
}
if (!storeService.tryDeductAccount()) {
// cancel 阶段: 商品预减库存失败,因为前面进行了用户预扣款,所以需要进入cancel阶段,恢复用户账户
userService.cancelDeductAccount();
}
// try增加、cancel也动态增加
if (!xxxService.tryDeductAccount()) {
xxxService.cancelDeductAccount();
xxxService.cancelDeductAccount();
}
if (!xxxService.tryDeductAccount()) {
xxxService.cancelDeductAccount();
xxxService.cancelDeductAccount();
xxxService.cancelDeductAccount();
}
........
// Confirm 阶段:try 成功就进行confirm阶段,这部分操作比如是将扣款成功状态和减库存状态设置为完成
if (!userService.confirmDeductAccount() || !storeService.confirmDeductAccount() || ......) {
// cancel 阶段:confirm的任意阶段失败了,需要进行数据恢复(回滚)
userService.cancelDeductAccount();
storeService.cancelDeductAccount();
.......
}
}
}
可以看出代码相似性很多,工程中相似的需要分布式调用的有很多,这样的话,大量这样的类似代码就会充斥在工程中,为了偷懒,引入TCC事务管理器就能简化很多
TCC 事务管理器
为了偷懒,用事务管理器,那偷的是哪部分懒呢?在之前的代码中,try阶段还是交给本地程序去做,而confirm和cancel委托给了事务管理器。下面看下Seata和Hmily的TCC伪代码:
interface UserService {
@TCCAction(name = "userAccount", confirmMethod = "confirm", cancelMethod = "cancel")
public void try();
public void confirm();
public void cancel();
}
interface