原创文章,禁止任何形式转载!

上篇的续集。

工具:

Idea201902/JDK11/Gradle5.6.2/Mysql8.0.11/Lombok0.27/Postman7.5.0/SpringBoot2.1.9/Nacos1.1.3/Seata0.8.1/SeataServer0.8.1/Dubbo2.7.3

难度:新手--战士--老兵--大师

目标:

1.使用Seata实现storage模块的TCC模式的本地模式

2.使用Seata实现多级TCC模式


步骤:

为了更好的遇到各种问题,同时保持时效性,我尽量使用最新的软件版本。本项目代码地址:其中的dubbo-day18-01-seata-TCC-local,https://github.com/xiexiaobiao/dubbo-project.git

文中图片有些显示不全,是图片很大,我担心缩放会看不清,所以部分显示不全的,可以下载图片再看。

1.先说下TCC(Try-Confirm-Cancel)模式:

TCC模式即将每个服务业务操作分为两个阶段,第一个阶段检查并预留相关资源,可视为一种临时操作,第二阶段根据所有服务业务的Try状态来操作,如果都成功,则进行Confirm操作,如果任意一个Try发生错误,则全部Cancel,特征在于它不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务,不同于AT的是就是需要自行定义各个阶段的逻辑,对业务有侵入。TCC使用要求就是业务接口都必须实现三段逻辑:

  • 准备操作 Try:完成所有业务检查,预留必须的业务资源。

  • 确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。

  • 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。

借用一张图来解释一下(此图来源见文末参考文章):

  • try-先扣款30元 --> confirm-空操作 --> cancel-退回30元。

如果再优化一下,将更接近TCC的定义:

  • try-冻结30元,预留资源 --> confirm-扣款30元 --> cancel-退回30元。

2.项目延续自前篇文章,不变,仅修改了storage模块,整体为多模块Dubbo微服务架构,目标一,即实现图一中TCC模式。

3.先从com.biao.mall.storage.conf.SeataAutoConfig开始:此类为配置类,完成Seata框架依赖元素的注入,

  • 先自动注入DataSourceProperties,获取JDBC信息;

  • 再注入一个GlobalTransactionScanner,用于扫描TCC事务;

@Configuration
public class SeataAutoConfig { private DataSourceProperties dataSourceProperties; @Autowired
public SeataAutoConfig(DataSourceProperties dataSourceProperties){
this.dataSourceProperties = dataSourceProperties;
} /**
* init durid datasource
* @Return: druidDataSource datasource instance
*/
@Bean
@Primary
public DruidDataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl(dataSourceProperties.getUrl());
druidDataSource.setUsername(dataSourceProperties.getUsername());
druidDataSource.setPassword(dataSourceProperties.getPassword());
druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
druidDataSource.setInitialSize(0);
druidDataSource.setMaxActive(180);
druidDataSource.setMaxWait(60000);
druidDataSource.setMinIdle(0);
druidDataSource.setValidationQuery("Select 1 from DUAL");
druidDataSource.setTestOnBorrow(false);
druidDataSource.setTestOnReturn(false);
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(25200000);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(1800);
druidDataSource.setLogAbandoned(true);
return druidDataSource;
} /**
* init datasource proxy
* @Param: druidDataSource datasource bean instance
* @Return: DataSourceProxy datasource proxy
*/
@Bean
public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
} /**
* init mybatis sqlSessionFactory
* @Param: dataSourceProxy datasource proxy
* @Return: DataSourceProxy datasource proxy
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSourceProxy);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/mapper/*Mapper.xml"));
factoryBean.setTransactionFactory(new JdbcTransactionFactory());
return factoryBean.getObject();
} /**
* init global transaction scanner
* @Return: GlobalTransactionScanner
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(){
return new GlobalTransactionScanner("${spring.application.name}", "my_test_tx_group");
}
}

4.核心之一com.biao.mall.storage.service.ProductService接口:

  • @LocalTCC,注解标识此TCC为本地模式:即该事务是本地调用,非RPC调用;

  • @TwoPhaseBusinessAction,注解标识为TCC模式,其中定义了commitMethod 和rollbackMethod

@LocalTCC
public interface ProductService extends IService<ProductEntity> {
/**
* 扣减库存
*/
// ObjectResponse decreaseStorage(CommodityDTO commodityDTO); /** TCC 模式 */
@TwoPhaseBusinessAction(name = "StorageAction",commitMethod = "commit",rollbackMethod = "rollback")
boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "commodityDTO") CommodityDTO commodityDTO); boolean commit(BusinessActionContext actionContext); boolean rollback(BusinessActionContext actionContext);
}

