TCC是Try、Confirm、Cancel三个词语的缩写
TCC要求每个分支事务(即多个不同的数据库实例)实现三个操作:预处理Try、确认 Confirm、撤销Cancel。
Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的 操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所 有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel 操作若执行失败,TM会进行重试
下面用几张图简单描述一下TCC模式下的执行流程
TCC分为三个阶段:
Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能 真正构成一个完整的业务逻辑。
Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则 认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引 入重试机制或人工处理。
Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采 用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
TM事务管理器
TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公 用组件,是为了考虑系统结构和软件复用
TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求 多少次,其结果都相同。
上面的执行是在操作成功的情况下,如果在分支事务执行失败的情况下,则会出现下面的情况:
最后我们再用简单的话总结一下TCC的执行过程:
其实在之前的篇章中,我们介绍过关于seata的概念,把seata的相关概念放到这里进行类比理解就很容易理解TCC 的执行流程,但需要说明的是,TCC模式下达到的效果是确保分布式事务的最终一致性
目前市面上的TCC框架众多比如下几种
框架名称 | Gitbub上star数量 |
---|---|
tcc-transaction | 3850 |
Hmily | 2407 |
ByteTCC | 1947 |
EasyTransaction | 1690 |
关于上面几种框架的技术和知识,有兴趣的同学可以参阅相关资料查找学习,本篇会提到Hmily ,其实seata也提供了tcc的事务解决方案,打算对这两种方式的TCC解决方案的代码整合和演示进行说明
Hmily是一个高性能分布式事务TCC开源框架。基于Java语言来开发(JDK1.8),支持Dubbo,Spring Cloud等 RPC框架进行分布式事务。它目前支持以下特性:
支持嵌套事务(Nested transaction support).
采用disruptor框架进行事务日志的异步读写,与RPC框架的性能毫无差别。
支持SpringBoot-starter 项目启动,使用简单。
RPC框架支持 : dubbo,motan,springcloud。
本地事务存储支持 : redis,mongodb,zookeeper,file,mysql。
事务日志序列化支持 :java,hessian,kryo,protostuff。
采用Aspect AOP 切面思想与Spring无缝集成,天然支持集群。
RPC事务恢复,超时异常恢复等。
Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的 调用到另一方的Try、Confirm、Cancel方法;传递事务上下文;并记录事务日志,酌情进行补偿,重试等。
Hmily不需要事务协调服务,但需要提供一个数据库(mysql/mongodb/zookeeper/redis/file)来进行日志存 储。Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务 逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被Hmily TCC事务框架 发现即可,不需要被调用它的其他业务服务所感知
3.1 环境准备
资源名称 | 版本 |
---|---|
seata-server | 1.0 |
mysql | 5.7.25 |
zookeeper | 3.4.6 |
dubbo | 2.7.0 |
3.2 业务描述
本文实现一个下订单减库存的场景,3个主工程模块,order - 订单模块,storage - 库存模块,代表2个分支事务,business - 业务实现模块,即开启全局事务的地方
3、数据库准备
按照业务描述,我们需要创建两个数据库,在2个库下分别保存着订单表和库存表,数据库执行sql如下:
CREATE TABLE `tcc_order` (
`order_id` int(255) NOT NULL AUTO_INCREMENT COMMENT '订单编号',
`order_code` varchar(255) DEFAULT NULL COMMENT '订单编码',
`goods_code` varchar(32) NOT NULL COMMENT '商品编码',
`quantity` int(255) NOT NULL COMMENT '购买数量',
`frozen_amount` float(255,0) NOT NULL DEFAULT '0' COMMENT '冻结金额 ',
`amount` float(255,0) NOT NULL COMMENT '物品总价',
`status` int(255) NOT NULL COMMENT '0-已创建 1-完成 2-取消',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=113 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
CREATE TABLE `tcc_storage` (
`storage_id` int(11) NOT NULL AUTO_INCREMENT,
`goods_code` varchar(255) NOT NULL,
`quantity` int(255) NOT NULL,
`frozen_quantity` varchar(255) NOT NULL,
PRIMARY KEY (`storage_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
4、启动seata-server和zookeeper
通过之前的关于seata的学习我们知道,seata-server组作为TC用于协调全局事务,即本身为一个服务,从github上面下载下来之后,windows下直接进入bin目录双击bat文件即可
zookeeper也是执行相同的操作,进入bin目录
以上为项目整合之前的环境准备,下面开始具体的项目搭建,采用聚合工程的方式搭建3个模块,由于使用到了dubbo,因此我们决定将接口层抽离出来作为一个单独的模块以便被其他模块引用
因此整个工程结构包括4个模块分别是:
其中order-provide 和 storage-provider具有相似之处,在下面的介绍中我们会选取其中的一种重点说明
order-provider
pom依赖
com.itlaoqi.seata.tcc
common
1.0.0-RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
5.1.47
org.apache.dubbo
dubbo
2.7.0
org.springframework
spring
io.seata
seata-all
1.0.0
org.apache.curator
curator-recipes
4.1.0
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
配置文件
由于采用seata,因此需要将file.conf和registry.conf两个文件拷贝到resources目录下,application.yml如下,file.conf和registry.conf直接从seata-server目录拷贝过来暂时不做修改
spring:
application:
name: order-provider
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://106.15.37.145:3306/tcc_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
jpa:
hibernate:
naming:
#开启驼峰命名转换
physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
show-sql: true
#数据库方言
database-platform: org.hibernate.dialect.MySQLDialect
server:
port: 8001
我们需要明白的是,order服务和storage服务各自提供一个操作数据库的方法即可,然后在business服务中通过dubbo的形式调用order服务和storage服务的接口,因此还需要配置duboo文件
下面进行编码,在order端,即提供下订单的服务接口
订单order实体类
@Entity
@Table(name="tcc_order")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer orderId;
private String orderCode;
private String goodsCode;
private Integer quantity;
private Float amount;
private Float frozenAmount;
private Integer status;
public Integer getOrderId() {
return orderId;
}
public void setOrderId(Integer orderId) {
this.orderId = orderId;
}
public String getOrderCode() {
return orderCode;
}
public void setOrderCode(String orderCode) {
this.orderCode = orderCode;
}
public String getGoodsCode() {
return goodsCode;
}
public void setGoodsCode(String goodsCode) {
this.goodsCode = goodsCode;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Float getAmount() {
return amount;
}
public void setAmount(Float amount) {
this.amount = amount;
}
public Float getFrozenAmount() {
return frozenAmount;
}
public void setFrozenAmount(Float frozenAmount) {
this.frozenAmount = frozenAmount;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
本文和数据库交互采用的是jpa的形式,需提供一个OrderRepository
public interface OrderRepository extends JpaRepository {
}
接口都放在common工程的模块中
order端暴露的服务接口如下:
public interface OrderAction {
//Seata TCC在RM端核心注解,用于声明TCC对应方法
@TwoPhaseBusinessAction(name="TccOrderAction",commitMethod = "commit" , rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "orderCode") String orderCode,
@BusinessActionContextParameter(paramName = "goodsCode") String goodsCode,
@BusinessActionContextParameter(paramName = "quantity") int quantity,
@BusinessActionContextParameter(paramName = "amount") float amount
);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
注意点:
这里我们需要对在TCC的模式下进行编码的一个说明,即一个具体的操作必须要有3个接口的支撑,即prepare,commit和rollback,命名可以自定,这样的话,框架层(seata)在执行的时候才知道并且管理分支事务的执行状态,其中BusinessActionContext 可以理解为spring中的applicationContext,一种可以携带上下文信息并且在整个环境中传递事务状态和参数的容器,通过BusinessActionContext 我们可以在commit和rollback阶段拿到prepare中传递过来的参数信息
服务实现service
@Service("orderActionImpl")
public class OrderActionImpl implements OrderAction{
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private OrderRepository orderRepository;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean prepare(BusinessActionContext actionContext, String orderCode, String goodsCode, int quantity, float amount) {
Order order = new Order();
setOrder(order,orderCode,goodsCode,quantity,amount);
orderRepository.save(order);
logger.info("orderActionImpl分支事务已就绪, xid:" + actionContext.getXid());
return true;
}
public void setOrder(Order order,String orderCode, String goodsCode, int quantity, float amount){
order.setOrderCode(orderCode);
order.setGoodsCode(goodsCode);
order.setQuantity(quantity);
order.setAmount(0f);
order.setFrozenAmount(amount);
order.setStatus(0);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean commit(BusinessActionContext actionContext) {
String orderCode = (String)actionContext.getActionContext("orderCode");
Order condition = new Order();
condition.setOrderCode(orderCode);
Example sample = Example.of(condition);
Order order = orderRepository.findOne(sample).get();
//幂等性校验
if(order.getStatus() == 1){
return true;
}
order.setAmount(order.getFrozenAmount());
order.setFrozenAmount(0f);
//幂等性,做一次与做多次结果相同
/**
* 第一次: FA: 100 -> FA:0 A: 100
* 第二次: FA: 0 -> FA: 0 A: 0 //不具备幂等性
*/
order.setStatus(1);
orderRepository.save(order);
logger.info("orderActionImpl分支事务已提交, xid:" + actionContext.getXid());
return true;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean rollback(BusinessActionContext actionContext) {
String orderCode = (String)actionContext.getActionContext("orderCode");
Order condition = new Order();
condition.setOrderCode(orderCode);
Example sample = Example.of(condition);
Order order = orderRepository.findOne(sample).get();
//幂等性校验
if(order.getStatus() == 2){
return true;
}
order.setAmount(0f);
order.setFrozenAmount(0f);
order.setStatus(2);
orderRepository.save(order);
logger.info("orderActionImpl分支事务已回滚, xid:" + actionContext.getXid());
return true;
}
}
启动类
@SpringBootApplication
@ImportResource("classpath:provider/*.xml")
public class OrderProviderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderProviderApplication.class, args);
}
}
可以对该服务实现进行单元测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {OrderProviderApplication.class})
public class OrderProviderTestor {
@Autowired
private OrderAction orderAction;
@Test
public void testPreare(){
orderAction.prepare(new BusinessActionContext(), UUID.randomUUID().toString(), "juice", 10, 30f);
}
@Test
public void testCommit(){
BusinessActionContext context = new BusinessActionContext();
Map map = new HashMap();
map.put("orderCode", UUID.randomUUID().toString());
context.setActionContext(map);
orderAction.commit(context);
}
}
order端的代码基本上就是这些,下面顺便贴出storage端的代码,基本上和order端代码类似,
appication.yml
spring:
application:
name: storage-provider
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://106.15.37.145:3306/tcc_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
jpa:
hibernate:
naming:
#开启驼峰命名转换
physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
show-sql: true
#设置数据库方言
database-platform: org.hibernate.dialect.MySQLDialect
server:
port: 8002
dubbo配置文件
服务接口
public interface StorageAction {
@TwoPhaseBusinessAction(name="TccStorageAction" ,commitMethod = "commit" , rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext context,
@BusinessActionContextParameter(paramName = "goodsCode") String goodsCode,
@BusinessActionContextParameter(paramName = "quantity") int quantity);
public boolean commit(BusinessActionContext context);
public boolean rollback(BusinessActionContext context);
}
接口实现类
@Service("storageActionImpl")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class StorageActionImpl implements StorageAction {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private StorageRepository storageRepository;
@Override
public boolean prepare(BusinessActionContext context, String goodsCode, int quantity) {
Storage condition = new Storage();
condition.setGoodsCode(goodsCode);
Example example = Example.of(condition);
Optional one = storageRepository.findOne(example);
Storage storage = null;
if(one.isPresent()){
storage = one.get();
}else{
throw new RuntimeException("[" + goodsCode + "]商品编码不存在");
}
if(quantity > storage.getQuantity()){
throw new RuntimeException("[" + goodsCode + "]库存数量不足" + quantity);
}
storage.setFrozenQuantity(quantity);
storageRepository.save(storage);
logger.info("StorageActionImpl分支事务已就绪, xid:" + context.getXid());
return true;
}
@Override
public boolean commit(BusinessActionContext context) {
String goodsCode = (String)context.getActionContext("goodsCode");
Storage condition = new Storage();
condition.setGoodsCode(goodsCode);
Storage storage = storageRepository.findOne(Example.of(condition)).get();
storage.setQuantity(storage.getQuantity() - storage.getFrozenQuantity());
storage.setFrozenQuantity(0);
storageRepository.save(storage);
logger.info("StorageActionImpl分支事务提交, xid:" + context.getXid());
return true;
}
@Override
public boolean rollback(BusinessActionContext context) {
String goodsCode = (String)context.getActionContext("goodsCode");
Storage condition = new Storage();
condition.setGoodsCode(goodsCode);
Optional one = storageRepository.findOne(Example.of(condition));
Storage storage = null;
if(!one.isPresent()){
return true;
}else{
storage = one.get();
}
storage.setFrozenQuantity(0);
storageRepository.save(storage);
logger.info("StorageActionImpl分支事务回滚, xid:" + context.getXid());
return true;
}
}
business为全局事务的发起方,可以理解为通过调用business的某接口实现全局事务的发起、提交与回滚,这里为了简单模拟演示我们就不再操作数据库
application.yml
spring:
application:
name: bussiness-consumer
server:
port: 8000
dubbo配置,这里是作为消费端进行配置
在business中,只需要调用order和storage中提供的接口即可,因此这里我们只需提供一个实现类,将order和storage的接口注入即可
@Service
public class BussinessService {
@Resource(name="storageAction")
private StorageAction storageAction;
@Resource(name="orderAction")
private OrderAction orderAction;
/**
* sale方法执行成功,TC通知RM执行confirm方法
* sale方法抛出RuntimeException,TC通知RM执行cancel方法
*/
@GlobalTransactional //开启全局TCC分布式事务
public void sale(String orderCode,String goodsCode,
int quantity,float amount){
orderAction.prepare(new BusinessActionContext(), orderCode, goodsCode, quantity, amount);
storageAction.prepare(new BusinessActionContext(), goodsCode, quantity);
if(quantity == 1000){
throw new RuntimeException("unknown exception");
}
}
}
business暴露出一个对外的接口
@RestController
public class TestController {
@Resource
private BussinessService bussinessService;
@GetMapping("/tcc1")
public String test1(){
String uuid = UUID.randomUUID().toString();
bussinessService.sale(uuid,"coke",10,30);
return "SUCCESS";
}
@GetMapping("/tcc2")
public String test2(){
String uuid = UUID.randomUUID().toString();
bussinessService.sale(uuid,"coke",10000,30000);
return "SUCCESS";
}
@GetMapping("/tcc3")
public String test3(){
String uuid = UUID.randomUUID().toString();
bussinessService.sale(uuid,"coke",100,300);
return "SUCCESS";
}
}
启动类
@SpringBootApplication
@ImportResource("classpath:consumer/*.xml")
public class BussinessConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(BussinessConsumerApplication.class, args);
}
}
下面我们将3个服务模块的工程运行起来
首先在数据库的storage表中初始化一条库存数据,即一个为juice的商品有5000件
正常测试1:
调用接口1,请求的库存数量没有超过实际的库存数量,可以执行成功,输入:http://localhost:8000/tcc1
同时数据库新增一条订单记录
异常测试2:
调用接口2,请求的库存数量超过实际的库存数量,理论上说,订单服务成功,库存扣减失败,触发全局事务回滚,最终数据库库存扣减不成功,同时新增订单记录的状态值为2取消状态,输入:http://localhost:8000/tcc2
如果想要更清楚的搞明白背后的执行原理,我们可以到控制台查看输出日志,通过这些日志我们可以清晰的了解到seata参与的过程中各个分支事务的具体运行操作的步骤
本篇的讲解到此结束,后续我们将会讲述如何使用Hmily完成TCC的整合和使用,本篇到此结束,最后感谢观看!