{{ v.name }}
{{ v.cls }}類
{{ v.price }} ¥{{ v.price }}
分布式系統下的數據一致性可以分為兩大類:
定義都比較抽象,舉個例子感受一下:
圖片
【注】本文著重介紹 “事務一致性”,多副本一致性,詳見 緩存 或 ES 篇。
在關系型數據庫中,事務(Transaction)是指一組數據庫操作,這些操作要么全部成功要么全部失敗。事務可以保證某些數據操作的一致性,當某一條操作失敗時,會進行回滾,即撤銷已執行的操作,使數據恢復到操作前的狀態。
提到事務一致性,不得不說數據庫事務 ACID:ACID是指數據庫事務的四個關鍵特性,分別為原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability):
銀行轉賬應用程序就是典型的 ACID 模型的應用場景。假設用戶A要向用戶B轉賬1000元,轉賬過程就是一個事務,具有原子性、一致性、隔離性和持久性四大特性:
數據庫事務絕對是程序員的一大利器,但由于各種原因,這把利器離我們越來越遠:
垂直拆分:將不同的表放到不同的數據庫實例,比如拆分出 User 實例,Order 實例;
圖片
水平拆分:數據量超過單表最大容量時,將數據分拆到不同的數據庫,比如 Order-1 實例、Order-2 實例;
圖片
垂直+水平拆分:先進行垂直拆分,在進行水平拆分;
圖片
微服務的“自治”要求每個微服務都應該有自己的獨立數據存儲,避免與其他服務共享數據存儲,從而降低服務之間的耦合性;
微服務間通過服務發現、負載均衡等方式,將服務之間的關系解耦,從而使得每個服務都具備獨立的自治性;
圖片
不管觸發哪一種條件,都會產生跨數據庫事務,從而增加系統設計的難度。
針對該問題前人已經提出來多種應對方案,特別是關系型數據庫。
熟悉 MySQL 實現的伙伴知道,MySQL 是通過 Redo log 和 Undo log 來實現事務一致性的:
具體的如下圖所示:
圖片
從圖中可知:
Redo log 記錄正向修改;
Undo log 記錄逆向恢復;
其中,可以看出存在兩個核心流程:
除了兩種補償機制外,還涉及一個重要的組件“補償管理器”,用于對補償機制進行統一協調。
2PC(Two-Phase Commit)和XA是分布式事務中常用的協議和接口:
MySQL 采用了兩階段提交(Two-Phase Commit,簡稱 2PC)協議,保證 Redolog 和 Binlog 間的數據一致性,確保事務在所有相關節點(包括 Redolog 和 Binlog)執行的情況下,要么全部提交成功,要么全部回滾失敗。
2PC只能應用于兩個事務參與者的場景,而XA可以應用于多個事務參與者的場景,具體如圖所示:
圖片
XA 定義了一組接口:
對應的事務提交和回滾流程如下:
2PC (包括升級后的 3PC),在事務執行的整個流程中都需要對資源進行鎖定,在分布式環境下將大幅增加系統響應時間,降低整個系統的吞吐,在實際工作中使用的非常少。
TCC 是實現分布式事務解決方案的一種有效方法,更是真正應用于實際工作的一大解決方案。
圖片
TCC (try-confirm-cancel) 是一種分布式事務解決方案,它將一個分布式事務拆分成三個過程:
TCC 的操作流程如下:
TCC 是一種補償型事務機制,通過人工干預來處理異常,本身具備極佳的靈活性,適用于各種不同類型的應用場景。
看了不少一致性解決方案,不知道有沒有發現一些規律?
核心組件基本一致:
核心流程基本一致:
簡單來說:事務一致性就是通過協調各個參與節點來實現分布式事務的提交或回滾,確保所有涉及到的操作,要么全部執行成功,要么全部不執行。不同的實現方式只是不同的工具,其實現思路基本一致。
前人已經為我們提供足夠多的工具,如何更好的使用這些工具,就需要對業務場景進行深入分析。
業務系統一致性是指在多個系統或不同的環境中,不同用戶或系統操作所產生的數據在邏輯上是相同的。它的本質是確保在任何情況下,不同系統或用戶產生的數據都是一致的,并且在系統中的所有操作都是以預期方式進行的。業務系統一致性是確保數據的準確性和可靠性的關鍵因素,可以有效地避免數據錯誤和丟失,提高業務系統的可用性和可靠性,保障企業的持續發展。\\如下圖所示:
圖片
如果可重試性事務間不存在依賴關系,可以并行執行,具體如下:
圖片
在一個復雜的業務流程中,可以將事務分為三類:
我們以分布式系統中的下單流程為例:
圖片
關鍵性事務:指在分布式系統中,只有當某個事務被成功提交后,整個系統才能認為這個事務是成功的。如果這個事務失敗了,那么整個系統就會回滾到之前的狀態。例如支付、訂單提交等。
從關鍵性事務的使用場景出發,最適合的工具便是關系數據庫的事務保障。
圖片
可補償事務指在某些業務操作中,如果其中一些子操作執行失敗,可以由后續補償操作進行補救,達到一定的業務目的,例如在資金交易中,如果賬戶余額不足而支付子操作失敗,可以通過撤銷訂單等補償操作來保障交易的正確性。
對于可補償事務,需要提供兩組操作:
Seata 是一個開源的分布式事務解決方案,旨在解決分布式系統中的事務一致性問題。在傳統的分布式系統中,由于各個服務之間的數據交互和操作都是獨立進行的,因此很容易出現數據不一致的情況。這會導致系統出現各種異常情況,如數據丟失、重復提交等,從而影響系統的穩定性和可靠性。
Seata 提供了多種解決方案來解決分布式事務一致性問題。其中包括 XA 模式、TCC 模式和 SAGA 模式等。
Seata 還提供了一些重要的功能,如事務日志記錄、故障恢復、動態擴展等,使得用戶可以更加方便地使用該框架來解決分布式事務一致性問題。同時,Seata 還具有高性能、高可用性和易用性等特點,可以滿足各種不同場景下的需求。
【注】感興趣的話,可以找下 seata 的官方文檔。
Seata 雖好,但中間件的引入將大幅提升系統的復雜性,對于一些不太嚴謹的場景或者一些運維能力不足的小團隊可以自己實現回滾方案。
整體方案如下:
圖片
關鍵事務提交成功,Context 注冊的 RollbackEntry 便失去意義;
關鍵事務提交失敗,調用 Context 的 fireFallback 方法進行逆向補償,fireFallback 方法逆向調用注冊的回滾方法,從而恢復業務狀態
該方案基于內存實現,存在失靈的情況,不建議使用在嚴謹的場景。
可重試型事務指在業務操作中,如果某些操作由于網絡波動等原因導致失敗,可以通過重新執行這些操作來達到其預期的結果,例如在發送短信驗證碼時,由于網絡狀況不佳而發送失敗,可以重新嘗試發送,直到發送成功為止。
可重試性事務沒有失敗,只有成功,哪怕是短暫的失敗也會通過不限的重試使其最終達到成功狀態。
@Retry 是 Spring 框架提供的一個注解,用于在方法調用失敗時自動進行重試。
通過 @Retry 注解,我們可以定義重試的次數、間隔時間和異常類型等信息,從而實現更可靠的方法調用。
具體來說,@Retry 注解可以通過以下屬性來配置:
我們看下具體的使用:
@Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public void doSomething() throws Exception { // 業務邏輯代碼 }
該實現會在方法調用失敗時進行最多3次的重試,每次重試之間會等待1秒的時間。如果超過3次重試仍然失敗,則拋出異常。
@Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000), fallback = @Fallback(fallbackMethod = "doDefault")) public void doSomething() throws Exception { // 業務邏輯代碼 }
private String doDefault(Exception e) { // 當出現指定異常時,執行該方法進行重試處理 }
該實現會在方法調用失敗時進行最多3次的重試,每次重試之間會等待1秒的時間。如果超過3次重試仍然失敗,則會執行 doDefault 方法來進行重試處理。在該方法中,我們可以自定義處理方式來處理異常情況。
@Retry 仍舊是一個內存解決方案,在極端場景下可能出現任務丟失的情況。因此在實際工作中,很少用于可重試性事務這種場景。
MQ(消息隊列)消費者重試機制是指在消費消息時,如果消費者無法成功消費消息(比如網絡異常、服務器故障等原因),會自動重試一定次數或間隔一定時間后再次嘗試消費消息,以保證消息的可靠性和可用性。
如下圖所示:
im
具有MQ的可重試性事務,需要以下保障:
一般情況下會采用多次投遞的方式來實現消息投遞和消息消費之間的一致性,所以消息消費者需要保障冪等性,避免多次投遞造成的業務問題。
RocketMQ事務消息是一種支持分布式事務的消息模型,將消息生產和消費與業務邏輯綁定在一起,確保消息發送和事務執行的原子性,保證消息的可靠性。
事務消息分為兩個階段:發送消息和確認消息,確認消息分為提交和回滾兩個操作。在提交操作執行完畢后,消息才會被消費端消費,而在回滾操作執行完畢后,消息會被刪除,從而達到了事務的一致性和可靠性。
事務消息的發生流程如下:
圖片
如果生成者發送 prepare 消息后,未在規定時間內發送 commit 或 rollback 消息,RocketMQ 將進入恢復流程,具體如下:
圖片
使用 RocketMQ 的事務消息代碼示例如下:
// 編寫事務監聽器類 public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0); // 執行本地事務 public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { int value = transactionIndex.getAndIncrement(); System.out.println("executeLocalTransaction " + value); // TODO 執行本地事務,并返回事務狀態 // 本例假定 index 為偶數的消息執行成功,奇數的消息執行失敗 if (value % 2 == 0) { return LocalTransactionState.COMMIT_MESSAGE; } return LocalTransactionState.ROLLBACK_MESSAGE; } // 檢查本地事務狀態 public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("checkLocalTransaction " + msg.getTransactionId()); // 模擬檢查本地事務狀態,返回事務狀態 boolean committed = prepare(true); if (committed) { return LocalTransactionState.COMMIT_MESSAGE; } return LocalTransactionState.UNKNOW; } // 模擬操作預處理邏輯 private boolean prepare(boolean commit) {
System.out.println("prepare " + (commit ? "commit" : "rollback")); return commit; }
} // 編寫發送消息的代碼 public class Producer {
private static final String NAME_SERVER_ADDR = "localhost:9876"; public static void main(String[] args) throws Exception {
TransactionMQProducer producer = new TransactionMQProducer("MyGroup"); producer.setNamesrvAddr(NAME_SERVER_ADDR); // 注冊事務監聽器 producer.setTransactionListener(new TransactionListenerImpl()); producer.start(); // 發送事務消息 String[] tags = {"TagA", "TagB", "TagC"}; for (int i = 0; i < 3; i++) {
Message msg = new Message("TopicTest", tags[i], ("Hello RocketMQ " + i).getBytes(StandardCharsets.UTF_8)); // 在消息發送時傳遞給事務監聽器的參數 SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); } // 關閉生產者 producer.shutdown(); }
}
單看代碼很難理解,簡單畫了張圖,具體如下:
圖片
其核心部分就是 TransactionListener 實現,其他部分與正常的消息發送基本一致,TransactionListener 主要完成:
為了使用事務消息,我們不得不在TransactionListener中編寫進行大量的適配邏輯,增加研發成本,同時由于邏輯被拆分到多處,也增加了代碼的理解成本。
事務消息存在一定的問題:
有沒有實用性強、使用簡單的方案,那可以使用 事務消息表 方案。
事務消息表方案是一種常用的保證消息發送與業務操作一致性的方法。該方案基于數據庫事務和消息隊列,將消息發送和業務操作放入同一個事務中,并將業務操作和消息發送的狀態記錄在數據庫的消息表中,以實現消息的可靠性和冪等性。
如下圖所示:
圖片
核心流程如下:
通過事務消息表方案,可以保證消息的可靠性和冪等性。即使在消息發送失敗或應用程序崩潰的情況下,也可以通過重新發送消息將業務操作和消息發送的狀態同步。同時,該方案可以避免消息重復發送和漏發的情況。
作為一種通用解決方案,lego 對其進行支持,可參考 reliable-message 模塊。
不管在設計時使用哪種方案,都是在盡力降低不一致出現的概率,但可怕的是不一致問題終究會發生。
是不是有些奇怪,做了這么多還是無法從根源上徹底解決一致性問題,在實際工作中就是這樣:
除了主動降低不一致性概率,還需要添加一些被動保護機制,也就是常說的業務補償。
查詢模型是最常用的一種方式,主要用于應對網絡傳輸中的第三態問題。
第三態指的是在分布式系統中,在進行跨網絡調用時,調用方無法確定被調用方的狀態是否改變了,因為這兩者之間存在一段未知而不可控的網絡延遲時間,導致調用方無法立即得到被調用方的結果。這種情況下,第三態可以看做是一個未知的狀態,需要通過一些機制來解決這個問題。
圖片
當網絡調用出現第三態時,最簡單的方式便是對不確定的狀態進行查詢,如上圖所示:
已完成,則繼續執行后續流程;
未完成,在重新發起業務調用;
RocketMQ 的事務消息便是基于該機制進行實現。
當一個業務操作完成后,需要處理多個后續任務,為了保障所有任務都會被執行,可以使用該模式。
如下圖所示:
圖片
image
已經執行,則更新任務狀態
如果未執行,則觸發任務執行
本地消息表就是基于該模式進行構建。
對賬模式經常出現在與銀行等金融機構對接的場景。
圖片
業務對賬思路非常簡單:
一致,則說明系統一致
不一致,進行報警,人工介入進行處理
必須是雙向對賬,單向對賬會出現數據丟失情況。
一致性是分布式系統面臨的巨大挑戰,根據不同場景可以將一致性分為:
本文重點對事務一致性進行全方位的闡述,包括:
MySQL 實現
2PC和XA協議
TCC 解決方案
有了這些方案后,很多場景下仍需落地業務補充,常見方案包括: