搜索 dubbo教程-10-分布式事务框架tcc-transaction

写在前面

欢迎大家收看御风大世界
本次课是我们dubbo系列教程的第10课
我们上一次讲到了
dubbo的作者 对于dubbo实现分布式事务的一些观点
他自己本人 不主张在 dubbo 这个框架本身 去实现分布式事务
而是 dubbo 和 任何 业务框架一样
都可以被事务管理器 事务切面 甚至是 事务框架集成
因为 事务 不是 dubbo 该考虑的事情
同事 dubbo 开发者 们
也在官网建议 大家 竟可能的 把 一个 服务的范围扩大
通过 模块 功能设计 来 回避 分布式事务的问题

但是我自己在网上搜索 dubbo 分布式事务的时候

我发现网上还是有不少小伙伴 喜欢这个课题的
并且做出了自己的一些研究
首先 我们说一下
我们 不缺乏 那些 实现了 分布式事务的框架
同时 在springboot 当中 也是有分布式事务的集成的
我们缺的是 有人 在 dubbo 和 分布式事务框架之间
架起一道桥梁
让他们能够连接起来

而这个时候我就去了github
还真的在 github上找到了
imagepng

因为我很喜欢这个项目
所以我想深入研究下
然后我就把我的一些提交和修改 push到了我的地址上 (我 fork 了一下这个项目)
我的地址在这里
我具体的一些提交还有后续的一些学习笔记和注释什么的都会提交到这个地方
https://github.com/ibywind/tcc-transaction

我们这次课就要来给大家演示 这个框架是如何工作的
以及我自己在动手的过程中
有哪些收获
在这里 我给大家分享

分布式事务

数据库事务

在说分布式事务之前,我们先从数据库事务说起。 数据库事务可能大家都很熟悉,在开发过程中也会经常使用到。但是即使如此,可能对于一些细节问题,很多人仍然不清楚。比如很多人都知道数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily),简称就是ACID。但是再往下比如问到隔离性指的是什么的时候可能就不知道了,或者是知道隔离性是什么但是再问到数据库实现隔离的都有哪些级别,或者是每个级别他们有什么区别的时候可能就不知道了。

InnoDB 实现原理

InnoDB 是 MySQL 的一个存储引擎,大部分人对 MySQL 都比较熟悉,这里简单介绍一下数据库事务实现的一些基本原理。

在本地事务中,服务和资源在事务的包裹下可以看做是一体的,如下图:

我们的本地事务由资源管理器进行管理:

而事务的 ACID 是通过 InnoDB 日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过 Redo Log(重做日志)来实现,原子性和一致性通过 Undo Log 来实现。

Undo Log 的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为 Undo Log)。然后进行数据的修改。

如果出现了错误或者用户执行了 Rollback 语句,系统可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。

和 Undo Log 相反,Redo Log 记录的是新数据的备份。在事务提交前,只要将 Redo Log 持久化即可,不需要将数据持久化。

当系统崩溃时,虽然数据没有持久化,但是 Redo Log 已经持久化。系统可以根据 Redo Log 的内容,将所有数据恢复到最新的状态。对具体实现过程有兴趣的同学可以去自行搜索扩展。

接着,我们就说一下分布式事务。

分布式理论

当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,这里所说的分区指的是物理分区,分区之后可能不同的库就处于不同的服务器上了,这个时候单个数据库的ACID已经不能适应这种情况了,而在这种ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则或者叫CAP定理,那么CAP定理指的是什么呢?

CAP定理

imagepng

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

  • 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
  • 可用性(Availability) : 每个操作都必须以可预期的响应结束
  • 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成

具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。

分区容错

先看 Partition tolerance,中文叫做"分区容错"。

大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。

一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。

一致性

Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

接下来,用户的读操作就会得到 v1。这就叫一致性。

问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。

为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。

这样的话,用户向 G2 发起读操作,也能得到 v1。

可用性

Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应。

用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。

Consistency 和 Availability 的矛盾

一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。

如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性不。

如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。

综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。

BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

有了以上理论之后,我们来看一下分布式事务的问题。

SOA分布式事务解决方案

