本文是公眾號讀者有故事的驢的投稿
感謝驢同學的技術分享
目錄:
1.什么是事務?
2.換個角度看事務
3.Java中的事務
4.啥又是分布式事務?
5.分布式事務的幾種實現思路
6.總結
寫在前面
在分布式、微服務大行其道的今天,相信大家對這些名詞都不會陌生。而說到使用分布式,或者拆分微服務的好處,你肯定能想到一大堆。
比如每個人只需要維護自己單獨的服務,沒有了以前的各種代碼沖突。自己想測試、想發布、想升級,只需要care自己寫的代碼就OK了,很方便很貼心!
然而事物都有兩面性,但是它也同時也會帶來的一些問題,今天的文章談的就是分布式系統架構帶來的其中一個棘手的問題:分布式事務
1、什么是事務?
首先拋出來一個問題:什么是事務?
有人會說事務就是一系列操作,要么同時成功,要么同時失敗;然后會從事務的ACID特性(原子性、一致性、隔離性、持久性)展開敘述。
確實如此,事務就是為了保證一系列操作可以正常執行,它必須同時滿足ACID特性。
但是今天我們換個角度思考下,我們不僅要知道what(比如什么是事務),更要知道事務的why(比如為什么會有事務這個概念?事務是為了解決什么問題)。
有時候,換個角度說不定有不一樣的收獲。
2、換個角度看事務
就像經典的文學作品均來自於生活,卻又高於生活,事務的概念同樣來自於生活,引入“事務”肯定是為了解決某種問題,不然,誰又願意干這么無聊的事情呢?
最簡單最經典的例子:銀行轉賬,我們要從A賬戶轉1000塊到B賬戶。
正常情況下如果從A轉出1000到B賬戶之后,A賬戶余額減1000(這個操作我們用action1代表),B賬戶余額加1000(這個操作我們用action2代表)
首先我們要明確一點,action1和action2是兩個操作。既然是兩個操作那么就一定會存在執行的先后順序。那么就可能會出現action1執行完剛准備去執行action2的時候出問題了(比如數據庫負載過大暫時拒絕訪問)。
類比到我們生活中,那就是我給朋友轉了1000塊錢,然后我卡里的余額少了1000,但是我朋友確沒有收到錢。
為解決這種“money去哪兒了”的問題,引入了“事務”的概念。也就是說,既然我轉賬的時候你保證不了100%能成功,比如銀行系統只能保證99.99%的高可用,那么在那0.01%的時間里如果出現了上述問題,銀行系統直接回滾action1操作?(即把1000塊錢再加回余額中去)
對於銀行系統來說,可能在0.01%的時間里我保證不了action1和action2同時成功,那么在出問題的時候,我保證它倆同時失敗。(事務的原子性)
通過這個例子,就已經回答了剛開始提出的2個問題(為什么會有事務?事務是為了解決什么問題?)
總結一下:事務就是通過它的ACID特性,保證一系列的操作在任何情況下都可以安全正確的執行。
3、Java中的事務
搞清楚了事務之后,我們來看點眼熟的,java中的事務是怎么玩的?
Java中我們平時用的最多的就是在service層的增刪改方法上添加@Transactional注解,讓spring去幫我們管理事務。
它底層會給我們的service組件生成一個對應的proxy動態代理,這樣所有對service組件的方法都由它對應的proxy來接管
當proxy在調用對應業務方法比如add()時,proxy就會基於AOP的思想在調用真正的業務方法前執行setAutoCommit(false)打開事務。
然后在業務方法執行完后執行commit提交事務,當在執行業務方法的過程中發生異常時就會執行rollback來回滾事務。
當然@Transactional注解具體的實現細節這里不再展開,這個不是本篇文章的重點,本文的topic是“分布式事務”,關於@Transactional注解大家有興趣的話,可以自己打斷點debug源碼研究下,源碼出真知。
4、啥又是分布式事務?
鋪墊了辣么久,終於到了本篇的第一個重點!
首先大家想過沒:既然有了事務,並且使用spring的@Transactional注解來控制事務是如此的方便,那為啥還要搞一個分布式事務的概念出來啊?
更進一步,分布式事務和普通事務到底是啥關系?有什么區別?分布式事務又是為了解決什么問題出現的?
各種疑問接踵而至,別着急,帶着這些思考,咱們接下來就詳細聊聊分布式事務。
既然叫分布式事務,那么必然和分布式有點關系啦!簡單來說,分布式事務指的就是分布式系統中的事務。
至於什么是分布式系統?可以參考石杉老師之前的文章:
好,那咱們繼續,首先來看看下面的圖:
如上圖所示,一個單塊系統有3個模塊:員工模塊、財務模塊和請假模塊。我們現在有一個操作需要按順序去調用完成這3個模塊中的接口。
這個操作是一個整體,包含在一個事務中,要么同時成功要么同時失敗回滾。不成功便成仁,這個都沒有問題。
但是當我們把單塊系統拆分成分布式系統或者微服務架構,事務就不是上面那么玩兒了。
首先我們來看看拆分成分布式系統之后的架構圖,如下所示:
上圖是同一個操作在分布式系統中的執行情況。員工模塊、財務模塊和請假模塊分別給拆分成員工系統、財務系統和請假系統。
比如一個用戶進行一個操作,這個操作需要先調用員工系統預先處理一下,然后通過http或者rpc的方式分別調用財務系統和請假系統的接口做進一步的處理,它們的操作都需要分別落地到數據庫中。
這3個系統的一系列操作其實是需要全部被包裹在同一個分布式事務中的,此時這3個系統的操作,要么同時成功要么同時失敗。
分布式系統中完成一個操作通常需要多個系統間協同調用和通信,比如上面的例子。
三個子系統:員工系統、財務系統、請假系統之間就通過http或者rpc進行通信,而不再是一個單塊系統中不同模塊之間的調用,這就是分布式系統和單塊系統最大的區別。
一些平時不太關注分布式架構的同學,看到這里可能會說:我直接用spring的@Transactional注解就OK了啊,管那么多干嘛!
但是這里極其重要的一點:單塊系統是運行在同一個JVM進程中的,但是分布式系統中的各個系統運行在各自的JVM進程中
因此你直接加@Transactional注解是不行的,因為它只能控制同一個JVM進程中的事務,但是對於這種跨多個JVM進程的事務無能無力
5、分布式事務的幾種實現思路
搞清楚了啥是分布式事務,那么分布式事務到底是怎么玩兒的呢?
下邊就來給大家介紹幾種分布式事務的實現方案。
4.1 可靠消息最終一致性方案
整個流程圖如下所示:
我們來解釋一下這個方案的大概流程:
-
A系統先發送一個prepared消息到mq,如果這個prepared消息發送失敗那么就直接取消操作別執行了,后續操作都不再執行
-
如果這個消息發送成功過了,那么接着執行A系統的本地事務,如果執行失敗就告訴mq回滾消息,后續操作都不再執行
-
如果A系統本地事務執行成功,就告訴mq發送確認消息
-
那如果A系統遲遲不發送確認消息呢?
此時mq會自動定時輪詢所有prepared消息,然后調用A系統事先提供的接口,通過這個接口反查A系統的上次本地事務是否執行成功
如果成功,就發送確認消息給mq;失敗則告訴mq回滾消息(后續操作都不再執行)
-
此時B系統會接收到確認消息,然后執行本地的事務,如果本地事務執行成功則事務正常完成
-
如果系統B的本地事務執行失敗了咋辦?
基於mq重試咯,mq會自動不斷重試直到成功,如果實在是不行,可以發送報警由人工來手工回滾和補償
這種方案的要點就是可以基於mq來進行不斷重試,最終一定會執行成功的。
因為一般執行失敗的原因是網絡抖動或者數據庫瞬間負載太高,都是暫時性問題。
通過這種方案,99.9%的情況都是可以保證數據最終一致性的,剩下的0.1%出問題的時候,就人工修復數據唄。
適用場景:
這個方案的使用還是比較廣,目前國內互聯網公司大都是基於這種思路玩兒的。
4.2 最大努力通知方案
整個流程圖如下所示:
這個方案的大致流程:
-
系統A本地事務執行完之后,發送個消息到MQ
-
這里會有個專門消費MQ的最大努力通知服務,這個服務會消費MQ,然后寫入數據庫中記錄下來,或者是放入個內存隊列。接着調用系統B的接口
-
假如系統B執行成功就萬事ok了,但是如果系統B執行失敗了呢?
那么此時最大努力通知服務就定時嘗試重新調用系統B,反復N次,最后還是不行就放棄。
這套方案和上面的可靠消息最終一致性方案的區別:
可靠消息最終一致性方案可以保證的是只要系統A的事務完成,通過不停(無限次)重試來保證系統B的事務總會完成
但是最大努力方案就不同,如果系統B本地事務執行失敗了,那么它會重試N次后就不再重試,系統B的本地事務可能就不會完成了。
至於你想控制它究竟有“多努力”,這個需要結合自己的業務來配置。
比如對於電商系統,在下完訂單后發短信通知用戶下單成功的業務場景中,下單正常完成,但是到了發短信的這個環節由於短信服務暫時有點問題,導致重試了3次還是失敗。
那么此時就不再嘗試發送短信,因為在這個場景中我們認為3次就已經算是盡了“最大努力”了。
簡單總結:就是在指定的重試次數內,如果能執行成功那么皆大歡喜,如果超過了最大重試次數就放棄,不再進行重試。
適用場景:
一般用在不太重要的業務操作中,就是那種完成的話是錦上添花,但失敗的話對我也沒有什么壞影響的場景。
比如上邊提到的電商中的部分通知短信,就比較適合使用這種最大努力通知方案來做分布式事務的保證。
4.3 tcc強一致性方案
TCC的全稱是:
-
Try(嘗試)
-
Confirm(確認/提交)
-
Cancel(回滾)。
這個其實是用到了補償的概念,分為了三個階段:
-
Try階段:這個階段說的是對各個服務的資源做檢測以及對資源進行鎖定或者預留;
-
Confirm階段:這個階段說的是在各個服務中執行實際的操作;
-
Cancel階段:如果任何一個服務的業務方法執行出錯,那么這里就需要進行補償,就是執行已經執行成功的業務邏輯的回滾操作;
還是給大家舉個例子:
比如跨銀行轉賬的時候,要涉及到兩個銀行的分布式事務,如果用TCC方案來實現,思路是這樣的;
-
Try階段:先把兩個銀行賬戶中的資金給它凍結住就不讓操作了
-
Confirm階段:執行實際的轉賬操作,A銀行賬戶的資金扣減,B銀行賬戶的資金增加
-
Cancel階段:如果任何一個銀行的操作執行失敗,那么就需要回滾進行補償,就是比如A銀行賬戶如果已經扣減了,但是B銀行賬戶資金增加失敗了,那么就得把A銀行賬戶資金給加回去
適用場景:
這種方案說實話幾乎很少有人使用,我們用的也比較少,但是也有使用的場景。
因為這個事務回滾實際上是嚴重依賴於你自己寫代碼來回滾和補償了,會造成補償代碼巨大,非常之惡心。
比如說我們,一般來說跟錢相關的,跟錢打交道的,支付、交易相關的場景,我們會用TCC,嚴格保證分布式事務要么全部成功,要么全部自動回滾,嚴格保證資金的正確性,在資金上不允許出現問題。
比較適合的場景:除非你是真的一致性要求太高,是你系統中核心之核心的場景,比如常見的就是資金類的場景,那你可以用TCC方案了
你需要自己編寫大量的業務邏輯,自己判斷一個事務中的各個環節是否ok,不ok就執行補償/回滾代碼。
而且最好是你的各個業務執行的時間都比較短。
但是說實話,一般盡量別這么搞,自己手寫回滾邏輯,或者是補償邏輯,實在太惡心了,那個業務代碼很難維護。
6、總結
本篇介紹了什么是分布式事務,然后還介紹了最常用的3種分布式事務方案
但除了上邊的方案外,其實還有兩階段提交方案(XA方案)和本地消息表等方案。
但是說實話極少有公司使用這些方案,鑒於篇幅所限,不做介紹。后續如果有機會再出篇文章,詳細聊聊這兩種方案的思路。