分事事務之非同步確保性(二)

追尋北極發表於2017-11-21

資料庫dbA
表t1

資料庫dbB
表t2

目標,t1插入記錄時,同時保證t2也插入

假如使用分佈事務,非常簡單

開始分佈事務
...
insert into t1 ...
insert into t2 ...
提交分佈事務


不使用分佈事務要保證一致是無法直接實現的,比如

開始事務
...
insert into t1 ...
提交事務
如果成功,insert into t2 ...

看起來,只要dbB始終可靠似乎沒問題,
實際即便能保證dbB可靠且始終能插入,也還是存在問題.
比如,提交事務成功,此時程式崩潰或者斷電了,
那麼就沒機會執行t2插入了.



為此我們需要兩個庫各增加一張訊息表
dbA
msgToB(id int not null,s varchar)

dbB
msgFromA(id int not null,s varchar)

其中id是主鍵,s是訊息內容,流程如下:


開始dbA事務
...
insert into t1 ...
給dbB發訊息 insert into msgToB (id,s)values(...)
提交dbA事務


其中插入msgToB的id可以是自增量,也可以是額外獲得的唯一ID.
這樣保證只要t1插入記錄,msgToB就有對應的一條訊息

然後另一個程式
迴圈
 讀dbA中msgToB到id,s
 開始dbB事務
   如果msgFromA中沒有id記錄
     將id插入msgFromA
     根據s中內容,insert into t2 ...
 
   如果msgFromA中已經存在,說明已經處理過,直接返回即可
 提交dbB事務
 如果事務成功,刪除msgToB中對應記錄

這樣事務保證處理且只處理一次訊息,事務外如果未刪除
msgToB記錄,等到下次處理就會被自動刪除且不會被重複處理.

這樣達到我們的目標,兩個獨立的庫通過額外的兩張表用非同步訊息的機制達成了最終一致性.

當然這裡有一點是需要應用注意的,就是在dbB中的事務中
如果插入t2失敗(比如已經存在記錄),那麼對應訊息將永遠無法處理,
在單資料事務中,t2出錯時,資料庫會回滾對t1的插入.
一般這也是我們期望的.然而此種機制則需要應用來解決這個問題,
比如如果處理反覆失敗,則標記這個訊息已經處理,並給dba傳送一條新訊息,
讓dbA來處理這種狀態,比如撤銷對t1的插入.

更通常的做法應該是由應用保證類似情況不會發生,
既然我們已經設計了兩個獨立的資料來源僅僅依靠非同步訊息來連線,
那麼就必須考慮所有的正常流程與回滾方法.
比如:
單機裡
1.下單,插入訂單表
2.扣倉庫庫存
3.庫存不夠,回滾

在一個事務裡,使用者下單後,系統可以立刻告訴他沒庫存了,
這時根本不會生成新訂單(事務中被回滾了).

如果分佈處理
1.訂單庫,使用者下單
此時訂單已經生成了

2.訂單轉到倉庫1
3.倉庫1意外發現無貨了(比如剛發現貨被摔爛了...)
4.倉庫1發訊息給訂單庫
5.訂單庫將訂單狀態修改成無貨取消
然後可以再給另外的支付庫發訊息,完成退款

這裡,訂單庫,與倉庫1不光邏輯上是不同資料庫而且物理上
可能相隔甚遠,兩者網路可能高延時,低頻寬,不穩定(比如星淘,在另外一個星球上...)
這時兩階段提交的分佈事務幾乎無可能,
這種分散式非同步處理最終強一致性系統可能是唯一的選擇.



擴充套件一下:
1.任何一個支援本地事務的資料來源都可以參與這種事務.
比如一個資料庫可以和Redis或者兩個redis之間.
因為雖然redis不支援分散式事務,但是內部卻有事務機制,
只要在雙方增加一個類似的訊息表.

2.分散式水平擴充套件
比如dba實際可以對應dbb0,dbb1,dbb2...
可以很簡單的根據t1的主鍵將資料分佈到不同資料庫的t2表而實現水平擴充套件.


3.通過級聯,可以將一個事務擴充套件到更多不同節點.
比如,資料來源1發訊息給資料來源2,資料來源2處理後發給資料來源3...

開始資料來源2事務
...
完成資料來源1的訊息處理,insert into msgFrom1
給資料來源3發訊息,insert into msgTo3

提交資料來源2事務

結合充分理解和使用單機事務,你會發現這種機制非常簡單好用。

4.與直接使用訊息佇列系統的比較
訊息佇列已經在現代大型IT系統廣泛採用了.
一種是弱事務性的處理,允許丟或多訊息,這種方式與我們上面要求的強一致性應用場景完全不同.
另一種也要求訊息絕對不能丟,這種場景與上面討論的一致.
但是要注意到,必須使用兩階段提交才能保證不丟訊息!
即便訊息佇列本身可以保證.

比如傳送訊息,如果一個保證可靠訊息的訊息系統告訴我們成功了,
我們可以肯定這個訊息不會丟失,接受者肯定可以接受到,
但是接受到不代表能處理!比如接收方

開始訊息事務
讀訊息
 開始資料庫事務
   處理訊息
 提交資料庫事務
刪除訊息
提交訊息事務

問題很顯然,提交資料庫事務後,訊息實際被處理了,
但是此時系統崩潰的話,訊息將不會被刪除,還會被再處理一遍.
反之如果先提交訊息事務,隨後崩潰的話,就會導致訊息丟失.
解決方式就是要麼將訊息事務與資料庫事務合併為分散式事務,
要麼同樣採用上面的方式,在兩個資料來源增加表.
而採用後種方式時,訊息佇列的優勢就喪失了(因為使用訊息系統
一般就是看中她快捷,如果在插入訊息佇列時還要額外再插入資料庫
訊息系統還有什麼存在的必要呢?).

從這裡可以看出,訊息佇列系統實際最適合非同步弱一致性處理,
類似處理日誌這種,偶爾多個少個幾個完全無所謂.
如果看重他的其他方面但還要求訊息絕對可靠,
那麼在和其他系統連線時,必須使用分散式事務.
不然長長的管子不漏,接頭漏了,還是白忙活.

相關文章