首先我们来看一个场景
下图是一个比较经典的电商下单扣款的场景
我们用户付款的这个过程
可以分为三个步骤,三个子系统参与其中
- 余额
- 积分
- 优惠券
而这三个子系统自己也有自己的数据库(分库分表)

如何解决这样的分布式事务问题呢 ?
我这里准备了一些 当下的解决方案

基于XA协议的两阶段提交方案

交易中间件与数据库通过 XA 接口规范,使用两阶段提交来完成一个全局事务, XA 规范的基础是两阶段提交协议。
第一阶段是表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者;第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚。

两阶段提交方案应用非常广泛,几乎所有商业OLTP数据库都支持XA协议。但是两阶段提交方案锁定资源时间长,对性能影响很大,基本不适合解决微服务事务问题。

TCC方案

TCC方案在电商、金融领域落地较多。TCC方案其实是两阶段提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。基本原理如下图所示。

事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。

TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 当然TCC方案也有不足之处,集中表现在以下两个方面:

  • 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
  • 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。

上述原因导致TCC方案大多被研发实力较强、有迫切需求的大公司所采用。微服务倡导服务的轻量化、易部署,而TCC方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大。

基于消息的最终一致性方案

消息一致性方案是通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。

消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本较高。

正常流程
imagepng

异常流程 (回滚)
imagepng

上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。
那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制
imagepng

业务补偿与人工订正

多熟中小企业靠业务补偿与人工订正解决。缺点是运维、支持投入人力大,优点是简单直接,逻辑不复杂。在业务量不大的情况下能hold住,但业务扩大了就很难应付。


我们本次课尝试使用一个开源的TCC实现方案
我们在github找到的

免费开源tcc-transaction

他是我在github上面找到的一个中间件
JAVA实现的
地址在这里 : https://github.com/changmingxie/tcc-transaction

我们把它下载到本地
然后我们看下他具体的一些细节吧
imagepng

导入到我们的IDEA 然后看下 他具体的一些代码设计

imagepng

首先他是一个 MAVEN 聚合项目
一共有这样几个 子模块

<modules>
	  <module>tcc-transaction-core</module>
	  <module>tcc-transaction-api</module>
	  <module>tcc-transaction-spring</module>
	  <module>tcc-transaction-unit-test</module>
	  <module>tcc-transaction-tutorial-sample</module>
	  <module>tcc-transaction-server</module>
	  <module>tcc-transaction-dubbo</module>
</modules>
  • tcc-transaction-core 核心代码实现
  • tcc-transaction-api 模型定义 接口定义
  • tcc-transaction-spring spirng框架集成桥接
  • tcc-transaction-unit-test 单元测试
  • tcc-transaction-tutorial-sample 实例程序 helloworld (里面几个示例)
  • tcc-transaction-server web程序 对 事务对象的管理台
  • tcc-transaction-dubbo dubbo框架的集成

tcc-transaction-tutorial-sample 内有乾坤

我们的 tcc-transaction-tutorial-sample 项目是一个 maven聚合项目
打开一下 内有乾坤哦
imagepng

我们继续点开 tcc-transaction-dubbo-sample 发现里面 也是一个 maven聚合项目
我们就来测试他

imagepng

测试 tcc-transaction-dubbo-sample

他讲述了一个 下单之后 两种支付方式 合并支付扣款的 过程
在现实生活中 很受用哦

分布式事务逻辑

作者这样说

在运行sample前,需搭建好db环境,运行dbscripts目录下的create_db.sql建立数据库实例及表;还需修改各种项目中jdbc.properties文件中的jdbc连接信息。

如有问题可以在本项目的github issues中提问。或是加微信:changmingxie,为便于识别,麻烦在备注中写下:名字+所在公司名字+是否线上使用,作者尽量回答疑问。

我们需要修改下我们的配置项 并且 运行 SQL
首先我们需要修改 配置文件 并且 建立数据库 TCC

imagepng

然后我们找到 作者准备的 SQL文件 执行生成测试数据

imagepng

imagepng

我们运行完了以后
我们生成了 四个数据库
imagepng

都是业务数据 红包 资产 订单 等等

我们打开TCC会出现以下几张表
imagepng

