搜索 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 分佈式事務有更好的理解的.
如果大家覺得這個框架有什麼不太好的設計的地方
大家可以留言告訴我
我們可以一起來研究這個課題
我們一起進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章