5.com.biao.mall.storage.impl.ProductServiceImpl,上面接口的实现类:try阶段执行业务逻辑,commit阶段空操作,rollback执行回退,看官当然可以实现一个优化的方案:try阶段执行数量冻结逻辑,commit阶段真实提交操作,rollback执行回退;

@Service
public class ProductServiceImpl extends ServiceImpl<ProductDao, ProductEntity> implements ProductService { @Override
public boolean prepare(BusinessActionContext actionContext, CommodityDTO commodityDTO) {
System.out.println("actionContext获取Xid prepare>>> "+actionContext.getXid());
System.out.println("actionContext获取TCC参数 prepare>>> "+actionContext.getActionContext("commodityDTO"));
int storage = baseMapper.decreaseStorage(commodityDTO.getCommodityCode(), commodityDTO.getCount());
//测试rollback时打开
/*int a = 1/0;
System.out.println(a);*/
if (storage > 0){
return true;
}
return false;
} @Override
public boolean commit(BusinessActionContext actionContext) {
System.out.println("actionContext获取Xid commit>>> "+actionContext.getXid());
return true;
} @Override
public boolean rollback(BusinessActionContext actionContext) {
System.out.println("actionContext获取Xid rollback>>> "+actionContext.getXid());
//必须注意actionContext.getActionContext返回的是Object,且不可使用以下语句直接强转!
//CommodityDTO commodityDTO = (CommodityDTO) actionContext.getActionContext("commodityDTO");
CommodityDTO commodityDTO = JSONObject.toJavaObject((JSONObject)actionContext.getActionContext("commodityDTO"),CommodityDTO.class);
int storage = baseMapper.increaseStorage(commodityDTO.getCommodityCode(), commodityDTO.getCount());
if (storage > 0){
return true;
}
return false;
}
}

对于以上代码,rollback方法中,必须注意actionContext.getActionContext返回的是Object,且不可使用以下语句直接强转,运行会报错!

CommodityDTO commodityDTO = (CommodityDTO)actionContext.getActionContext("commodityDTO");

6.com.biao.mall.storage.controller.ProductController,写个简单的API入口:

@RestController
@RequestMapping("/product")
public class ProductController { @Autowired
private ProductService productService; private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); @PostMapping("/dec")
@GlobalTransactional
public String handleBusiness(@RequestBody CommodityDTO commodityDTO) {
LOGGER.info("请求参数:{}", commodityDTO.toString());
LOGGER.info("全局XID:{}", RootContext.getXID());
ObjectResponse<Object> response = new ObjectResponse<>(); if (productService.prepare(null,commodityDTO)){
return "success";
};
return null;
}
}

7.成功Commit测试:依次启动:Nacos-->SeataServer-->storage

Postman提交数据:

后台输出,注意数字标识:

  • 获得请求参数对象DTO(1)

  • Seata开启全局事务(9)

  • Try阶段处理(2,3)

  • 标识事务模式为TCC(4)

  • Commit阶段处理(5,6)

  • 第二阶段成功(7),全局事务成功(8)

8.rollback测试:

打开com.biao.mall.storage.impl.ProductServiceImpl中prepare方法中的异常测试代码,再次运行:

  • Seata开启全局事务(1)

  • Rollback阶段处理(2)

  • 第二阶段回退成功(3),全局事务成功(4)

目标一达成!

9.目标二,目前暂时没办法在该项目框架下实现,因基于@Reference注解获取的Dubbo服务,在Seata框架下,分支事务中BusinessActionContext实例一直是Null,折腾一整天,发现这是Seata的一个未关闭的Issue,作罢,以后再写!替代方案是基于XML方式,注册和获取Dubbo服务,再使用Spring模式的ApplicationContext获取服务Bean,是可以正常使用的,但我觉得这套方案不是未来的方向,过于原始,故放弃了,看官可以尝试!


复盘记:

1.SeataServer,即TC,其安装目录下文件 \seata\bin\sessionStore\root.data会持久化事务执行状态,经测试,如果提交阶段失败,即将storage/src/main/java/com/biao/mall/storage/impl/ProductServiceImpl.java中commit空提交改为返回false,发生错误重启TC或应用,会自动继续尝试commit提交,再重启应用,RM注册失败,重启SeataServer,也会自动继续commit,见下图,说明有进行文件形式的持久化机制!为啥?因为对于二阶段式提交,只要try成功,commit是必须要成功的(或者try成功后,rollback一定要成功),如不这样,数据从理论上就是处于半状态,这系列动作本来就是一个事务,故由TC来保证此原则的执行。要强行恢复正常,手动删掉root.data即可!

2.后台的红色告警:

使用依赖树分析:

