在分布式微服務(wù)架構(gòu)應(yīng)用中如何實現(xiàn)最終一致性?
在分布式系統(tǒng)中,實現(xiàn)強一致性并不容易。即使2PC、3PC階段提交,也無法保證絕對的強一致性。
我們也不能因為極小的不一致性概率,導(dǎo)致系統(tǒng)整體性能低下,或者擴展性受到影響,并且架構(gòu)也變得極其復(fù)雜。因此,在2PC/3PC提交缺乏大規(guī)模應(yīng)用的情況下,最終一致性是一個較好的方案,在業(yè)界得到了大量使用。
一、重試機制
如下圖所示,Service Consumer 同時調(diào)用 Service A 和 Service B,如果Service A 調(diào)用成功,Service B 調(diào)用識別,為了保證最終一致性,最簡單的辦法是重試。

重試的時候,要注意設(shè)置Service Consumer 的超時時間, 避免長時間等待或卡死,耗盡資源。
Consumer 重試時,需要注意如下幾個方面:
- 超時時間;
- 重試的次數(shù);
- 重試的間隔時間;
- 重試間隔時間的衰減度;
具體實現(xiàn)細節(jié),可以參考《 基于Spring-tryer 優(yōu)雅的重試方案》。
二、本地記錄日志
通過本地記錄日志,然后收集到分布式監(jiān)控系統(tǒng)或者其他后端系統(tǒng)中,啟動一個定期檢查的工具。根據(jù)實際情況,可以選擇人工處理。
日志格式:TranID-A-B-Detail
- TransID為事務(wù)ID,可以生成一個隨機序列號;
- Detail 為數(shù)據(jù)的詳細內(nèi)容;
- 如果調(diào)用A成功,則記錄 A success;
- 如果調(diào)用B失敗,或者出現(xiàn)故障,沒有記錄等等,也就是日志中沒有B success,則重新調(diào)用B;
- 可以定期檢測,并處理日志。
收集識別日志的設(shè)計圖,如下所示。

三、可靠消息模式
考慮到實際業(yè)務(wù)場景中發(fā)生故障的概率概率比較低,可以考慮如下方案。
Service Consumer 在調(diào)用 Service B 失敗,先進行重試。如果重試一定的次數(shù)仍然失敗,則直接發(fā)送消息Message Queue,轉(zhuǎn)換為異步處理。
可以采用分布式能力比較強的MQ,如Kafka、RocketMQ等開源分布式消息系統(tǒng),進行異步處理。
- Service B 可以專門集成一個錯誤處理的組件,不斷從MQ 收集補償消息。
- 或者獨立一個錯誤處理的組件,獨立處理MQ 的補償消息,包括其他Service 組件的異常。

這種方案也有丟失消息的風(fēng)險,就是Service Consumer 的消息還沒有發(fā)出來就掛了,這是小概率事件。
還有一種方案-可靠消息模式,如下圖所示。Service Consumer 發(fā)送一條消息給Message Queue Broker,如RocketMQ、Kafka等等。由Service A和Service B 消費消息。
MQ 可以采用分布式MQ,并且可以持久化,這樣通過MQ 保證消息不丟失,認為MQ 是可靠的。

可靠消息模式的優(yōu)點:
- 提升了吞吐量;
- 在一些場景下,降低了響應(yīng)時間;
存在問題:
- 存在不一致的時間窗口(業(yè)務(wù)數(shù)據(jù)進入了MQ,但是沒有進入DB,導(dǎo)致一些場景讀不到業(yè)務(wù)數(shù)據(jù));
- 增加了架構(gòu)的復(fù)雜度;
- 消費者(Service A/B)需要保證冪等性;
針對上述不一致的時間窗口問題,可以進一步優(yōu)化。
- 將業(yè)務(wù)分為:核心業(yè)務(wù)和從屬業(yè)務(wù)
- 核心業(yè)務(wù)服務(wù) - 直接調(diào)用;
- 從屬業(yè)務(wù)服務(wù) - 從MQ 消費消息;

直接調(diào)用訂單服務(wù)(核心服務(wù)),將業(yè)務(wù)訂單數(shù)據(jù)落地DB;同時,發(fā)送向MQ 發(fā)送消息。
考慮到在向MQ 發(fā)送消息之前,Service Consumer(創(chuàng)建訂單)可以會掛掉,也就是說調(diào)用訂單服務(wù)和發(fā)送Message 必須在一個事務(wù)中,因為處理分布式事務(wù)比較麻煩,且影響性能。
因此,創(chuàng)建了另外一張表:事件表,和訂單表在同一個數(shù)據(jù)庫中,可以添加事務(wù)保護,把分布式事務(wù)變成單數(shù)據(jù)庫事務(wù)。
整個流程如下:
(1)創(chuàng)建訂單 - 持久化業(yè)務(wù)訂單數(shù)據(jù),并在事件表中插入一條事件記錄。注意,這里在一個事務(wù)中完成,可以保證一致性。如果失敗了,無須關(guān)心業(yè)務(wù)服務(wù)的回退,如果成功則繼續(xù)。
(2)發(fā)送消息 - 發(fā)送訂單消息到消息隊列。
- 如果發(fā)送消息失敗,則進行重試,如果重試成功之前,掛掉了,則由補償服務(wù)去重新發(fā)送消息(小概率事件)。
- 補償服務(wù)會不斷地輪詢事件表,找出異常的事件進行補償消息發(fā)送,如果成功則忽略。
- 如果發(fā)送消息成功,或者補償服務(wù)發(fā)送消息成功,則可以考慮刪除事件表中的事件信息記錄(邏輯刪除)。
(3)消費消息 - 其他從屬業(yè)務(wù)服務(wù),則可以消費MQ中的訂單消息,進行自身業(yè)務(wù)邏輯的處理。
上述設(shè)計方案中,有3點需要說明一下:
(1)直接調(diào)用訂單服務(wù)(核心業(yè)務(wù)),是為了讓業(yè)務(wù)訂單數(shù)據(jù)盡快落地,避免不一致的時間窗口問題,保證寫后讀一致性。
(2)創(chuàng)建訂單業(yè)務(wù)直接發(fā)送消息給MQ,是為了增加實時性,只有異常的情況,才使用補償服務(wù)。如果對實時性要求不高,也可以考慮去掉Message 直接發(fā)送的邏輯。
(3)額外引入一張事件表,是為了將分布式事務(wù)變成單數(shù)據(jù)庫事務(wù),在一定程度上,也增加了數(shù)據(jù)庫的壓力。