分佈式事務實戰:分佈式環境下的最終一致性與消息事務

摘要:CAP 理論中的強一致性與可用性的告訴我們兩者不可兼得,並由此催生出了 BASE 理論,將強一致性和可用性弱化爲最終一致性和基本可用性。本文主要敘述筆者對最終一致性實現的理解,希望對大家有幫助。


1 - 分佈式事務

在單機應用上,我們使用事務是很方便的,因爲所有的業務邏輯都在本地,數據庫事務就能解決 ACID 問題,特別是使用一些J2EE的框架,每一層的業務邏輯都給我們安排得妥妥當當的。

當系統已經被拆分部署到多個服務器實例上時,一般每個服務器都只負責維護一個子系統一張/數張表。與單機相比,業務還是那個業務,但從直接調用本地的下層服務變成了一個遠程的RPC調用。

在分佈式環境下,一個遠程調用是不可靠的(因爲網絡是不可靠的),我們無法保證在一臺服務器上發出的請求一定能在另一臺服務器上執行成功,也無法保證執行結果能夠準確/準時地返回。有可能被調用的服務執行失敗,也有可能執行成功,只是報文丟失。因此導致數據不一致,這些實際上也是分佈式事務的問題。

常見的分佈式事務方案有:

  1. 兩階段提交
  2. TCC(Try/Confirm/Cancel)
  3. 消息事務+最終一致性

可以參考這篇高點擊率文章——高併發下分佈式事務的解決方案,文章篇幅較短,但比較全面,可以簡單瞭解這幾種方案。

下面會介紹“消息事務+最終一致性”的分佈式事務方案,給出我們設計階段的細節流程供大家參考。

2 - 消息事務與最終一致性的設計

最終一致性不要求系統的數據實時一致,允許數據同步之間存在時延,只要保證最終返回給用戶的一定是最新的數據即可。

我們解決分佈式事務的核心是將分佈式事務轉化爲本地事務,實現的核心組件是消息中間件/消息隊列

2.1 場景

以我們實訓課做的互聯網教學網站爲例。用戶可以根據喜好購買視頻資源,這會觸發以下幾個動作:

  1. 用戶點擊“購買”,發起一條創建訂單的請求。
  2. “訂單”服務接受到請求,往本地數據庫插入一條訂單記錄,訂單狀態爲“待支付”。
  3. 瀏覽器重定向到支付頁面,用戶輸入相關支付信息,點擊“支付”,發起支付請求。
  4. “支付”服務接受請求後,確認支付成功後,向“訂單”服務發起請求,修改本地數據庫的訂單狀態爲“已支付”,同時向“課程”服務發起請求,將用戶添加到該課程的成員名單下,也是往本地數據庫插入一條記錄。
  5. 瀏覽器重定向到購買成功的頁面,結束。

這是我們互聯網電商平臺比較常見的場景了,相信多數人即使沒有做過,對這個流程也會一定了解。

其中的步驟 4 就涉及到分佈式事務,“支付成功”、“訂單狀態更新”、“課程名單修改”這三個動作必須一起完成/回滾。
在這裏插入圖片描述

2.2 @Transactional的本地事務管理

在理想的情況下,我們可以簡單地使用下面的順序圖描述這個場景,這裏沒有任何分佈式事務的管理,事務管理僅僅停留在某個方法上的@Transactional,事務只對本地數據庫起作用,回滾的也只有本地數據庫。
在這裏插入圖片描述
但受網絡影響或者服務器資源問題,請求失敗是很可能的,假如某個請求丟失怎麼辦?某個下游服務宕機了怎麼辦?對於本地事務而言,已經發出的請求就像潑出去的水,覆水難收。我們必須通過分佈式事務執行補償措施,要麼重試,要麼回滾。

2.3 最終一致性的事務管理

