首先想要了解分佈式事務,得先了解什麼是事務。能夠瀏覽我上面寫過的事務的博客java
Spring事務原理分析-部分一mysql
分佈式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不一樣的分佈式系統的不一樣節點之上。簡單的說,就是一次大的操做由不一樣的小操做組成,這些小的操做分佈在不一樣的服務器上,且屬於不一樣的應用,分佈式事務須要保證這些小操做要麼所有成功,要麼所有失敗。本質上來講,分佈式事務就是爲了保證不一樣數據庫的數據一致性。github
當數據庫單表一年產生的數據超過1000W,那麼就要考慮分庫分表,具體分庫分表的原理在此不作解釋,之後有空詳細說,簡單的說就是原來的一個數據庫變成了多個數據庫。這時候,若是一個操做既訪問01庫,又訪問02庫,並且要保證數據的一致性,那麼就要用到分佈式事務。web
所謂的SOA化,就是業務的服務化。好比原來單機支撐了整個電商網站,如今對整個網站進行拆解,分離出了訂單中心、用戶中心、庫存中心。對於訂單中心,有專門的數據庫存儲訂單信息,用戶中心也有專門的數據庫存儲用戶信息,庫存中心也會有專門的數據庫存儲庫存信息。這時候若是要同時對訂單和庫存進行操做,那麼就會涉及到訂單數據庫和庫存數據庫,爲了保證數據一致性,就須要用到分佈式事務。spring
原子性是指事務包含的全部操做要麼所有成功,要麼所有失敗回滾,所以事務的操做若是成功就必需要徹底應用到數據庫,若是操做失敗則不能對數據庫有任何影響。sql
一致性是指事務必須使數據庫從一個一致性狀態變換到另外一個一致性狀態,也就是說一個事務執行以前和執行以後都必須處於一致性狀態。數據庫
拿轉帳來講,假設用戶A和用戶B二者的錢加起來一共是5000,那麼無論A和B之間如何轉帳,轉幾回帳,事務結束後兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。編程
隔離性是當多個用戶併發訪問數據庫時,好比操做同一張表時,數據庫爲每個用戶開啓的事務,不能被其餘事務的操做所幹擾,多個併發事務之間要相互隔離。api
即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始以前就已經結束,要麼在T1結束以後纔開始,這樣每一個事務都感受不到有其餘事務在併發地執行。
關於事務的隔離性數據庫提供了多種隔離級別,稍後會介紹到。
持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,即使是在數據庫系統遇到故障的狀況下也不會丟失提交事務的操做。
例如咱們在使用JDBC操做數據庫時,在提交事務方法後,提示用戶事務操做完成,當咱們程序執行完成直到看到提示後,就能夠認定事務以及正確提交,即便這時候數據庫出現了問題,也必需要將咱們的事務徹底執行完成,不然就會形成咱們看到提示事務處理完畢,可是數據庫由於故障而沒有執行事務的重大錯誤。
最經典的場景就是支付了,一筆支付,是對買家帳戶進行扣款,同時對賣家帳戶進行加錢,這些操做必須在一個事務裏執行,要麼所有成功,要麼所有失敗。而對於買家帳戶屬於買家中心,對應的是買家數據庫,而賣家帳戶屬於賣家中心,對應的是賣家數據庫,對不一樣數據庫的操做必然須要引入分佈式事務。
買家在電商平臺下單,每每會涉及到兩個動做,一個是扣庫存,第二個是更新訂單狀態,庫存和訂單通常屬於不一樣的數據庫,須要使用分佈式事務保證數據一致性。
XA是一個分佈式事務協議,由Tuxedo提出。XA中大體分爲兩部分:事務管理器和本地資源管理器。其中本地資源管理器每每由數據庫實現,好比Oracle、DB2這些商業數據庫都實現了XA接口,而事務管理器做爲全局的調度者,負責各個本地資源的提交和回滾。XA實現分佈式事務的原理以下:
總的來講,XA協議比較簡單,並且一旦商業數據庫實現了XA協議,使用分佈式事務的成本也比較低。可是,XA也有致命的缺點,那就是性能不理想,特別是在交易下單鏈路,每每併發量很高,XA沒法知足高併發場景。XA目前在商業數據庫支持的比較理想,在mysql數據庫中支持的不太理想,mysql的XA實現,沒有記錄prepare階段日誌,主備切換會致使主庫與備庫數據不一致。許多nosql也沒有支持XA,這讓XA的應用場景變得很是狹隘。
所謂的消息事務就是基於消息中間件的兩階段提交,本質上是對消息中間件的一種特殊利用,它是將本地事務和發消息放在了一個分佈式事務裏,保證要麼本地操做成功成功而且對外發消息成功,要麼二者都失敗,開源的RocketMQ就支持這一特性,具體原理以下:
一、A系統向消息中間件發送一條預備消息
二、消息中間件保存預備消息並返回成功
三、A執行本地事務
四、A發送提交消息給消息中間件
經過以上4步完成了一個消息事務。對於以上的4個步驟,每一個步驟均可能產生錯誤,下面一一分析:
步驟一出錯,則整個事務失敗,不會執行A的本地操做
步驟二出錯,則整個事務失敗,不會執行A的本地操做
步驟三出錯,這時候須要回滾預備消息,怎麼回滾?答案是A系統實現一個消息中間件的回調接口,消息中間件會去不斷執行回調接口,檢查A事務執行是否執行成功,若是失敗則回滾預備消息
步驟四出錯,這時候A的本地事務是成功的,那麼消息中間件要回滾A嗎?答案是不須要,其實經過回調接口,消息中間件可以檢查到A執行成功了,這時候其實不須要A發提交消息了,消息中間件能夠本身對消息進行提交,從而完成整個消息事務
基於消息中間件的兩階段提交每每用在高併發場景下,將一個分佈式事務拆成一個消息事務(A系統的本地操做+發消息)+B系統的本地操做,其中B系統的操做由消息驅動,只要消息事務成功,那麼A操做必定成功,消息也必定發出來了,這時候B會收到消息去執行本地操做,若是本地操做失敗,消息會重投,直到B操做成功,這樣就變相地實現了A與B的分佈式事務。原理以下:
雖然上面的方案可以完成A和B的操做,可是A和B並非嚴格一致的,而是最終一致的,咱們在這裏犧牲了一致性,換來了性能的大幅度提高。固然,這種玩法也是有風險的,若是B一直執行不成功,那麼一致性會被破壞,具體要不要玩,仍是得看業務可以承擔多少風險。
另外此方案嚴重依賴消息中間件的高可用性,一旦消息中間件掛了整個系統就掛了。洪峯到來時,全部的系統都依賴消息中間件的話,消息中間件很容易就掛了。
所謂的TCC編程模式,也是兩階段提交的一個變種。TCC提供了一個編程框架,將整個業務邏輯分爲三塊:Try、Confirm和Cancel三個操做。以在線下單爲例,Try階段會去扣庫存,Confirm階段則是去更新訂單狀態,若是更新訂單失敗,則進入Cancel階段,會去恢復庫存。總之,TCC就是經過代碼人爲實現了兩階段提交,不一樣的業務場景所寫的代碼都不同,複雜度也不同,咱們今天着重講解TCC編程模式的代碼實現。
TCC是一種補償性分佈式事務解決方案,最初由支付寶提出。TCC是三個英文單詞的首字母縮寫,分別對應Try、Confirm和Cancel三種操做,這三種操做的業務含義以下:
Try:預留業務資源
Confirm:確認執行業務操做
Cancel:取消執行業務操做
稍稍對照下關係型數據庫事務的三種操做:DML、Commit和Rollback,會發現和TCC有殊途同歸之妙。在一個跨應用的業務操做中,Try操做是先把多個應用中的業務資源預留和鎖定住,爲後續的確認打下基礎,相似的,DML操做要鎖定數據庫記錄行,持有數據庫資源;Confirm操做是在Try操做中涉及的全部應用均成功以後進行確認,使用預留的業務資源,和Commit相似;而Cancel則是當Try操做中涉及的全部應用沒有所有成功,須要將已成功的應用進行取消(即Rollback回滾)。其中Confirm和Cancel操做是一對反向業務操做。
簡而言之,TCC是應用層的2PC(2 Phase Commit,兩階段提交),若是你將應用看作資源管理器的話。詳細來講,TCC每項操做須要作的事情以下:
一、Try:嘗試執行業務。
完成全部業務檢查(一致性)
預留必須業務資源(準隔離性)
二、Confirm:確認執行業務。
真正執行業務
不作任何業務檢查
只使用Try階段預留的業務資源
三、Cancel:取消執行業務
釋放Try階段預留的業務資源
TCC的理論有點讓人費解。故接下來將以帳務拆分爲例,對TCC事務的流程作一個描述,但願對理解TCC有所幫助。帳務拆分的業務場景以下,分別位於三個不一樣分庫的賬戶A、B、C,A和B一塊兒向C轉賬共80元:
一、Try:嘗試執行業務。
完成全部業務檢查(一致性):檢查A、B、C的賬戶狀態是否正常,賬戶A的餘額是否很多於30元,賬戶B的餘額是否很多於50元。
預留必須業務資源(準隔離性):賬戶A的凍結金額增長30元,賬戶B的凍結金額增長50元,這樣就保證不會出現其餘併發進程扣減了這兩個賬戶的餘額而致使在後續的真正轉賬操做過程當中,賬戶A和B的可用餘額不夠的狀況。
二、Confirm:確認執行業務。
真正執行業務:若是Try階段賬戶A、B、C狀態正常,且賬戶A、B餘額夠用,則執行賬戶A給帳戶C轉帳30元、賬戶B給帳戶C轉帳50元的轉賬操做。
不作任何業務檢查:這時已經不須要作業務檢查,Try階段已經完成了業務檢查。
只使用Try階段預留的業務資源:只須要使用Try階段賬戶A和賬戶B凍結的金額便可。
三、Cancel:取消執行業務
釋放Try階段預留的業務資源:若是Try階段部分紅功,好比賬戶A的餘額夠用,且凍結相應金額成功,賬戶B的餘額不夠而凍結失敗,則須要對賬戶A作Cancel操做,將賬戶A被凍結的金額解凍掉。
冪等性就是用戶對於同一操做發起的一次請求或者屢次請求的結果是一致的,不會由於屢次點擊而產生了反作用。
tcc-transaction是開源的TCC補償性分佈式事務框架
Git 地址: github.com/changmingxi…
咱們目前使用的版本是1.2,引入tcc-transaction工程。
安裝tcc-transaction-api tcc-transaction-core 和 tcc-transaction-dubbo到本地倉庫
(1)pom.xml中引入依賴
<dependency>
<groupId>org.mengyun</groupId>
<artifactId>tcc-transaction-dubbo-capital-api</artifactId>
<version>1.2.4.14</version>
</dependency>
<dependency>
<groupId>org.mengyun</groupId>
<artifactId>tcc-transaction-dubbo</artifactId>
<version>1.2.4.14</version>
</dependency>
<dependency>
<groupId>org.mengyun</groupId>
<artifactId>tcc-transaction-spring</artifactId>
<version>1.2.4.14</version>
</dependency>
複製代碼
(2)web.xml上下文加載tcc-transaction
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/spring/local/appcontext-*.xml,classpath:tcc-transaction.xml,classpath:tcc-transaction-dubbo.xml
</param-value>
</context-param>
複製代碼
發佈一個Tcc服務方法,可被遠程調用並參與到Tcc事務中,四個約束:
(1)在服務提供方的實現方法上加上@Compensable註解,並設置註解的屬性
(2)在服務提供方的接口方法上加上@Compensable註解
(3)服務方法的入參能被序列化(默認使用jdk序列化機制,須要參數實現Serializable接口,能夠設置repository的serializer屬性自定義序列化實現)
(4)try方法、confirm方法和cancel方法入參類型須同樣
Compensable的屬性包括propagation、confirmMethod、cancelMethod、transactionContextEditor。propagation可不用設置,框架使用缺省值;設置confirmMethod指定CONFIRM階段的調用方法;設置cancelMethod指定CANCEL階段的調用方法;設置transactionContextEditor爲DubboTransactionContextEditor.class。
發佈Tcc服務示例:
try接口方法:
@Compensable
public String record(CapitalTradeOrderDto tradeOrderDto);
複製代碼
try實現方法:
@Compensable(confirmMethod
= "confirmRecord", cancelMethod = "cancelRecord",
transactionContextEditor = DubboTransactionContextEditor.class)
public String record(CapitalTradeOrderDto tradeOrderDto) {}
複製代碼
confirm方法:
public void confirmRecord(CapitalTradeOrderDto tradeOrderDto) {}
複製代碼
cancel方法:
public void cancelRecord(CapitalTradeOrderDto tradeOrderDto) {}
複製代碼
調用遠程Tcc服務,將遠程Tcc服務參與到本地Tcc事務中,本地的服務方法也須要聲明爲Tcc服務,有三個約束:
(1)在服務方法上加上@Compensable註解,並設置註解屬性
(2)服務方法的入參都須能序列化(實現Serializable接口)
(3)try方法、confirm方法和cancel方法入參類型須同樣
調用Tcc服務示例:
try方法:
@Compensable(confirmMethod
= "confirmMakePayment", cancelMethod =
"cancelMakePayment")
public void makePayment(Order order,
BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
System.out.println("order try make payment called.time seq:"
+ DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd
HH:mm:ss"));
order.pay(redPacketPayAmount, capitalPayAmount);
orderRepository.updateOrder(order);
String result = capitalTradeOrderService.record(buildCapitalTradeOrderDto(order));
String result2 =
redPacketTradeOrderService.record(buildRedPacketTradeOrderDto(order));
}
複製代碼
confirm方法:
public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {}
複製代碼
cancel方法:
public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {}
複製代碼
執行提供的建庫建表腳本 (資源文件夾)
tcc-transaction-dubbo-capital 資產服務
tcc-transaction-dubbo-capital-api 資產服務接口
tcc-transaction-dubbo-redpacket 紅包服務
tcc-transaction-dubbo-redpacket-api 紅包服務接口
tcc-transaction-dubbo-order 訂單WEB工程(TCC調用方)
修改sample-dubbo-xxxxx.properties和jdbc.properties