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

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

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

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

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

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





