是 作者 设计用来 控制分布式事务的表
并且 作者 提倡 把他们 和 业务数据库 分开来
避免相互影响
这个大家注意下

开始运行

整体 mvn clean install 一下
然后我们开始部署

来到我们需要测试的项目
打开它
imagepng

  • tcc-transaction-dubbo-capital 这是个 war 项目
  • tcc-transaction-dubbo-order 这是个 war 项目
  • tcc-transaction-dubbo-redpacket 这个是war项目

我们现在要测试的是

不同项目 不同数据库 dubbo RPC 调用下的 分布式事务

我们用tomcat启动试下

imagepng

因为 一个tomcat 运行多个项目 需要 设置不同的路径 否则会 说你端口占用 无法启动
imagepng

启动ZK
imagepng

我们启动tomcat
毫无意外的
项目报错了
因为 作者 的 SQL还有一些字段没有完成
如果大家想跳过这个 地方
可以用我这个脚本 执行下 , 执行完后 你就可以开始测试了,数据初始化已经完成!!!

drop DATABASE if exists  tcc_cap;
drop DATABASE if exists  tcc_ord;
drop DATABASE if exists  tcc_red;
drop DATABASE if exists  tcc;

CREATE DATABASE `tcc_cap` /*!40100 DEFAULT CHARACTER SET utf8 */;

