前言
先聲明定硝,本文不會介紹諸如ACID、2PC毫目、CAP等概念性的問題(要介紹也是Ctrl CV (:?? )蔬啡,想了解的同學可自行Google~ 本文只記錄筆者工作中遇到的事務問題诲侮,以及解決方案。
在工作中星爪,事務問題是比較常見的浆西,同時也是比較危險的,稍一不注意就會背P0事故顽腾。那我們在工作中要如何解決事務問題近零,保證業(yè)務安全運行呢?
試想一個業(yè)務場景:用戶在電商網(wǎng)站下單某個商品抄肖,這時需要進行兩個操作:
- 更改訂單狀態(tài)
- 將商品從購物車中移除
如果操作1成功了久信,操作2卻失敗。這時用戶看到商品仍在購物車中漓摩,以為沒有下單成功裙士,又再次點擊下單,造成重復訂單管毙。
如果操作1失敗了腿椎,操作2卻成功。這時顯示下單失敗夭咬,但是購物車中的商品被移除了啃炸,用戶對此也會產生疑惑。
綜上卓舵,操作1南用、2 只能同時成功,或者同時失敗掏湾。 否則就會出現(xiàn)各種想象不到的異常情況裹虫。
那要怎么保證呢?這時候就需要事務了
1. 數(shù)據(jù)庫事務
這應該算是最簡單的事務問題了融击,因為常用的數(shù)據(jù)庫本身也支持事務操作筑公。
針對以上場景,可以編寫偽代碼:
// 開啟一個事務
tx := db.Begin()
// 1.更改訂單狀態(tài)
tx.updateOrderStatus(xxxx)
// 2.將商品從購物車中移除
tx.removeItemFromShoppingCart(xxxx)
// 出現(xiàn)錯誤砚嘴,回滾操作 1十酣、2
if err != nil {
tx.rollback()
return
}
//操作1、2都運行成功际长,提交事務
tx.commit()
通過偽代碼不難發(fā)現(xiàn),數(shù)據(jù)庫事務只適用于操作同一個DB兴泥,但現(xiàn)實的項目中工育,往往是以微服務劃分各自的職責,訂單服務和購物車服務甚至都歸屬于不同的團隊搓彻,更別說用同一個數(shù)據(jù)庫了如绸。
這時候就需要使用分布式事務了
2. 事務消息
還是上文的場景嘱朽,雖然訂單服務和購物車服務歸屬于不同的服務,但是服務之間的協(xié)作可以通過消息隊列實現(xiàn)怔接。
簡單來說搪泳,就是當更新訂單狀態(tài)成功之后,發(fā)送一條消息給購物車服務扼脐,然后購物車服務執(zhí)行移除操作
這樣岸军,乍一看好像沒什么問題,但深入思考之后會有幾個疑問:
- 訂單狀態(tài)更新成功了瓦侮,但消息發(fā)送失敗艰赞,要如何處理?
- 消息發(fā)送成功了肚吏,要怎么保證購物車服務一定能消費到方妖?
概括成一句話:如何保證消息的生產端和消費端的事務性
2.1 事務消息 - 生產端
還是以用戶下單場景解析事務消息是如何發(fā)送的:
- producer發(fā)送訂單狀態(tài)已更新的消息
- 消息發(fā)送成功
- 執(zhí)行本地事務,這時真正更新訂單的狀態(tài)
- 本地事務執(zhí)行成功罚攀,則提交該事務消息党觅,失敗則回滾消息。
但是考慮到網(wǎng)絡的原因斋泄,在發(fā)送commit或rollback的消息丟失了杯瞻,broker接收不到信息,無法進行下一步操作是己。
對于這個問題很好解決又兵,如步驟5:在Producer端提供一個回查的接口,供Broker定期回查本地事務的狀態(tài)卒废。然后可以根據(jù)反查結果決定回滾還是提交事務沛厨。
2.2 事務消息 - 消費端
話說大部分文章在介紹事務消息時都只側重于生產端,對消費端一筆帶過甚至提都不提但其實在實戰(zhàn)中摔认,如何實現(xiàn)高效逆皮、正確的消費端也是一大難題。
想問大家一個問題参袱,在消費端电谣,是先消費消息再提交commit?還是先提交commit 再消費消息呢抹蚀?
其實這兩種方式?jīng)]有對錯之分剿牺,只是在不同業(yè)務下的選擇。
我們就以這兩種方式來介紹如何實現(xiàn)消費端的“事務性”
1. 先消費消息再提交commit
err := handle(msg)
if err!=nil{
return err
}
consumer.commit(msg)
這種消費方式本身也具備事務性了环壤,因為只有消息消費成功晒来,才提交偏移量,如果消費失敗郑现,Broker則會重新投遞湃崩。(多次投遞失敗荧降,則會發(fā)送死信隊列)
但這種方式也有兩個缺點:
- Broker可能會多次投遞,造成重復消費攒读,所以消費者要實現(xiàn)好冪等邏輯
- 先消費再提交commit意味著不能異步多線程消費朵诫,消費速度較慢
2. 先提交commit再消費
consumer.commit(msg)
go handle(msg)
這種實現(xiàn)方式可以運用多線程異步消費,較于方式1能極大提升消費速度薄扁。但是同時也帶來了隱患剪返。
因為Broker只投遞一次消息,所以處理失敗case只能由業(yè)務自己去重試泌辫。
通用的方案是設計重試隊列随夸,當業(yè)務邏輯處理失敗時,交由重試隊列去處理震放,當重試超過一定次數(shù)宾毒,則需要告警人為干預。
注:這種實現(xiàn)方式殿遂,不能保證最終一致性诈铛,在極端情況下仍會出現(xiàn)不一致的情況。
對于事務消息的消費端墨礁,兩種實現(xiàn)方式都各有利弊幢竹,要深入業(yè)務調研,從而做出最好的選擇恩静。
對于分布式事務的解決方案焕毫,上文介紹了事務消息,對于這種方案驶乾,能保證最終的結果是可靠的邑飒,過程也非常簡單易理解。但是整個過程完全沒有任何隔離性可言级乐。
對于訂單和購物車的場景疙咸,對隔離性要求不高,所以使用事務消息來解決該種場景是非常合適的风科。
但是對于另一個場景:用戶下單某個商品撒轮,對應兩個操作:
- 更改訂單狀態(tài)
- 扣減商品庫存
如果使用缺乏隔離性的事務消息來處理該場景,會帶來一個顯而易見的問題“超賣”贼穆。
因為兩個客戶完全有可能在短時間內都成功購買了同一件商品题山,而且他們各自購買的數(shù)量都不超過目前的庫存,但他們購買的數(shù)量之和卻超過了庫存故痊。
所以就需要使用隔離性更強的分布式事務方案 -- TCC 事務來處理臀蛛。
3. TCC
在具體實現(xiàn)上,TCC 較為煩瑣崖蜜,它是一種業(yè)務侵入式較強的事務方案浊仆。要求業(yè)務處理過程必須拆分為“預留業(yè)務資源”和“確認/釋放消費資源”兩個子過程。如同 TCC 的名字所示豫领,它分為(Try抡柿、Confirm、Cancel)三個階段等恐。
- Try:嘗試執(zhí)行階段洲劣,完成所有業(yè)務可執(zhí)行性的檢查(保障一致性),并且預留好全部需用到的業(yè)務資源(保障隔離性)课蔬。
- Confirm:確認執(zhí)行階段囱稽,不進行任何業(yè)務檢查,直接使用 Try 階段準備的資源來完成業(yè)務處理二跋。Confirm 階段可能會重復執(zhí)行战惊,因此本階段所執(zhí)行的操作需要具備冪等性。
- Cancel:取消執(zhí)行階段扎即,釋放 Try 階段預留的業(yè)務資源吞获。Cancel 階段可能會重復執(zhí)行,也需要滿足冪等性谚鄙。
業(yè)務時序圖:
訂單服務發(fā)起事務請求各拷,庫存服務&積分服務預留業(yè)務資源(凍結庫存、預添加積分)
Try階段全部成功闷营,完成業(yè)務操作(扣減庫存烤黍,為會員添加積分)
Try階段有操作失敗或超時,取消業(yè)務操作(釋放庫存傻盟、取消添加積分)
總結
分布式事務有多種解決方案速蕊,同一種方案,根據(jù)業(yè)務的不同也有不同的實現(xiàn)方式莫杈。所以要深入業(yè)務互例,選擇一個最合適的方案。