Seata的TCC 模式
TCC事务模式
TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:
- Try:对业务资源的检查并预留;
- Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
- Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。
TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。
Seata的TCC模式
Seata TCC 模式跟通用型 TCC 模式原理一致。
TCC和AT区别
AT 模式基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
具体实现
实现代码
首先我们还是使用上次实现AT模式的项目,一个订单服务seata-order-8001, 一个库存服务seata-stock-8002。(具体配置可参考专栏里面的另一篇文章——四、Seata的AT模式)
- 首先在订单服务seata-order-8001服务中新增方法
OrderController:
@GetMapping("/createOrder-tcc")
@GlobalTransactional// 开启分布式事务
public String createTcc(){
Map<String, String> params = new HashMap<>(1);
params.put("name", "张三");
orderService.createTcc(params);
return "生成订单";
}
OrderService:
/**
* TCC
* @param params
*/
void createTcc(Map<String, String> params);
OrderServiceImpl:
@Override
public void createTcc(Map<String, String> params) {
System.out.println("TCC------------------> xid = " + RootContext.getXID());
// 减库存
stockClient.changeStockTcc(params);
// 添加异常, 测试时此处添加断点
int i = 1/0;
// 创建订单
orderMapper.create();
}
StockClient:
@PutMapping("/changeStock-tcc")
String changeStockTcc(Map<String, String> params);
- 在库存服务seata-stock-8002服务中新增方法和文件
StockController:
package com.seata.seataStock8002.controller;
import com.seata.seataStock8002.service.StockService;
import com.seata.seataStock8002.service.StockServiceTcc;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
public class StockController {
@Resource
private StockService stockService;
@Resource
private StockServiceTcc stockServiceTcc;
@PutMapping("/changeStock-at")
public String changeStockAt() {
stockService.changeStockAt();
return "库存减1";
}
@PutMapping("/changeStock-tcc")
public String changeStockTcc(@RequestBody Map<String, String> params) {
stockServiceTcc.changeStockTcc(params);
return "库存减1";
}
}
新建StockServiceTcc文件:
package com.seata.seataStock8002.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import java.util.Map;
/**
* 这里定义tcc的接口
* 一定要定义在接口上
* 我们使用springCloud的远程调用
* 那么这里使用LocalTCC便可
*/
@LocalTCC
public interface StockServiceTcc {
/**
* 定义两阶段提交
* name = 该tcc的bean名称,全局唯一, 一般写方法名即可;
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中
*
* @param params -入参
* @return String
*/
@TwoPhaseBusinessAction(name = "changeStockTcc", commitMethod = "commitTcc", rollbackMethod = "cancel")
void changeStockTcc(@BusinessActionContextParameter(paramName = "params") Map<String, String> params);
/**
* 确认方法、可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param context 上下文
* @return boolean
*/
boolean commitTcc(BusinessActionContext context);
/**
* 二阶段回滚方法
*
* @param context 上下文
* @return boolean
*/
boolean cancel(BusinessActionContext context);
}
新建StockServiceTccImpl文件:
package com.seata.seataStock8002.service.impl;
import com.seata.seataStock8002.mapper.StockMapper;
import com.seata.seataStock8002.service.StockService;
import com.seata.seataStock8002.service.StockServiceTcc;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Map;
@Service
public class StockServiceTccImpl implements StockServiceTcc {
@Resource
private StockMapper stockMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void changeStockTcc(Map<String, String> params) {
System.out.println("changeStockTcc------------------> xid = " + RootContext.getXID());
stockMapper.subStock();
}
/**
* tcc服务 confirm方法
* 若一阶段采用资源预留,在二阶段确认时要提交预留的资源
*
* @param context 上下文
* @return boolean
*/
@Override
public boolean commitTcc(BusinessActionContext context) {
System.out.println("xid = " + context.getXid() + "提交成功");
//todo 若一阶段资源预留,这里则要提交资源
return true;
}
/**
* tcc 服务 cancel方法
*
* @param context 上下文
* @return boolean
*/
@Override
public boolean cancel(BusinessActionContext context) {
//todo 这里写中间件、非关系型数据库的回滚操作
System.out.println("please manually rollback this data:" + context.getActionContext("params"));
// 回滚
stockMapper.subStockRollback();
return true;
}
}
StockMapper:
@Update("update tb_stock set count = count + 1 where goods_id = 1")
void subStockRollback();
数据库设计
还是使用两个数据库:seata-order和seata-stock
案例演示
前提:Nacos 和将Seata-Server为正常运行状态
-
首先,将服务seata-order-8001和 seata-stock-8002用Debug模式运行起来
-
调用接口前,seata-stock库中的库存表tb_stock的库存数量count为100,seata-order库中的订单表tb_order中无订单记录
-
然后我们在订单服务中的OrderServiceImpl的createTcc方法中打上断点, 在库存服务中的StockServiceTccImpl的cancel方法中也打上断点
-
这个时候我们进行访问Order的REST接口:http://localhost:8001/createOrder-tcc,此时程序进了断点,我们去查看数据库
此时可以看到seata-stock库中库存表tb_stock的库存数量count减少了到了99
然后放行第一个断点,可以看到程序进了库存服务的cancel方法,放行断点,数据被回滚更新
此时我们查看数据库,seata-stock库中库存表tb_stock的库存数量count恢复到了100
seata-order库中的订单表tb_order中订单记录也没有新增,此时我们就验证了TCC事务的执行过程
注意
被调用方需要用到的几个注解:
@LocalTCC (必要)
该注解需要添加到接口上,表示实现该接口的类被 seata 来管理,seata 根据事务的状态,自动调用我们定义的方法,如果没问题则调用 Commit 方法,否则调用 Rollback 方法。
@TwoPhaseBusinessAction (必要)
该注解用在接口的 Try 方法上,该注解的用法如下:
@TwoPhaseBusinessAction(name = "changeStockTcc", commitMethod = "commitTcc", rollbackMethod = "cancel")
@BusinessActionContextParameter
该注解用来修饰 Try 方法的入参,被修饰的入参可以在 Commit 方法和 Rollback 方法中通过 BusinessActionContext 获取,例如:
@Override
public boolean cancel(BusinessActionContext context) {
//todo 这里写中间件、非关系型数据库的回滚操作
System.out.println("please manually rollback this data:" + context.getActionContext("params"));
// 回滚
stockMapper.subStockRollback();
return true;
}