use tcc_cap;
CREATE TABLE `CAP_CAPITAL_ACCOUNT` (
  `CAPITAL_ACCOUNT_ID` int(11) NOT NULL AUTO_INCREMENT,
  `BALANCE_AMOUNT` decimal(10,0) DEFAULT NULL,
  `USER_ID` int(11) DEFAULT NULL,
  PRIMARY KEY (`CAPITAL_ACCOUNT_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

CREATE TABLE `CAP_TRADE_ORDER` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `SELF_USER_ID` bigint(11) DEFAULT NULL,
  `OPPOSITE_USER_ID` bigint(11) DEFAULT NULL,
  `MERCHANT_ORDER_NO` varchar(45) DEFAULT NULL,
  `AMOUNT` decimal(10,0) DEFAULT NULL,
  `STATUS` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

ALTER TABLE `CAP_TRADE_ORDER`
ADD COLUMN `VERSION`  int(11) NULL AFTER `STATUS`;

INSERT INTO `CAP_CAPITAL_ACCOUNT`(CAPITAL_ACCOUNT_ID, BALANCE_AMOUNT, USER_ID) VALUE (1,10000,1000);
INSERT INTO `CAP_CAPITAL_ACCOUNT`(CAPITAL_ACCOUNT_ID, BALANCE_AMOUNT, USER_ID) VALUE (2,10000,2000);


-- 

CREATE DATABASE `tcc_ord` /*!40100 DEFAULT CHARACTER SET utf8 */;
use tcc_ord;
CREATE TABLE `ORD_ORDER` (
  `ORDER_ID` int(11) NOT NULL AUTO_INCREMENT,
  `PAYER_USER_ID` int(11) DEFAULT NULL,
  `PAYEE_USER_ID` int(11) DEFAULT NULL,
  `RED_PACKET_PAY_AMOUNT` decimal(10,0) DEFAULT NULL,
  `CAPITAL_PAY_AMOUNT` decimal(10,0) DEFAULT NULL,
  `STATUS` varchar(45) DEFAULT NULL,
  `MERCHANT_ORDER_NO` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`ORDER_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `ORD_ORDER_LINE` (
  `ORDER_LINE_ID` int(11) NOT NULL AUTO_INCREMENT,
  `PRODUCT_ID` int(11) DEFAULT NULL,
  `QUANTITY` decimal(10,0) DEFAULT NULL,
  `UNIT_PRICE` decimal(10,0) DEFAULT NULL,
  PRIMARY KEY (`ORDER_LINE_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `ORD_SHOP` (
  `SHOP_ID` int(11) NOT NULL,
  `OWNER_USER_ID` int(11) DEFAULT NULL,
  PRIMARY KEY (`SHOP_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `ORD_PRODUCT`(
  `PRODUCT_ID` int(11) NOT NULL,
  `SHOP_ID` int(11) NOT NULL,
  `PRODUCT_NAME` VARCHAR(64) DEFAULT NULL ,
  `PRICE` DECIMAL(10,0) DEFAULT NULL,
  PRIMARY KEY (`PRODUCT_ID`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

ALTER TABLE `ord_order`
ADD COLUMN `VERSION`  int(11) NULL AFTER `MERCHANT_ORDER_NO`;

INSERT INTO `ORD_SHOP` (`SHOP_ID`,`OWNER_USER_ID`) VALUES (1,1000);

INSERT INTO `ORD_PRODUCT` (`PRODUCT_ID`,`SHOP_ID`,`PRODUCT_NAME`,`PRICE`) VALUES (1,1,'IPhone6S',5288);
INSERT INTO `ORD_PRODUCT` (`PRODUCT_ID`,`SHOP_ID`,`PRODUCT_NAME`,`PRICE`) VALUES (2,1,'MAC Pro',10288);
INSERT INTO `ORD_PRODUCT` (`PRODUCT_ID`,`SHOP_ID`,`PRODUCT_NAME`,`PRICE`) VALUES (3,1,'IWatch',2288);

-- 
CREATE DATABASE `tcc_red` /*!40100 DEFAULT CHARACTER SET utf8 */;
use tcc_red;
CREATE TABLE `RED_RED_PACKET_ACCOUNT` (
  `RED_PACKET_ACCOUNT_ID` int(11) NOT NULL,
  `BALANCE_AMOUNT` decimal(10,0) DEFAULT NULL,
  `USER_ID` int(11) DEFAULT NULL,
  PRIMARY KEY (`RED_PACKET_ACCOUNT_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `RED_TRADE_ORDER` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `SELF_USER_ID` BIGINT(11) DEFAULT NULL,
  `OPPOSITE_USER_ID` BIGINT(11) DEFAULT NULL,
  `MERCHANT_ORDER_NO` varchar(45) DEFAULT NULL,
  `AMOUNT` decimal(10,0) DEFAULT NULL,
  `STATUS` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

ALTER TABLE `RED_TRADE_ORDER`
ADD COLUMN `VERSION`  int(11) NULL AFTER `STATUS`;

INSERT INTO `RED_RED_PACKET_ACCOUNT` (`RED_PACKET_ACCOUNT_ID`,`BALANCE_AMOUNT`,`USER_ID`) VALUES (1,950,1000);
INSERT INTO `RED_RED_PACKET_ACCOUNT` (`RED_PACKET_ACCOUNT_ID`,`BALANCE_AMOUNT`,`USER_ID`) VALUES (2,500,2000);

-- 
CREATE DATABASE `TCC` /*!40100 DEFAULT CHARACTER SET utf8 */;
use tcc;
CREATE TABLE `TCC_TRANSACTION_CAP` (
  `TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,
  `DOMAIN` varchar(100) DEFAULT NULL,
  `GLOBAL_TX_ID` varbinary(32) NOT NULL,
  `BRANCH_QUALIFIER` varbinary(32) NOT NULL,
  `CONTENT` varbinary(8000) DEFAULT NULL,
  `STATUS` int(11) DEFAULT NULL,
  `TRANSACTION_TYPE` int(11) DEFAULT NULL,
  `RETRIED_COUNT` int(11) DEFAULT NULL,
  `CREATE_TIME` datetime DEFAULT NULL,
  `LAST_UPDATE_TIME` datetime DEFAULT NULL,
  `VERSION` int(11) DEFAULT NULL,
  PRIMARY KEY (`TRANSACTION_ID`),
  UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `TCC_TRANSACTION_ORD` (
  `TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,
  `DOMAIN` varchar(100) DEFAULT NULL,
  `GLOBAL_TX_ID` varbinary(32) NOT NULL,
  `BRANCH_QUALIFIER` varbinary(32) NOT NULL,
  `CONTENT` varbinary(8000) DEFAULT NULL,
  `STATUS` int(11) DEFAULT NULL,
  `TRANSACTION_TYPE` int(11) DEFAULT NULL,
  `RETRIED_COUNT` int(11) DEFAULT NULL,
  `CREATE_TIME` datetime DEFAULT NULL,
  `LAST_UPDATE_TIME` datetime DEFAULT NULL,
  `VERSION` int(11) DEFAULT NULL,
  PRIMARY KEY (`TRANSACTION_ID`),
  UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `TCC_TRANSACTION_RED` (
  `TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,
  `DOMAIN` varchar(100) DEFAULT NULL,
  `GLOBAL_TX_ID` varbinary(32) NOT NULL,
  `BRANCH_QUALIFIER` varbinary(32) NOT NULL,
  `CONTENT` varbinary(8000) DEFAULT NULL,
  `STATUS` int(11) DEFAULT NULL,
  `TRANSACTION_TYPE` int(11) DEFAULT NULL,
  `RETRIED_COUNT` int(11) DEFAULT NULL,
  `CREATE_TIME` datetime DEFAULT NULL,
  `LAST_UPDATE_TIME` datetime DEFAULT NULL,
  `VERSION` int(11) DEFAULT NULL,
  PRIMARY KEY (`TRANSACTION_ID`),
  UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `TCC_TRANSACTION_UT` (
  `TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,
  `DOMAIN` varchar(100) DEFAULT NULL,
  `GLOBAL_TX_ID` varbinary(32) NOT NULL,
  `BRANCH_QUALIFIER` varbinary(32) NOT NULL,
  `CONTENT` varbinary(8000) DEFAULT NULL,
  `STATUS` int(11) DEFAULT NULL,
  `TRANSACTION_TYPE` int(11) DEFAULT NULL,
  `RETRIED_COUNT` int(11) DEFAULT NULL,
  `CREATE_TIME` datetime DEFAULT NULL,
  `LAST_UPDATE_TIME` datetime DEFAULT NULL,
  `VERSION` int(11) DEFAULT NULL,
  PRIMARY KEY (`TRANSACTION_ID`),
  UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


ALTER TABLE `TCC_TRANSACTION_RED`
ADD COLUMN `IS_DELETE`  int(11) NULL AFTER `LAST_UPDATE_TIME`;

ALTER TABLE `TCC_TRANSACTION_UT`
ADD COLUMN `IS_DELETE`  int(11) NULL AFTER `LAST_UPDATE_TIME`;


ALTER TABLE `TCC_TRANSACTION_ORD`
ADD COLUMN `IS_DELETE`  int(11) NULL AFTER `LAST_UPDATE_TIME`;

ALTER TABLE `TCC_TRANSACTION_CAP`
ADD COLUMN `IS_DELETE`  int(11) NULL AFTER `LAST_UPDATE_TIME`;



启动效果

后台没有报错
imagepng

页面可以访问
imagepng

太好了 接下来我们就操作吧

imagepng

需要购买了
红包 和 余额两种方式 组合支付
imagepng

我们看下结果
imagepng

这个地方会报错 以为 我们加了路径 所以 需要到这个方法 修改下路径
修改完以后我们就OK了
imagepng

很显然现在是成功的
余额扣款 红包扣款成功了
整个过程也是成功的

那么现在 我们 模拟下 红包系统失败的情况

我们再次 运行 上面 SQL 脚本 让数据库还原

我们来到这个类
imagepng

查看他的远程调用
我们找到 现金支付的 业务代码
然后给他制造一个异常

imagepng

我么再次启动项目 看下效果

imagepng

我们会发现 现金金额和 红包金额都回滚了
然后我们看下控制台

imagepng

我们刚才的人为错误出现了
后续我还测试了 两个都出错的情况 他都是可以 去实现 分布式事务
到这里我们的一次尝试之旅就 结束了

数据库中 :
imagepng

其他表中的数据 和 原始数据一致
只有 这个订单中 会有一订单记录 并且 状态是 payfaild

实现细节

package org.mengyun.tcctransaction.sample.dubbo.capital.service;

import org.apache.commons.lang3.time.DateFormatUtils;
import org.mengyun.tcctransaction.api.Compensable;
import org.mengyun.tcctransaction.dubbo.context.DubboTransactionContextEditor;
import org.mengyun.tcctransaction.sample.capital.domain.entity.CapitalAccount;
import org.mengyun.tcctransaction.sample.capital.domain.entity.TradeOrder;
import org.mengyun.tcctransaction.sample.capital.domain.repository.CapitalAccountRepository;
import org.mengyun.tcctransaction.sample.capital.domain.repository.TradeOrderRepository;
import org.mengyun.tcctransaction.sample.dubbo.capital.api.CapitalTradeOrderService;
import org.mengyun.tcctransaction.sample.dubbo.capital.api.dto.CapitalTradeOrderDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Calendar;

/**
 * Created by changming.xie on 4/2/16.
 */
@Service("capitalTradeOrderService")
public class CapitalTradeOrderServiceImpl implements CapitalTradeOrderService {

    @Autowired
    CapitalAccountRepository capitalAccountRepository;

    @Autowired
    TradeOrderRepository tradeOrderRepository;

    @Override
    @Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = DubboTransactionContextEditor.class)
    @Transactional
    public String record(CapitalTradeOrderDto tradeOrderDto) {

        int i = 10/0;

        try {
            Thread.sleep(1000l);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("capital try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));


        TradeOrder foundTradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());


        //check if trade order has been recorded, if yes, return success directly.
        if (foundTradeOrder == null) {

            TradeOrder tradeOrder = new TradeOrder(
                    tradeOrderDto.getSelfUserId(),
                    tradeOrderDto.getOppositeUserId(),
                    tradeOrderDto.getMerchantOrderNo(),
                    tradeOrderDto.getAmount()
            );

            try {
                tradeOrderRepository.insert(tradeOrder);

                CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

                transferFromAccount.transferFrom(tradeOrderDto.getAmount());

                capitalAccountRepository.save(transferFromAccount);

            } catch (DataIntegrityViolationException e) {
                //this exception may happen when insert trade order concurrently, if happened, ignore this insert operation.
            }
        }

        return "success";
    }

    @Transactional
    public void confirmRecord(CapitalTradeOrderDto tradeOrderDto) {
        try {
            Thread.sleep(1000l);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("capital confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

        TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

        //check if the trade order status is DRAFT, if yes, return directly, ensure idempotency.
        if (tradeOrder != null && tradeOrder.getStatus().equals("DRAFT")) {
            tradeOrder.confirm();
            tradeOrderRepository.update(tradeOrder);

            CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());

            transferToAccount.transferTo(tradeOrderDto.getAmount());

            capitalAccountRepository.save(transferToAccount);
        }
    }

    @Transactional
    public void cancelRecord(CapitalTradeOrderDto tradeOrderDto) {
        try {
            Thread.sleep(1000l);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("capital cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

        TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

        //check if the trade order status is DRAFT, if yes, return directly, ensure idempotency.
        if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
            tradeOrder.cancel();
            tradeOrderRepository.update(tradeOrder);

            CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

            capitalAccount.cancelTransfer(tradeOrderDto.getAmount());

            capitalAccountRepository.save(capitalAccount);
        }
    }
}

我们会发现 一个 实际的业务方法 会伴随两个 切面方法

  • cancel 在失败的时候 我去给你回滚
  • confirm 在成功的时候 我去给你实际执行

我这里有一个疑问
确定大家 都成功 和 一方失败的 地方在 哪里 ?
这个问题 目前我未能找到一个好的答案
所以无法给你大家带来分享
但是我会继续研究这个地方
我也希望我这次是一次抛砖引玉的过程
大家如果对这个地方很感兴趣的话
我希望大家可以参与进来
我们一起来讨论下这个问题
我们一进步

另外还有一个问题就是
如果 大家觉得这个项目有哪些 不足的地方
也希望大家可以 来 留言弹幕告诉我
我们一起想办法来改进他

总结

本次课我们只是探讨下
什么是 分布式事务
以及他目前有的几种实现方式
并且我们尝试了一下 开源的 TCC 分布式事务框架
这个人写的代码我感觉总体还是不错的
设计思路也很清晰的
由于我无法给他提交代码
所以大家 下载他的代码之后 可以参照我的博客文章 修改下对应的东西
就可以启动了起来 自己断点调试了
调试的过程中 我相信大家会对于这个 TCC 分布式事务有更好的理解的.
如果大家觉得这个框架有什么不太好的设计的地方
大家可以留言告诉我
我们可以一起来研究这个课题
我们一起进步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值