更新依赖即可消除,神奇的是org.apache.dubbo:dubbo:2.7.3中,我测试3.24,3.25,3.26都会报警告,只有3.23.1-GA刚刚好,这口味真独特,过老过嫩都不要!

compile group: 'org.javassist', name: 'javassist', version: '3.23.1-GA'

3.事实上com.biao.mall.storage.conf.SeataAutoConfig中:可以将DataSourceProxy不进行DI,执行效果如下图,可以看到sql的过程了,因为此时是由Spring来操作的,DataSourceProxy配置是AT模式必须的,因为要由Seata来代理完成数据操作,这也是TCC和AT模式的一大区别!

4.参考文章:https://juejin.im/post/5cbfd9a26fb9a03212505785

原创文章,禁止任何形式转载!否则追究法律责任!!

====================

以下为更新部分(2020/02/14):

本项目代码地址:其中的 dubbo-day18-seata-TCC-global ,https://github.com/xiexiaobiao/dubbo-project.git

我看了seata官方说明,截至目前,对@reference引用issue有待解决:

在开启 GlobalTransactional 的服务中使用 Reference 引用了 TCC 服务,但是 TCC 服务一阶段方法中的 BusinessActionContext 始终为
null,也始终收不到二阶段提交的消息。跟踪发现 wrapIfNecessary 遍历的 Bean 中并没有该服务。需要在 Configuration 中使用
ReferenceBean 显式引用 dubbo 服务,然后在 GlobalTransactional 的方法中使用 Autowired 引入的 TCC 服务,才可以正常使用。

比如我需要在business模块使用dubbo服务,先显式注入:

com.biao.mall.business.conf.ReferenceConfig

@Configuration
public class ReferenceConfig {
@Bean
public ReferenceBean<StorageDubboService> dubboServiceReferenceBean(){
ReferenceBean<StorageDubboService> dubboStorageServiceBean = new ReferenceBean<>();
dubboStorageServiceBean.setVersion("1.0.0");
dubboStorageServiceBean.setInterface(StorageDubboService.class);
dubboStorageServiceBean.setTimeout(5000);
dubboStorageServiceBean.setCheck(false);
return dubboStorageServiceBean;
} @Bean
public ReferenceBean<OrderDubboService> dubboOrderReferenceBean(){
ReferenceBean<OrderDubboService> dubboOrderServiceBean = new ReferenceBean<>();
dubboOrderServiceBean.setVersion("1.0.0");
dubboOrderServiceBean.setInterface(OrderDubboService.class);
dubboOrderServiceBean.setTimeout(5000);
dubboOrderServiceBean.setCheck(false);
return dubboOrderServiceBean;
}
}

然后在使用的地方,使用 Autowired 引入,就可以取到 BusinessActionContext :

com.biao.mall.business.service.BusinessServiceImpl

@Service
public class BusinessServiceImpl implements BusinessService { @Autowired
private StorageDubboService storageDubboService; @Autowired
private OrderDubboService orderDubboService;
....
....
}

测试数据:

POST http://localhost:8084/buy
Content-Type: application/json { "userId":"1", "commodityCode":"1", "name":"book", "count":1, "amount":12.56 }

结论:
TCC 目前只能适用 A --> B,C 分支情形,对 A --> B,C 且 B -- > D的情形暂不可使用!
换句话说,即本例中business-->storage,order可以,但business-->storage,order且order-->account,报错。

A --> B --> C 类型的长事务,可以使用seata的saga模式。


我的其他文章:

微信公众号,只写原创文章!

