分佈式事務是個業界難題,在看分佈式事務方案以前,先從單機數據庫事務開始看起。html
什麼是事務 java
事務(Transaction)是數據庫系統中一系列操做的一個邏輯單元,全部操做要麼所有成功要麼所有失敗。spring
能夠看一個經典的轉帳事務示例,小哥哥轉帳100元給小姐姐:sql
begin:數據庫
操做1:查詢小哥哥帳號餘額,確保餘額充足架構
操做2:從小哥哥帳號扣除100元併發
操做3:往小姐姐帳號增長100元框架
commit;異步
轉帳的一系列操做就是一個事務,事務會確保這一系列操做要麼所有成功,要麼所有失敗。分佈式
談起事務就不得不談事務的四大特性ACID
原子性(Atomicity)
一個事務(transaction)中的全部操做,要麼所有完成,要麼所有不完成,不會結束在中間某個環節。事務在執行過程當中發生錯誤,會被恢復(Rollback)到事務開始前的狀態,就像這個事務歷來沒有執行過同樣。
還拿以前的轉帳案例來理解,原子性就是要求轉帳的一系列操做(操做一、操做二、操做3)要麼所有完成,要麼所有失敗。不能出現錢轉了一半的狀況,好比小哥哥的帳號錢扣除成功了,可是小姐姐帳號加錢的操做失敗了,這種屬於不知足原子性。
一致性(Consistency)
一致性這個詞老是一個讓人困惑話題,有時不一樣語境下說的都不是同一個事情。咱們先看看維基百科定義:
Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof.
咱們看看關鍵點,事務確保數據從一個valid狀態轉換到另一個valid狀態。什麼樣纔算是valid呢,符合all defined rules。all defined rules包括了constraints、 cascades、triggers等。因此這裏的一致性強調的是事務操做使得數據一直處於符合預約規則(約束、觸發器等)。This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct。可是咱們一般討論的一致性的含義每每比較廣,並不侷限於ACID的C。
看看轉帳案例,怎麼樣纔算符合一致性呢,帳戶餘額不能爲負數能夠算,而二者帳戶餘額相加=200則屬於應用語義層面的一致性,由原子性來保證。
隔離性(Isolation)
事務一般是併發執行的,同時對數據進行讀寫和修改的,隔離性強調的多個事務併發執行對數據的影響看起來跟串行同樣。一般爲了提升併發度,弱化了事務併發時對數據一致性的要求,容許若干種數據異常現象,從而定義了不一樣的事務隔離分爲不一樣級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(Serializable)。嚴格來講只有最高隔離的串行化隔離級別纔是符合隔離性的。
在轉帳案例中,若是在轉帳事務執行過程當中,能讀取到事務中間狀態,好比轉了一半而後出錯事務進行了回滾,讀到了「轉一半」的不一致的數據狀態,屬於髒讀。爲了提升併發度,在最低的讀未提交隔離級別是容許這種髒讀,其餘幾種不會出現此種髒讀。
持久性(Durability)
事務處理結束後,對數據的修改就是永久的,即使系統故障也不會丟失。持久性其實好理解,事務作完了要保證持久不丟失,好比轉帳已經轉成功了,不能這筆事務丟失掉。
從某種意義來講,ACID都是爲了保障數據的一致性,不知足ACID則會有數據的不一致。
分佈式事務
互聯網時代,業務發展迅猛,數據每每超出單機數據庫所能處理的極限,遇到性能的瓶頸。應用層面微服務架構愈來愈流行,從原來的單體應用拆分紅一個個獨立的微服務,當應用經過一組微服務來協助完成時,對數據的一致性就須要分佈式事務來保證。
對數據庫一般採用垂直拆分和水平數據分片,將數據拆分到多個不一樣的數據節點上。若是一個事務裏的操做涉及了多個不一樣分片節點則產生了分佈式事務。
咱們來看看業界常見的幾種分佈式事務實現:
基於XA協議的2pc
兩階段提交(2pc)大概屬於被提的最多的分佈式事務實現方案了。XA協議是 X/Open DTP Group提出的定義的兩段提交(2PC - Two-Phase-Commit)協議,主要用於分佈式數據庫事務管理。XA規範主要定義了(全局)事務管理器(Transaction Manager)和(局部)資源管理器(Resource Manager)之間的接口。
兩階段提交將提交過程分爲兩個階段,在第一階段,協調者詢問全部的參與者是否能夠提交事務(請參與者投票),全部參與者向協調者投票。在第二階段,協調者根據全部參與者的投票結果作出是否事務能夠全局提交的決定,並通知全部的參與者執行該決定。
2PC的缺點
2PC雖然保證了提交的原子性,但缺點也很明顯,先從協議自己來看看兩階段提交的缺點:
一、同步阻塞。執行過程當中,全部參與節點都是事務阻塞型的。當參與者佔有資源時,其餘第三方節點訪問資源不得不處於阻塞狀態。
二、單點故障。因爲協調者的重要性,一旦協調者發生故障。參與者會一直阻塞下去。尤爲在第二階段,協調者發生故障,那麼全部的參與者還都處於鎖定事務資源的狀態中,而沒法繼續完成事務操做。
從性能來看,2PC中協調者與每一個參與者至少有2輪消息交互、屢次寫日誌,過程又是同步阻塞,性能十分低下。
2PC是強一致嗎
常常聽到一些人說2PC是強一致的,真的是嗎?咱們先從隔離性的角度來看看,好比有兩個節點a和b,一個事務已經到第二階段準備提交,在某個時間點上,a節點已經提交,但b節點還未提交,這時另外一個事務就能看到a節點提交後的值以及b節點提交前的值。
還以以前轉帳爲例,轉入分支事務已經成功,轉出分支事務還未提交成功,這個時候就看到不一致,兩個帳號總額度是300,屬於髒讀,能看到不一致還能算強一致麼。
從隔離性角度來講,2PC的分佈式事務只能算最終一致,算不得強一致。通常人說的強一致只是說的原子性,事務要麼全成功要麼全失敗。因此一致性這個詞已經被玩壞了。
MySQL對XA支持的坑
另外咱們能夠看看MySQL對XA的支持,在MySQL5.7以前一直有2個缺陷多年未修復。
1. prepare未寫入binlog,若主庫宕機切換後則丟失prepare。
2. 客戶端退出或者服務宕機,MySQL會自動回滾。
MySQL對外部xa的支持還有其餘很多bug,這裏不一一贅述。
TCC
關於TCC(Try-Confirm-Cancel)的概念,最先是由Pat Helland於2007年發表的一篇名爲《Life beyond Distributed Transactions: an Apostate's Opinion》的論文提出。
TCC事務機制相對於XA的2PC相比,其特徵在於它不依賴資源管理器(RM)對XA的支持,而是經過對(由業務系統提供的)業務邏輯的調度來實現分佈式事務。
TCC型事務(Trying/Confirming/Canceling)。
TRYING 階段主要是對業務系統作檢測及資源預留。
CONFIRMING 階段主要是對業務系統作確認提交,TRYING階段執行成功並開始執行CONFIRMING階段時,默認CONFIRMING階段是不會出錯的。即:只要TRYING成功,CONFIRMING必定成功。
CANCELING 階段主要是在業務執行錯誤,須要回滾的狀態下執行的業務取消,預留資源釋放。
TCC與2PC區別
當討論2PC時,只專一於事務處理階段,於是只討論prepare和commit,每每忽略了業務邏輯執行階段,或者默認爲prepare包括了業務邏輯執行。能夠看下MySQL的XA的一個示例就比較好理解。
2PC的一個完整的事務生命週期是:begin -> 業務邏輯 -> prepare -> commit。再看TCC的一個完整的事務生命週期是:begin -> 業務邏輯(try業務) -> commit(comfirm業務)。
雖然TCC的confirm階段也會包含部分業務邏輯,固然從事務執行角度能夠簡化來看將commit與confirm類比,因此TCC並非兩階段提交。
TCC的Trying/Confirming/Canceling三個接口針對每一個事務都須要用戶本身來實現,其實對用戶不太友好,增長用戶開發工做量,另外不能保證全部人實現的接口必定能符合一致性要求,若是接口實現的有漏洞極可能會形成不一致。
SAGA
Saga是由普林斯頓大學的H.Garcia-Molina等人提出。其核心思想是將長事務拆分爲多個本地短事務,由Saga事務協調器協調。每一個Saga由一系列本地分支事務組成,每一個分支事務有對應一個補償事務。若是正常結束那就正常完成,若是某個步驟失敗,則根據相反順序一次調用補償操做。
SAGA事務模型,是犧牲了必定的隔離性的,可是提升了long-running事務的可用性。
除了隔離性的問題,SAGA跟TCC同樣對於補償的動做也是須要用戶本身實現,這點其實對用戶不太友好。
基於消息隊列
這種思路最先來自於ebay,核心思想將分佈式事務分紅多個本地事務,這裏稱之爲主事務與從事務。主事務本地先行提交,而後經過消息通知從事務,從事務從消息中獲取信息進行本地提交。能夠看出這是一種異步事務機制、只能保證最終一致性;但可用性很是高,不會由於故障而發生阻塞。
上述解決方案看似完美,實際上尚未解決分佈式問題。爲了使第一個事務不涉及分佈式操做,消息隊列必須與主事務使用同一套存儲資源,但爲了使第二個事務是本地的,消息隊列存儲又必須與第二事務的存儲在一塊兒。這二者是不可能同時知足的。本質上並無規避分佈式事務。
若是消息具備操做冪等性,也就是一個消息被應用屢次與應用一次產生的效果是同樣的話,上述問題是很好解決的。但實際狀況下,有些消息很難具備冪等性,好比轉帳中的扣款操做,執行一次和執行屢次的結束顯然是不同的,所以須要作不少額外處理,通常經過狀態表或者事務消息來解決。
最大努力提交
最大努力提交(Best Efforts)的關鍵點在於:
1. 與2PC相比,省去了prepare,本質上屬於1PC
2. commit推遲到最後一塊兒執行
最大努力提交最先在spring中事務管理中普遍流傳,感興趣的能夠參考:
https://www.javaworld.com/article/2077963/open-source-tools/distributed-transactions-in-spring--with-and-without-xa.html
在分佈式數據庫中間件的場景也普遍應用,咱們來看看MyCAT的事務模型,有時也被稱爲弱XA。
最大努力提交優勢是性能很是好且對用戶透明,缺點是可能存在部分提交成功部分失敗的場景(Partial commits),而對於已經commit成功的場景沒法rollback。可是因爲將容易出錯的sql執行階段先執行,commit推遲到最後一塊兒執行,至關於可能出錯的危險窗口期縮短到只有最後的commit階段,實際出錯機率很低。而commit開始以前出錯時能夠正常回滾,不會有不一致。
若是是在應用層採用該事務模型能夠將分支事務設計成冪等性,這樣在commit出錯時能夠對出錯分支進行重試。在分佈式數據庫中間件的場景,則很難具有冪等性。
各類分佈式事務的方案都各有優缺點,而業務場景又是複雜多樣的,對一致性的要求也各不同,很難有一種方案包打天下。因此DDM在設計分佈式事務方案時,充分考慮和權衡了各類方案的優缺點,提供了四種分佈式事務模型,能夠由用戶自由選擇。TCC等模型使用起來須要用戶本身實現相應的接口,對用戶很是不友好。所以DDM提供了全透明模型的分佈式事務,使用接口與原來單機一致。
適合業務拆分比較合理,在應用層有本身的完善的事務處理框架,到DDM的事務都是單分片事務,單分片事務由底層數據庫提供強一致性的保證。單機事務模型下,若是出現跨分片的事務,會報錯進行提示,避免達不到預期目的。
最大努力提交
該事務模型前文有描述,在此再也不贅述。該模型適合絕大部分不涉及金錢往來的業務,在性能和一致性之間比較好的一個平衡。事務中commit時由於是往多個節點發送執行,有部分commit成功部分commit失敗的可能性,可是可能性比較低,只有在commit的時間窗內出現異常纔有可能出現此種狀況。
最終一致性
最大努力提交模型的問題(Partial commits)本質是不知足ACID中的A原子性 ,針對該問題 作了改進,針對Partial commits中已經提交成功的分支事務進行自動的事務補償,保證了原子性,因爲從Partial commits到補償成功會有比較短的時間窗口,該時間窗口內數據處於不一致狀態但最終會達到一致狀態,因此稱之爲最終一致性。
當出現Partial commits異常狀況是,是容許應用支持讀取,因此可能會有髒讀,若是業務場景對髒讀比較敏感,好比以前轉帳事務中的查詢餘額,能夠經過對該select加for update或者lock in share mode來解決,至關於針對該語句了保證了讀已提交。
從Partial commits到補償成功時間窗內,業界有選擇不加鎖的則會出現回滾覆蓋,形成數據錯誤回補不成功,而DDM採用了高效的加鎖避免了該問題。
強一致性
強一致性模型既解決了分佈式事務的原子性,又可避免髒讀,確保了讀取到的都是commit成功的數據,從隔離級別上來屬於讀已提交。適合對一致性有極端要求的場景。可是一致性級別越高,付出性能代價會越大,因此請根據業務須要選擇合適的模型。