分布式數據一致性場景與方案處理分析
目錄
一、引言
二、CAP理論與BASE理論
三、一致性失效場景及其解決方案
1. 調用寫RPC
2. 消息發(fā)送
3. 本地消息表
四、總結
一、引言
在經典的CAP理論中一致性是指分布式或多副本系統中數據在任一時刻均保持邏輯與物理狀態(tài)的統一,這是確保業(yè)務邏輯正確性和系統可靠性的核心要素。在單體應用單一數據庫中可以直接通過本地事務(ACID)保證數據的強一致性。
然而隨著微服務架構的普及和業(yè)務場景的復雜化,原來的原子性操作會隨著系統拆分而無法保障原子性從而產生一致性問題,但業(yè)務實際又需要保障一致性,為此BASE理論提出了最終一致性來解決這類問題。那么如何在跨服務、跨數據庫的事務中保證數據最終一致性。
二、CAP理論與BASE理論
在經典的CAP理論中提到一個分布式系統中,一致性(C)、可用性(A)、分區(qū)容錯性(P)最多只能同時實現兩點,不可能三者兼顧。實際上這是一個偽命題,必須從 A 和 C 選擇一個和 P 組合,更進一步基本上都會選擇 A,相比一致性,系統一旦不可用或不可靠都可能會造成整個站點崩潰,所以一般都會選擇 AP。
圖片
BASE理論源于對大規(guī)?;ヂ摼W分布式系統實踐的總結,作為CAP定理中一致性與可用性矛盾的實踐性補充逐步演化形成。該理論主張在無法保證強一致性的場景下,系統可基于業(yè)務特性靈活調整架構設計,通過基本可用性保障、允許短暫中間狀態(tài)等機制,確保數據最終達成一致性狀態(tài),從而在分布式環(huán)境中實現可靠服務能力與業(yè)務需求的平衡。
三、一致性失效場景及其解決方案
這里有一個簡化的倉庫上架的流程(在實際業(yè)務中可能還會涉及到履約,倉儲庫存等等),體現分布式系統中可能出現的一致性問題,在分布式系統中的處理流程可能如下所示:
操作員操作商品倉庫上架
商品在倉儲系統(WMS)中上架,寫入倉儲數據庫
倉儲系統通知中央庫存系統(SCI)添加可用庫存
倉儲系統通知交易該商品可以進行售賣
多服務協作交互示例
簡化代碼示例:
@Transactional
public void upper(upperRequest request) {
// 1. 寫入倉儲數據庫
UpperDo upperDo = buildUpperDo(request);
wmsService.upper(upperDo);
// 2. 調用rpc添加中央庫存系統庫存
SciAInventoryRequest sciInventoryRequest = buildSciAInventoryRequest(request);
sciRpcService.addInventory(sciInventoryRequest)
// 3. 發(fā)送商品可以售賣的消息
TradeMessageRequest tradeMessage = buildTradeMessageRequest(request);
sendMessageToDealings(tradeMessage);
// 4. 其他處理
recordLog(buildLogRequest(request))
return;
}
整個時序邏輯拆解到事務層面執(zhí)行流程如下:
發(fā)送消息
在第5步添加sci庫存之前任意一步出現問題,事務都會回滾,對其他系統的影響為0,所以不存在一致性問題。
但是,在此之后出現問題都有可能會出現事務問題。
調用寫RPC
在分布式系統中,調用RPC一般可以分為著兩類:
1.讀RPC:當前數據結構不完整,需要通過其他服務補充數據,對其他服務無影響。
2.寫RPC:當前業(yè)務操作、數據變更需要通知其他服務,對其他服務有影響。
調用寫RPC添加sci可用庫存可能出現的問題:
- 調用處理成功,返回成功?!緮祿恢隆?/li>
- 調用處理成功,返回失敗?!緮祿灰恢隆?/li>
對于這種情況,最簡單的做法是直接操作重試,但是需要下游冪等處理,保證同樣的請求效果一致。這里重試的方式,即重新操作上架,此外也可以直接在rpc方法中異步重試機制(這種方式不會阻塞整體流程,但是增大了數據不一致的風險)。如果重試失敗可能需要研發(fā)介入排查具體失敗的原因(對于寫RPC的接口超時問題,需要研發(fā)關注,配置告警或拋出特定異常等)。
針對RPC方法重試,可以考慮采用本地消息表的方式實現,具體參考3.3.本地消息表。
消息發(fā)送
寫RPC調用成功后,會給trade服務發(fā)送消息,而后提交事務,整個流程結束。
Rocket消息發(fā)送有多種方式,不同的方式適用場景不一,一般業(yè)務邏輯使用同步發(fā)送消息配合重試機制即可,對于一致性要求高的場景,可以考慮事務消息確保消息與本地事務的原子性。
圖片
同步消息+重試
同步消息比異步消息更可靠,比事務消息性能更高是一種廣泛采用的方式。
同步消息通過confirm機制能保證消息發(fā)送成功:生產者發(fā)送同步消息后,等待Broker返回確認結果(SendResult)。如果 Broker 成功接收并存儲消息,返回成功狀態(tài);否則返回失敗狀態(tài)。消息發(fā)送失敗時,Rocket默認自動重試2次,支持手動設置,提高消息發(fā)送的可靠性。
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
producer.setRetryTimesWhenSendFailed(3); // 設置重試次數為 3 次
producer.start();
Message msg = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes());
SendResult sendResult = producer.send(msg); // 同步發(fā)送
if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
log.info("Send Success: " + sendResult);
} else {
log.warn("Send Failed: " + sendResult);
}
同步消息+重試機制能盡可能的保證消息成功發(fā)送,但是在這種情況下仍可能出現一致性問題:消息成功發(fā)送,在提交事務之前,依然可能出現問題(第8步出現問題),導致事務回滾,但是下游的消息是無法回滾的。
為此在RocketMQ中提供了事務消息作為一種解決方案。
RocketMQ事務消息
RocketMQ 的分布式事務消息功能,在普通消息基礎上,支持二階段的提交能力。將二階段提交和本地事務綁定,實現全局提交結果的一致性。
發(fā)送事物消息
Rocket的事務消息可以確保消息和本地事務的原子性,但是實現起來很復雜,性能也比較低,特別是需要實現回查本地事務狀態(tài),這是一個比較復雜的問題,需要case by case,每一個消息都需要單獨寫邏輯,還必須確保消息體中的數據支持回查本地事務狀態(tài),對代碼入侵度較高。
在筆者的了解中我司事務消息的使用情況不多,對于低并發(fā)且強一致性的場景可以考慮使用這種方式。在這個業(yè)務場景中使用事務消息可以解決3.2.1中出現的消息發(fā)送成功但事務回滾的問題,但是這個場景使用這種方式并不太合適。最終結果可能是整體數據一致性提升2%-3%,但是業(yè)務性能下降20%-30%。
spring提供給了一種事件發(fā)布-訂閱機制可以解決事務回滾但消息依然發(fā)送成功的問題,并且性能損失幾乎可以忽略。
事務事件+同步消息
事務事件是指在事務執(zhí)行的不同階段觸發(fā)的事件。這些事件通常用于處理次要邏輯,例如發(fā)送領域事件、消息或者郵件等。
spring通過事務管理@Transactional和事件發(fā)布機制ApplicationEventPublisher,可以實現類似事務事件的功能。事件發(fā)布后事件廣播器(SimpleApplicationEventMulticaster)接收事件,根據事件類型匹配所有的監(jiān)聽者(getApplicationListeners)。
@Service
public class wmsService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void upper(upperRequest request) {
// 1. 寫入倉儲數據庫
UpperDo upperDo = buildUpperDo(request);
wmsService.upper(upperDo);
// 3. 發(fā)布上架事件
UpperFinishEvent upperFinishEvent = buildUpperFinishEvent(request)
eventPublisher.publishEvent(upperFinishEvent);
return;
}
}
@Component
public class upperFinishEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUpperFinishEvent(UpperFinishEvent event) {
// 處理事件
// 1. 調用rpc添加中央庫存系統庫存
SciAInventoryRequest sciInventoryRequest = buildSciAInventoryRequest(event);
sciRpcService.addInventory(sciInventoryRequest)
// 2. 發(fā)送商品可以售賣的消息
TradeMessageRequest tradeMessage = buildTradeMessageRequest(event);
sendMessageToDealings(tradeMessage);
// 2. 其他處理
recordLog(buildLogRequest(event))
}
}
上述流程在寫完DB,調用寫RPC之后,發(fā)布上架完成的事件并提交事務。upperFinishEventListener訂閱上架完成的事件,并發(fā)送可以售賣的消息。
通過這種方式可以在事務提交之后再發(fā)送消息。通過事務事件保證事務提交,通過重試機制和confirm機制確保生產者發(fā)送消息成功。
本地消息表
在上述過程中我們選擇使用事務事件+同步消息可以來替代事務消息,但是事務事件對RPC調用并不太友好,本地事務提交之后,調用寫RPC就一定要成功,不然一致性問題就無法保證。
為此可以考慮使用本地消息表這個方案:將需要分布式處理的事件通過本地消息日志存儲的方式來異步執(zhí)行,通過異步線程或者自動Job發(fā)起重試,確保上下游一致。
圖片
將上述流程抽象為代碼可以實現一個一致性框架,通過注解實現無侵入、策略化、通用性和高復用性的能力。然后本地消息表的方式仍然存在一些問題:
- 高并發(fā)場景不適用,寫本地消息會帶來延遲可能出現數據積壓,影響系統的吞吐量。
- 業(yè)務邏輯過程會長時間的占用事務,造成大事務問題。
- 本地消息報文巨大,難以存儲等。
四、總結
本文分析的場景都是解決生產者端的一致性問題。結合部分場景探討不同方式的優(yōu)缺點。
- 事務事件+普通消息&重試 :適合對實時一致性要求不高、需要異步處理的場景、適合高并發(fā)場景,可靠性一般,實現簡單但需手動處理重試和冪等性。
- 事務消息 :適合一致性要求較高的場景(如金融交易),性能較低,實現復雜但能確保消息與事務的原子性。
- 本地消息表 :適合跨服務事務、異步任務處理和最終一致性場景,高并發(fā)場景可能出現數據積壓,實現簡單且可靠性高,但存在延遲性和資源占用問題。
在分布式系統中,很難有能100%保證一致性的方案,正如《人月神話》中說的“沒有不存在缺陷的軟件,只是尚未發(fā)現缺陷”。
在上面提到的各種方案中,筆者所在團隊高并發(fā)場景很少,所以一般都采用本地詳細表的方式來處理一致性問題,這既可以處理寫RPC的調用問題,也能通過消息狀態(tài)顯示的統一失敗情況,統一進行重試。