Dubbo学习系列之十五(Seata分布式事务方案TCC模式)的更多相关文章

  1. Dubbo学习系列之十四(Seata分布式事务方案AT模式)

    一直说写有关最新技术的文章,但前面似乎都有点偏了,只能说算主流技术,今天这个主题,我觉得应该名副其实.分布式微服务的深水区并不是单个微服务的设计,而是服务间的数据一致性问题!解决了这个问题,才算是把分 ...

  2. Dubbo学习系列之十六(ELK海量日志分析框架)

    外卖公司如何匹配骑手和订单?淘宝如何进行商品推荐?或者读者兴趣匹配?还有海量数据存储搜索.实时日志分析.应用程序监控等场景,Elasticsearch或许可以提供一些思路,作为业界最具影响力的海量搜索 ...

  3. Dubbo学习系列之十二(Quartz任务调度)

    Quartz词义为"石英"水晶,然后聪明的人类利用它发明了石英手表,因石英晶体在受到电流影响时,它会产生规律的振动,于是,这种时间上的规律,也被应用到了软件界,来命名了一款任务调度 ...

  4. Dubbo学习系列之十(Sentinel之限流与降级)

    各位看官,先提个问题,如果让你设计一套秒杀系统,核心要点是啥???我认为有三点:缓存.限流和分离.想当年12306大面积崩溃,还有如今的微博整体宕机情况,感觉就是限流降级没做好,"用有限的资 ...

  5. Dubbo学习系列之十八(Skywalking服务跟踪)

    我们知道,微服务不是独立的存在,否则就不需要微服务这个架构了,那么当发起一次请求,如何知道这次请求的轨迹,或者说遇到响应缓慢. 请求出错的情况,我们该如何定位呢?这就涉及到APM(Applicatio ...

  6. WP8.1学习系列(第二十五章)——控件样式

      XAML 框架提供许多自定义应用外观的方法.通过样式可以设置控件属性,并重复使用这些设置,以便保持多个控件具有一致的外观. 路线图: 本主题与其他主题有何关联?请参阅: 使用 C# 或 Visua ...

  7. WP8.1学习系列(第十六章)——交互UX之命令模式

    命令模式   在本文中 命令类型 命令放置 相关主题 你可以在应用商店应用的几个曲面中放置命令和控件,包括应用画布.弹出窗口.对话框和应用栏.在正确的时间选择合适的曲面可能就是易于使用的应用和很难使用 ...

  8. Dubbo学习系列之十三(Mycat数据库代理)

    软件界有只猫,不用我说,各位看官肯定知道是哪只,那就是大名鼎鼎的Tomcat,现在又来了一只猫,据说是位东方萌妹子,暂且认作Tom猫的表妹,本来叫OpencloudDB,后又改名为Mycat,或许Ca ...

  9. Dubbo学习系列之十一(Dashboard+Nacos规则推送)

    中国武术,门派林立,都是号称多少代的XXX传人,结果在面对现代武术时,经常被KO秒杀,为啥,光靠宣传和口号撑门面,终究是靠不住,必须得有真货 ,得经得住考验,所以不能只说Sentinel有多好,也得给 ...

随机推荐

  1. 单细胞转录组测序技术(scRNA-seq)及细胞分离技术分类汇总

    单细胞测序流程(http://learn.gencore.bio.nyu.edu) 在过去的十多年里,高通量测序技术被广泛应用于生物和医学的各种领域,极大促进了相关的研究和应用.其中转录组测序(RNA ...

  2. SpringBoot系列__01HelloWorld

    接触SpringBoot很久了,但是一直没有很深入的研究一下源码,最近重启了博客,顺便开始深入研究一下技术. 1.简介 参照官方文档的说法,SpringBoot的设计理念就是为了简化Java程序员搭建 ...

  3. Go语言基础之网络编程

    现在我们几乎每天都在使用互联网,我们前面已经学习了如何编写Go语言程序,但是如何才能让我们的程序通过网络互相通信呢?本章我们就一起来学习下Go语言中的网络编程. 关于网络编程其实是一个很庞大的领域,本 ...

  4. 每天学会一点点(JAVA基础)

    1.什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”? 虚拟机是一个可以执行Java字节码的虚拟机进程.Java源文件被编译成能被Java虚拟机执行的字节码文件. Java被设计成允 ...

  5. 集合数组与String的互转

    1.集合转成数组: 转之前集合里面存的什么类型的数据,就new什么类(特别:存的是基本数据的封装类,就要new他的封装类) 例如: 1.1集合: ArrayList<Character> ...

  6. 2019-2020-1 20199322《Linux内核原理与分析》第一周作业

    图解sudo deluser name和sudo deluser name --remove -home的区别? 先众所周知地创建一个用户“hanmeimei” 然后给韩梅梅创建一个二级的目录,并且在 ...

  7. 为什么StringBuilder是线程不安全的?StringBuffer是线程安全的?

    面试中经常问到的一个问题:StringBuilder和StringBuffer的区别是什么? 我们非常自信的说出:StringBuilder是线程安全的,StirngBuffer是线程不安全的 面试官 ...

  8. Vuex,从入门到...

    Vuex 是什么? 官方是这么说的:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. 不懂? ...

  9. Flink 从 0 到 1 学习 —— 如何自定义 Data Sink ?

    前言 前篇文章 <从0到1学习Flink>-- Data Sink 介绍 介绍了 Flink Data Sink,也介绍了 Flink 自带的 Sink,那么如何自定义自己的 Sink 呢 ...

  10. charles 黑名单

    本文参考:charles 黑名单 charles 黑名单 功能:阻止对匹配HOST的请求:可以直接把请求丢掉,也可以直接返回403状态码: 我一般用黑名单工具来block一些软件的自动上传功能 黑名单 ...