結合消息中間件實現最終一致性。我們可以使用異步的方式完成下游服務的調用,這樣在保證事務的基礎上提高了系統的響應速度,只要消息發送成功,不需等待下游服務執行就能響應用戶。(可以看到我特意拉長了下游服務的 lifeline)
在這裏插入圖片描述
在上游服務中,我們需要保證兩點:

  1. 如果支付成功,那麼消息必鬚髮送成功。
  2. 如果消息發送不成功,支付必須回滾。

簡單來說,本地業務邏輯和消息發送必須同時成功/回滾

問:怎麼保證?
答:如果我們的消息都是直接發送到消息隊列的,比如直接調用send()方法,那麼將支付、消息發送放在一個本地事務即可。如果消息發送失敗,扔個異常出來,直接回滾本地事務。缺點是不支持多個消息發送,比如,“更新訂單狀態”的消息發送成功,“修改課程名單”的消息發送失敗,哦豁~完蛋,回滾不了,“更新訂單狀態”的消息已經發送出去了,甚至下游服務可能已經開始執行了。

我們可以引入可靠消息機制,確保無論如何都能將消息發送出去。

在本地維持一個消息發送的庫表,消息發送的時候不直接發送到消息隊列,而是向庫表插入這條消息,只要能插入就算這個消息發送成功了。如果插入失敗,回滾事務。後臺開一個線程定期掃描庫表,如果有消息滯留在庫表就嘗試發送,發送失敗就重試,發送成功就從庫表刪除消息記錄。
在這裏插入圖片描述
當然,也有極端情況,消息中間件集體掛掉了怎麼辦,消息怎麼重試都失敗。要麼停止業務,等待中間件重新上線,要麼準備消息中間件的替代品。這裏涉及到消息中間件的高可用性問題,不多做討論。推薦 最終一致性分佈式事務如何保障實際生產中99.99%高可用?裏面有提到使用降級的消息中間件方案。

對於下游服務來說,使用異步的方式,代表只要消息發送成功,我們無論如何都要保證對應的下游服務執行成功,否則就會有一致性的問題出現。

問:怎麼保證下游服務一定執行成功呢?
答:可以使用消息中間件的ACK機制,一般來說,消息中間件會提供自動/手動確認,消息被確認後纔會被移出隊列,也就是被消費掉。我們可以使用手動確認,只有下游服務執行成功才確認消息。否則,消息一直沒有被消費,消息中間件就會不斷重試,發送消息。

問:消息隊列嘗試重複發送消息,怎麼處理冪等問題
答:正如我們上面所說的,網絡是不可靠的,有可能下游服務執行成功了,但是消息隊列並沒有接收到回傳的ACK,就會導致消息隊列再次發送消息,下游服務重複收到同一條消息。保證消息的冪等性一般有兩種方法:

  1. 在業務邏輯中保證,有些業務邏輯天生就是冪等性的,比如 Redis 的 set,重複 set 一條記錄並沒有影響;但業務數據庫的庫表裏重複插入一條記錄就有可能有問題,我們可以使用主鍵,重複插入相同的主鍵數據庫會報錯;庫表字段的更新一般來說也能保證冪等性,重複更新同一個字段影響不大。
  2. 如果無法在業務中保證冪等性,可以增加一個庫表來記錄已經處理過的消息,使用唯一標識符區分消息。執行前,對照一下庫表,如果消息已經執行過就忽略它。

3 - 總結

  1. 異步調用的響應時間短,用戶體驗好。但不適用於同步調用,比如需要下游服務反饋的業務。
  2. 上游業務需要確保消息的發送、下游業務需要確保消息的消費。
  3. 具體的方案選擇還是需要根據業務場景,其他諸如兩階段、TCC的方案也有利有弊。
  4. 引入消息中間件必然增加風險,需要做好權衡。

4 - 參考

  1. 面試:如何保證分佈式數據最終一致性
  2. 高併發下分佈式事務的解決方案
  3. 拜託,面試請不要再問我TCC分佈式事務的實現原理!
  4. 最終一致性分佈式事務如何保障實際生產中99.99%高可用?

正文結束,歡迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章