“分布式事務(wù)”,這次徹底懂了!
在分布式、微服務(wù)大行其道的今天,相信大家對這些名詞都不會陌生。而說到使用分布式,或者拆分微服務(wù)的好處,你肯定能想到一大堆。
圖片來自包圖網(wǎng)
比如每個(gè)人只需要維護(hù)自己單獨(dú)的服務(wù),沒有了以前的各種代碼沖突。自己想測試、想發(fā)布、想升級,只需要 Care 自己寫的代碼就 OK 了,很方便很貼心!
然而事物都有兩面性,它同時(shí)也會帶來一些問題,今天的文章談的就是分布式系統(tǒng)架構(gòu)帶來的其中一個(gè)棘手的問題:分布式事務(wù)!
什么是事務(wù)?
首先拋出來一個(gè)問題:什么是事務(wù)?有人會說事務(wù)就是一系列操作,要么同時(shí)成功,要么同時(shí)失敗;然后會從事務(wù)的 ACID 特性(原子性、一致性、隔離性、持久性)展開敘述。
確實(shí)如此,事務(wù)就是為了保證一系列操作可以正常執(zhí)行,它必須同時(shí)滿足 ACID 特性。
但是今天我們換個(gè)角度思考下,我們不僅要知道 What(比如什么是事務(wù)),更要知道事務(wù)的 Why(比如為什么會有事務(wù)這個(gè)概念?事務(wù)是為了解決什么問題)。
有時(shí)候,換個(gè)角度說不定有不一樣的收獲。
換個(gè)角度看事務(wù)
就像經(jīng)典的文學(xué)作品均來自于生活,卻又高于生活,事務(wù)的概念同樣來自于生活,引入“事務(wù)”肯定是為了解決某種問題,不然,誰又愿意干這么無聊的事情呢?
最簡單最經(jīng)典的例子:銀行轉(zhuǎn)賬,我們要從 A 賬戶轉(zhuǎn) 1000 塊到 B 賬戶。
正常情況下如果從 A 轉(zhuǎn)出 1000 到 B 賬戶之后,A 賬戶余額減 1000(這個(gè)操作我們用 Action1 代表),B 賬戶余額加 1000(這個(gè)操作我們用 Action2 代表)
首先我們要明確一點(diǎn),Action1 和 Action2 是兩個(gè)操作。既然是兩個(gè)操作那么就一定會存在執(zhí)行的先后順序。
那么就可能會出現(xiàn) Action1 執(zhí)行完剛準(zhǔn)備去執(zhí)行 Action2 的時(shí)候出問題了(比如數(shù)據(jù)庫負(fù)載過大暫時(shí)拒絕訪問)。
類比到我們生活中,那就是我給朋友轉(zhuǎn)了 1000 塊錢,然后我卡里的余額少了 1000,但是我朋友卻沒有收到錢。
為解決這種“Money 去哪兒了”的問題,引入了“事務(wù)”的概念。也就是說,既然我轉(zhuǎn)賬的時(shí)候你保證不了 100% 能成功,比如銀行系統(tǒng)只能保證 99.99% 的高可用,那么在那 0.01% 的時(shí)間里如果出現(xiàn)了上述問題,銀行系統(tǒng)直接回滾 Action1 操作?(即把 1000 塊錢再加回余額中去)
對于銀行系統(tǒng)來說,可能在 0.01% 的時(shí)間里我保證不了 Action1 和 Action2 同時(shí)成功,那么在出問題的時(shí)候,我保證它倆同時(shí)失敗。(事務(wù)的原子性)
通過這個(gè)例子,就已經(jīng)回答了剛開始提出的 2 個(gè)問題(為什么會有事務(wù)?事務(wù)是為了解決什么問題?)
總結(jié)一下:事務(wù)就是通過它的 ACID 特性,保證一系列的操作在任何情況下都可以安全正確的執(zhí)行。
Java 中的事務(wù)
搞清楚了事務(wù)之后,我們來看點(diǎn)眼熟的,Java 中的事務(wù)是怎么玩的?
Java 中我們平時(shí)用的最多的就是在 Service 層的增刪改方法上添加 @Transactional 注解,讓 Spring 去幫我們管理事務(wù)。
它底層會給我們的 Service 組件生成一個(gè)對應(yīng)的 Proxy 動態(tài)代理,這樣所有對 Service 組件的方法都由它對應(yīng)的 Proxy 來接管。
當(dāng) Proxy 在調(diào)用對應(yīng)業(yè)務(wù)方法比如 add() 時(shí),Proxy 就會基于 AOP 的思想在調(diào)用真正的業(yè)務(wù)方法前執(zhí)行 setAutoCommit(false)打開事務(wù)。
然后在業(yè)務(wù)方法執(zhí)行完后執(zhí)行 Commit 提交事務(wù),當(dāng)在執(zhí)行業(yè)務(wù)方法的過程中發(fā)生異常時(shí)就會執(zhí)行 Rollback 來回滾事務(wù)。
當(dāng)然 @Transactional 注解具體的實(shí)現(xiàn)細(xì)節(jié)這里不再展開,這個(gè)不是本篇文章的重點(diǎn),本文的 Topic 是“分布式事務(wù)”,關(guān)于 @Transactional 注解大家有興趣的話,可以自己打斷點(diǎn) Debug 源碼研究下,源碼出真知。
啥又是分布式事務(wù)?
鋪墊了辣么久,終于到了本篇的***個(gè)重點(diǎn)!首先大家想過沒:既然有了事務(wù),并且使用 Spring 的 @Transactional 注解來控制事務(wù)是如此的方便,那為啥還要搞一個(gè)分布式事務(wù)的概念出來啊?
更進(jìn)一步,分布式事務(wù)和普通事務(wù)到底是啥關(guān)系?有什么區(qū)別?分布式事務(wù)又是為了解決什么問題出現(xiàn)的?
各種疑問接踵而至,別著急,帶著這些思考,咱們接下來就詳細(xì)聊聊分布式事務(wù)。
既然叫分布式事務(wù),那么必然和分布式有點(diǎn)關(guān)系啦!簡單來說,分布式事務(wù)指的就是分布式系統(tǒng)中的事務(wù)。
好,那咱們繼續(xù),首先來看看下面的圖:
如上圖所示,一個(gè)單塊系統(tǒng)有 3 個(gè)模塊:員工模塊、財(cái)務(wù)模塊和請假模塊。我們現(xiàn)在有一個(gè)操作需要按順序去調(diào)用完成這 3 個(gè)模塊中的接口。
這個(gè)操作是一個(gè)整體,包含在一個(gè)事務(wù)中,要么同時(shí)成功要么同時(shí)失敗回滾。不成功便成仁,這個(gè)都沒有問題。
但是當(dāng)我們把單塊系統(tǒng)拆分成分布式系統(tǒng)或者微服務(wù)架構(gòu),事務(wù)就不是上面那么玩兒了。
首先我們來看看拆分成分布式系統(tǒng)之后的架構(gòu)圖,如下所示:
上圖是同一個(gè)操作在分布式系統(tǒng)中的執(zhí)行情況。員工模塊、財(cái)務(wù)模塊和請假模塊分別給拆分成員工系統(tǒng)、財(cái)務(wù)系統(tǒng)和請假系統(tǒng)。
比如一個(gè)用戶進(jìn)行一個(gè)操作,這個(gè)操作需要先調(diào)用員工系統(tǒng)預(yù)先處理一下,然后通過 HTTP 或者 RPC 的方式分別調(diào)用財(cái)務(wù)系統(tǒng)和請假系統(tǒng)的接口做進(jìn)一步的處理,它們的操作都需要分別落地到數(shù)據(jù)庫中。
這 3 個(gè)系統(tǒng)的一系列操作其實(shí)是需要全部被包裹在同一個(gè)分布式事務(wù)中的,此時(shí)這 3 個(gè)系統(tǒng)的操作,要么同時(shí)成功要么同時(shí)失敗。
分布式系統(tǒng)中完成一個(gè)操作通常需要多個(gè)系統(tǒng)間協(xié)同調(diào)用和通信,比如上面的例子。
三個(gè)子系統(tǒng):員工系統(tǒng)、財(cái)務(wù)系統(tǒng)、請假系統(tǒng)之間就通過 HTTP 或者 RPC 進(jìn)行通信,而不再是一個(gè)單塊系統(tǒng)中不同模塊之間的調(diào)用,這就是分布式系統(tǒng)和單塊系統(tǒng)***的區(qū)別。
一些平時(shí)不太關(guān)注分布式架構(gòu)的同學(xué),看到這里可能會說:我直接用 Spring 的 @Transactional 注解就 OK 了啊,管那么多干嘛!
但是這里極其重要的一點(diǎn):單塊系統(tǒng)是運(yùn)行在同一個(gè) JVM 進(jìn)程中的,但是分布式系統(tǒng)中的各個(gè)系統(tǒng)運(yùn)行在各自的 JVM 進(jìn)程中。
因此你直接加 @Transactional 注解是不行的,因?yàn)樗荒芸刂仆粋€(gè) JVM 進(jìn)程中的事務(wù),但是對于這種跨多個(gè) JVM 進(jìn)程的事務(wù)無能無力。
分布式事務(wù)的幾種實(shí)現(xiàn)思路
搞清楚了啥是分布式事務(wù),那么分布式事務(wù)到底是怎么玩兒的呢?下邊就來給大家介紹幾種分布式事務(wù)的實(shí)現(xiàn)方案。
可靠消息最終一致性方案
整個(gè)流程圖如下所示:
我們來解釋一下這個(gè)方案的大概流程:
- A 系統(tǒng)先發(fā)送一個(gè) Prepared 消息到 MQ,如果這個(gè) Prepared 消息發(fā)送失敗那么就直接取消操作別執(zhí)行了,后續(xù)操作都不再執(zhí)行。
- 如果這個(gè)消息發(fā)送成功了,那么接著執(zhí)行 A 系統(tǒng)的本地事務(wù),如果執(zhí)行失敗就告訴 MQ 回滾消息,后續(xù)操作都不再執(zhí)行。
- 如果 A 系統(tǒng)本地事務(wù)執(zhí)行成功,就告訴 MQ 發(fā)送確認(rèn)消息。
- 那如果 A 系統(tǒng)遲遲不發(fā)送確認(rèn)消息呢?此時(shí) MQ 會自動定時(shí)輪詢所有 Prepared 消息,然后調(diào)用 A 系統(tǒng)事先提供的接口,通過這個(gè)接口反查 A 系統(tǒng)的上次本地事務(wù)是否執(zhí)行成功。
- 如果成功,就發(fā)送確認(rèn)消息給 MQ;失敗則告訴 MQ 回滾消息。(后續(xù)操作都不再執(zhí)行)
- 此時(shí) B 系統(tǒng)會接收到確認(rèn)消息,然后執(zhí)行本地的事務(wù),如果本地事務(wù)執(zhí)行成功則事務(wù)正常完成。
- 如果系統(tǒng) B 的本地事務(wù)執(zhí)行失敗了咋辦?基于 MQ 重試咯,MQ 會自動不斷重試直到成功,如果實(shí)在是不行,可以發(fā)送報(bào)警由人工來手工回滾和補(bǔ)償。
這種方案的要點(diǎn)就是可以基于 MQ 來進(jìn)行不斷重試,最終一定會執(zhí)行成功的。
因?yàn)橐话銏?zhí)行失敗的原因是網(wǎng)絡(luò)抖動或者數(shù)據(jù)庫瞬間負(fù)載太高,都是暫時(shí)性問題。
通過這種方案,99.9% 的情況都是可以保證數(shù)據(jù)最終一致性的,剩下的 0.1% 出問題的時(shí)候,就人工修復(fù)數(shù)據(jù)唄。
適用場景:這個(gè)方案的使用還是比較廣,目前國內(nèi)互聯(lián)網(wǎng)公司大都是基于這種思路玩兒的。
***努力通知方案
整個(gè)流程圖如下所示:
這個(gè)方案的大致流程:
- 系統(tǒng) A 本地事務(wù)執(zhí)行完之后,發(fā)送個(gè)消息到 MQ。
- 這里會有個(gè)專門消費(fèi) MQ 的***努力通知服務(wù),這個(gè)服務(wù)會消費(fèi) MQ,然后寫入數(shù)據(jù)庫中記錄下來,或者是放入個(gè)內(nèi)存隊(duì)列。接著調(diào)用系統(tǒng) B 的接口。
- 假如系統(tǒng) B 執(zhí)行成功就萬事 OK 了,但是如果系統(tǒng) B 執(zhí)行失敗了呢?
- 那么此時(shí)***努力通知服務(wù)就定時(shí)嘗試重新調(diào)用系統(tǒng) B,反復(fù) N 次,***還是不行就放棄。
這套方案和上面的可靠消息最終一致性方案的區(qū)別:可靠消息最終一致性方案可以保證的是只要系統(tǒng) A 的事務(wù)完成,通過不停(***次)重試來保證系統(tǒng) B 的事務(wù)總會完成。
但是***努力方案就不同,如果系統(tǒng) B 本地事務(wù)執(zhí)行失敗了,那么它會重試 N 次后就不再重試,系統(tǒng) B 的本地事務(wù)可能就不會完成了。至于你想控制它究竟有“多努力”,這個(gè)需要結(jié)合自己的業(yè)務(wù)來配置。
比如對于電商系統(tǒng),在下完訂單后發(fā)短信通知用戶下單成功的業(yè)務(wù)場景中,下單正常完成,但是到了發(fā)短信的這個(gè)環(huán)節(jié)由于短信服務(wù)暫時(shí)有點(diǎn)問題,導(dǎo)致重試了 3 次還是失敗。
那么此時(shí)就不再嘗試發(fā)送短信,因?yàn)樵谶@個(gè)場景中我們認(rèn)為 3 次就已經(jīng)算是盡了“***努力”了。
簡單總結(jié):就是在指定的重試次數(shù)內(nèi),如果能執(zhí)行成功那么皆大歡喜,如果超過了***重試次數(shù)就放棄,不再進(jìn)行重試。
適用場景:一般用在不太重要的業(yè)務(wù)操作中,就是那種完成的話是錦上添花,但失敗的話對我也沒有什么壞影響的場景。
比如上邊提到的電商中的部分通知短信,就比較適合使用這種***努力通知方案來做分布式事務(wù)的保證。
TCC 強(qiáng)一致性方案
TCC的 全稱是:
- Try(嘗試)
- Confirm(確認(rèn)/提交)
- Cancel(回滾)
這個(gè)其實(shí)是用到了補(bǔ)償?shù)母拍睿譃榱巳齻€(gè)階段:
- Try 階段:這個(gè)階段說的是對各個(gè)服務(wù)的資源做檢測以及對資源進(jìn)行鎖定或者預(yù)留。
- Confirm 階段:這個(gè)階段說的是在各個(gè)服務(wù)中執(zhí)行實(shí)際的操作。
- Cancel 階段:如果任何一個(gè)服務(wù)的業(yè)務(wù)方法執(zhí)行出錯(cuò),那么這里就需要進(jìn)行補(bǔ)償,就是執(zhí)行已經(jīng)執(zhí)行成功的業(yè)務(wù)邏輯的回滾操作。
還是給大家舉個(gè)例子:
比如跨銀行轉(zhuǎn)賬的時(shí)候,要涉及到兩個(gè)銀行的分布式事務(wù),如果用 TCC 方案來實(shí)現(xiàn),思路是這樣的:
- Try 階段:先把兩個(gè)銀行賬戶中的資金給它凍結(jié)住就不讓操作了。
- Confirm 階段:執(zhí)行實(shí)際的轉(zhuǎn)賬操作,A 銀行賬戶的資金扣減,B 銀行賬戶的資金增加。
- Cancel 階段:如果任何一個(gè)銀行的操作執(zhí)行失敗,那么就需要回滾進(jìn)行補(bǔ)償,就是比如 A 銀行賬戶如果已經(jīng)扣減了,但是 B 銀行賬戶資金增加失敗了,那么就得把 A 銀行賬戶資金給加回去。
適用場景:這種方案說實(shí)話幾乎很少有人使用,我們用的也比較少,但是也有使用的場景。
因?yàn)檫@個(gè)事務(wù)回滾實(shí)際上是嚴(yán)重依賴于你自己寫代碼來回滾和補(bǔ)償了,會造成補(bǔ)償代碼巨大,非常之惡心。
比如說我們,一般來說跟錢相關(guān)的,跟錢打交道的,支付、交易相關(guān)的場景,我們會用 TCC,嚴(yán)格保證分布式事務(wù)要么全部成功,要么全部自動回滾,嚴(yán)格保證資金的正確性,在資金上不允許出現(xiàn)問題。
比較適合的場景:除非你是真的一致性要求太高,是你系統(tǒng)中核心之核心的場景,比如常見的就是資金類的場景,那你可以用 TCC 方案了。
你需要自己編寫大量的業(yè)務(wù)邏輯,自己判斷一個(gè)事務(wù)中的各個(gè)環(huán)節(jié)是否 OK,不 OK 就執(zhí)行補(bǔ)償/回滾代碼。
而且***是你的各個(gè)業(yè)務(wù)執(zhí)行的時(shí)間都比較短。但是說實(shí)話,一般盡量別這么搞,自己手寫回滾邏輯,或者是補(bǔ)償邏輯,實(shí)在太惡心了,那個(gè)業(yè)務(wù)代碼很難維護(hù)。
總結(jié)
本篇介紹了什么是分布式事務(wù),然后還介紹了最常用的 3 種分布式事務(wù)方案
但除了上邊的方案外,其實(shí)還有兩階段提交方案(XA 方案)和本地消息表等方案。
但是說實(shí)話極少有公司使用這些方案,鑒于篇幅所限,不做介紹。后續(xù)如果有機(jī)會再出篇文章,詳細(xì)聊聊這兩種方案的思路。
中華石杉:十余年 BAT 架構(gòu)經(jīng)驗(yàn),一線互聯(lián)網(wǎng)公司技術(shù)總監(jiān)。帶領(lǐng)上百人團(tuán)隊(duì)開發(fā)過多個(gè)億級流量高并發(fā)系統(tǒng)?,F(xiàn)將多年工作中積累下的研究手稿、經(jīng)驗(yàn)總結(jié)整理成文,傾囊相授。微信公眾號:石杉的架構(gòu)筆記(ID:shishan100)。