微服務架構的數據一致性
微服務架構下,最好的分布式數據一致性解決方案就是盡量避免分布式事務,然而,在很多場景下,分布式事務是難以避免的。在金融、電信領域中,很多業務場景要求數據的強一致性,同時要保證服務的可擴展性和可靠性。如何保證分布式事務下的數據一致性成為微服務架構的一個重要課題和難點。
解決方案概覽
在工程領域,分布式事務的討論主要聚焦于強一致性和最終一致性的解決方案。常見的分布式事務有基于XA協議的兩階段(2PC)提交模式,以及改良版本的三階段(3PC)提交模式。JAVA事務編程接口(Java Transaction API,JTA)和Java事務服務(Java TransactionService,JTS)正是基于XA協議的實現。分布式事務包括事務管理器TM(Transaction Manager)和一個或多個支持XA協議的資源管理器RM(Resource Manager)。在微服務架構下,我們傾向使用最終一致性的方案。下面將介紹2PC模式、TCC模式、Saga模式等。
● 2PC模式,分布式事務比較典型的解決方案,但是對于微服務架構而言,可能這一方案并不適用,主要原因是不同微服務可能使用的數據存儲類型不同。如果使用的NoSQL不支持事務的數據庫,那么事務根本無法實現2PC模式。此外,2PC模式本身也存在同步阻塞、單點故障和性能問題。
● TCC模式,相比2PC模式,具有更強的靈活性和性能優勢,TCC模式本質上是基于服務層的2PC編程模式,把事務從數據庫層的事務操作邏輯抽象到業務服務層,通過服務層業務邏輯實現服務的補償模式。
● Saga模式,核心理念是將事務切分成一組依次執行的短事務,也可以理解成基于業務補償邏輯實現分布式下的高性能分布式事務模式。較之基于單一數據庫資源訪問的本地事務,分布式事務的應用架構更為復雜。在不同的分布式應用架構下,實現一個分布式事務要考慮的問題并不完全一樣,比如對多資源的協調、事務的跨服務傳播、業務的侵入性、隔離性等,實現機制也是復雜多變的。
● 可靠消息模式的解決方案是使用消息隊列進行系統間的解耦。
由上游服務發起事件,通過消息隊列傳遞到下游服務,下游服務接收到消息后進行事件消費,最終完成業務,達到數據一致。為了解決消息隊列及上下游服務的不可靠性,通常還會借助額外的事件表和定時器輔助完成數據補償。
兩階段提交模式
2PC(兩階段提交)
2PC是一個非常經典的強一致、中心化的通過原子提交來實現分布式事務一致性管理的協議。這里所說的中心化指協議中有兩類節點:
中 心 化 協 調 者 節 點 ( Coordinator ) 和 N 個 參 與 者 節 點(Participant)。當一個事務跨越多個節點時,為了保持事務的ACID特性,需要協調者統一掌控所有節點(又稱作參與者)的操作結果,并最終指示這些節點是否要把操作結果進行真正的提交或回滾。
2PC將整個事務流程分為兩個階段:準備階段(Prepare Phase)和提交階段(Commit Phase)。整個事務過程由事務管理器和參與者組成,事務管理器負責決策整個分布式事務的提交和回滾,事務參與者負責自己本地事務的提交和回滾。大部分關系數據庫,如Oracle、MySQL,都支持兩階段提交協議。下面是計算機數據庫進行兩階段提交的說明。
● 準備階段:事務管理器為每個參與者準備(Prepare)消息,每個參與者在本地執行事務,并寫本地的Undo/Redo日志,此時事務沒有被提交。(Undo日志記錄修改的數據,用于數據回滾;Redo日志記錄修改后的數據,用于提交事務后寫入數據文件)。
● 提交階段:如果事務管理器收到了參與者執行失敗或者超時的消息,則直接向每個參與者發送回滾消息;否則,發送提交消息。參與者根據事務管理器的消息執行提交或者回滾操作,并釋放事務處理過程中使用的鎖資源。
2PC的算法思路可以概括為:參與者將操作成敗的結果通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是執行提交操作還是回滾操作。兩階段提交就是分成兩個階段提交,第一階段詢問各個事務數據源是否準備好,第二階段才真正將數據提交給事務數據源。但因為2PC協議成本比較高,又有全局鎖的問題,性能會比較差。現在我們基本上不會采用這種強一致性解決方案。2PC流程如下圖所示。
在2PC中,如果兩階段出現協調者和參與者都宕機的情況,則有可能出現數據不一致的問題,同時還存在著諸如同步阻塞、單點問題、腦裂等問題,所以研究者們在2PC的基礎上做了改進,提出了3PC。
3PC(三階段提交)
3PC是2PC的改進版本,流程如下圖所示。
3PC要解決的最關鍵問題就是協調者和參與者同時宕機的問題,所以3PC將2PC的第一階段一分為二,形成了由canCommit、preCommit和Commit 3個階段組成的事務處理協議。
3PC的核心理念是:在詢問時并不鎖定資源,除非所有參與者都同意了,才開始鎖資源。一旦參與者無法及時收到來自協調者的信息,它就會默認執行Commit,而不會一直持有事務資源并處于阻塞狀態,但是這種機制也會導致數據一致性問題。3PC在2PC的基礎上做了如下改進:
● 增加了超時機制。
● 在兩階段之間插入了準備階段。
TCC補償模式
關于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland在2007年發表的一篇名為Lifebeyond Distributed Transactions:anApostate’s Opinion的論文中提出的。在該論文中,TCC還是以
Tentative-Confirmation-Cancellation命名的。
TCC的核心思想是:針對每個操作都要注冊一個與其對應的確認和補償(撤銷)操作。TCC事務處理流程和2PC類似,不過2PC通常都在跨庫的DB層面,而TCC本質上就是一個應用層面的2PC,需要通過業務邏輯來實現。
TCC模式的工作原理
TCC模式的工作原理如下圖所示。TCC將一個完整的事務提交分為Try、Confirm、Cancel 3個操作。
● Try:預留業務資源/數據效驗。
● Confirm:確認執行真正要執行的業務,如果所有事務參與者的Try操作都執行成功了,就會調用所有事務參與者的Confirm操作,確認資源。Confirm操作滿足冪等性,要求具備冪等設計,Confirm失敗后需要進行重試。
● Cancel:取消執行,如果有事務參與者在Try階段執行失敗,就調用所有已成功執行Try階段的參與者的Cancel方法,釋放Try階段占用的資源。Cancel操作滿足冪等性,Cancel階段的異常和Confirm階段的異常處理方案基本上一致。
下面是實際模擬用戶下單的一個業務場景使用TCC模式的主要流程圖。TCC第一階段是圖中的粗線條部分,這個階段需要分別執行訂單服務和庫存服務的Try事務,預留必需的業務資源;在第一階段執行成功后,就會進入第二階段的Confirm操作,如果不成功,則進行Cancel操作。
TCC的優點和缺點
● TCC的優點:讓應用自己定義數據庫操作的粒度,使降低鎖沖突、提高吞吐量成為可能。TCC的特點在于業務資源檢查與加鎖,一階段進行校驗,鎖定資源,如果第一階段都成功,則第二階段對鎖定資源進行交易邏輯,否則對鎖定資源進行釋放,這樣就避免了數據庫兩階段提交中的鎖沖突和長事務低性能風險。
● TCC的缺點:業務邏輯的每個分支都需要實現Try、Confirm、Cancel 3個操作,應用侵入性較強,改造成本高。另外,實現難度較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。為了滿足一致性的要求,Confirm和Cancel接口必須實現冪等。
TCC的開源解決方案
目前,國內的螞蟻金服主要采用TCC模式進行分布式事務管理。下面總結了常用的TCC開源分布式管理框架:
● Seata
● Tcc-transaction
● Hmily
● ByteTCC
● EasyTransaction
Saga長事務模式
1987年,普林斯頓大學的Hector Garcia-Molina和Ke.NETh Salem發表了一篇論文:Sagas[3]。這篇論文提出了使用Saga機制作為分布式事 務 的 替 代 品 , 以 解 決 長 時 間 運 行 的 分 布 式 事 務 ( long-LivedTransaction,LLT)問題,LLT指長時間持有數據庫資源的長活事務。
該論文認為業務過程經常由很多步驟組成,每一個步驟都涉及一個事務,如果將這些事務組成一個分布式事務,就可以實現總體一致。然而在長時間運行的分布式事務中,使用分布式事務會影響效率和系統的并發處理能力,因為在執行分布式事務時會有鎖產生。Saga通過確保每一個業務過程都有修正事務來減少系統對分布式事務的依賴。這種在業務流程中執行修正事務的方式最終保證了系統數據一致性。
每一個LLT的Saga都由一系列sub-transaction Ti組成,每個Ti都有對應的補償動作Ci,補償動作用于撤銷Ti造成的結果。同時Saga定義了兩種恢復策略:
● 向后恢復模式(Backward Recovery),在這種模式下,每一個內部子事務都有一個對應的補償事務,如果任一子事務失敗,則將撤銷之前所有成功的sub-transaction,使整個Saga的執行結果都撤銷。
● 向前恢復模式(Forward Recovery),這種模式假設每個子事務最終都會成功,適用于必須要成功的場景,執行順序如下:T1,T2,…,Tj(失敗),Tj(重試),…,Tn,其中Tj是發生錯誤的sub-transaction,此時執行重試邏輯,在該模式下不需要執行補償事務。
微服務架構大師Chris Richardson在介紹微服務架構與數據一致性技術時,提出了Saga模式,將微服務中的分布式事務劃分為一組小的事務,所劃分事務或者全部提交,或者全部回滾。強調在微服務中,每個單獨的微服務都可以確保ACID,因為每一個微服務都具備自己的數據庫,但是,Saga模式作為整體并不保證隔離性,所以需要對異常情況進行補償操作。
在Saga模式中,為了保障事務提交和回滾,應使用事務日志結尾和消息傳遞的方式。在發消息之前,將消息寫入本地數據庫,這就是所謂的事務日志結尾。它的作用是當新的日志到來時,可以直接發布,這樣就可以強化ACD特性;同時Saga模式中子模塊之間的消息通信建議采用消息傳遞方式,因為和HTTP通信協議相比,消息傳遞方式具備持久性。
Saga模式的工作原理
在微服務架構下,Saga存在兩種協調模式。
● 編排模式(Choreography)
這種模式在微服務之間傳遞Saga,沒有重協調器,每個微服務都監聽其他服務,并決定采取行動。這種模式最大的優勢就是簡單,容易理解,參與者之間松散耦合,對于參與者較少的情況,適合采用這種模式。下面是使用編排模式的分布式事務時序圖,實線表示消息發布,虛線表示訂閱。
事件執行順序如下:
(1)訂單服務(Order Service)在Approval_pending狀態下創建了訂單,并發布訂單創建事件。
(2)庫存服務(Inventory Service)消費訂單創建事件,在Inventory_pending狀態下驗證訂單,訂單出庫,并創建InventoryCreate Event。
(3)賬戶服務(Account Service)消費訂單創建事件并進入等待的狀態。
(4)賬戶服務消費Inventory Event,收取費用并發布AccountEvent。
( 5 ) 庫 存 服 務 訂 閱 Account Event 商 品 出 庫 , 更 改Inventory_pending狀態為Accept狀態。
(6)訂單服務收到Account Event,訂單狀態改為Approved狀態。
(7)如果上述訂單服務失敗,那么庫存服務消費費用收取失敗事件,回滾之前庫存事務清單,將狀態改為拒絕。
(8)如果上述賬戶服務失敗,那么訂單服務消費費用收取失敗事件,回滾訂單狀態改為拒絕。
● 編制模式(Orchestrator)
這種模式需要一個集中的服務觸發器,跟蹤Saga的所有子任務調用情況,根據調用情況來決定是否采用補償措施。這種中央協調的方式可以減少每一個微服務之間的循環依賴,集成處理事件決策和邏輯排序,也更容易推理Saga組合,保證長事務數據的最終一致性。使用業務流程時,可以定義一個控制類,其唯一職責是告訴Saga參與者該做什么。Saga控制使用命令或異步回復樣式交互與參與者進行通信。
下面是使用編制模式的分布式事務時序圖,實線表示消息request,虛線表示消息reply。
事件執行順序如下:
(1)訂單服務(Order Service)創建一個訂單和一個訂單控制器:Saga Orchestrator。
(2)Saga Orchestrator向庫存服務(Inventory Service)發送一個創建訂單命令。
(3)庫存服務回復庫存出庫命令。
(4)Saga Orchestrator向賬號服務(Account Service)發送一個費用收取命令。
(5)賬號服務回復訂單費用扣除命令。
(6)Saga Orchestrator向訂單服務發送一個訂單已批準命令。
說明:基于業務流程,Saga編制的每個步驟都包括更新數據庫和發布消息的服務,例如訂單服務持久化和創建Saga Orchestrator,并向第一個Saga參與者發送消息。第一個Saga參與者是庫存服務,通過更新數據庫回復消息。然后訂單服務通過更新Saga協調器的狀態向下一個Saga參與者發送命令,并處理參與者的命令回復響應,服務必須使用事務性的消息傳遞,以便自動更新數據并發布消息。
編排模式與編制模式
微服務中推薦使用編制模式。相比編排模式,編制模式有下面幾個優勢。
● 編制模式有更簡化的依賴關系。編排模式中,服務之間需要掌握相互依賴關系,而且針對不同的異常,可能還需要采用不同的補償措施,Saga Orchestrator則調用Saga參與者,所有參與者之間是不需要了解Orchestrator的實現細節的。
● 每個服務只需要暴露各自的API供Orchestrator調用即可,因此每個微服務之間有較少耦合,可以降低每個參與者的復雜度。
● 可以使用Saga Orchestrator更加方便地增加或減少Saga分布式事務邏輯,不需要改變每一個參與者的Saga內部事務。同時可以基于Orchestrator進行關注點分離,并簡化業務,操作更加容易、方便。
Saga與ACID
Saga不提供對整體隔離性的保證。一個長事務劃分為若干本地事務,在本地事務提交后,整個Saga未完成之前,其他服務可以訪問“未完成”的中間狀態事務數據;Saga協調器實現原子特性,通過日志實現持久性;通過本地日志與Saga日志保證事務一致性。所以Saga模式只支持ACD,不提供隔離性的保證。
Saga與2PC的區別
Saga與2PC最主要的區別在于,2PC是一個強一致性的分布式事務模式,Saga和TCC都通過應用服務層犧牲ACID特性來實現事務的最終一致性,Saga和TCC可以理解為分布式環境下通過事務補償模式的事務控制模型,只是Saga和TCC有各自不同的實現策略。
Saga與TCC的區別
Saga和TCC最大的區別在于,Saga沒有預留資源,而是直接提交到數據庫,Saga比TCC少了一步Try操作,無論最終事務成功或失敗,TCC都需要與事務參與方交互兩次。而Saga在事務成功的情況下只需要與事務參與方交互一次。如果事務失敗,則需要采取補償事務的方式進行回滾。
Saga的開源解決方案
Saga為相互獨立、自治的微服務提供了一種分布式網絡場景下的數據一致性的解決方案。下面是一些Saga模式的開源解決方案,由于篇幅所限,這里不再贅述方案的實現細節。
● ServiceComb Saga
● Axon Framework
● Eventuate-tram-sagas
可靠消息模式
可靠消息模式主要采用一個可靠的消息中間件作為中介,事務的發起方在完成本地事務后向可靠的消息中間件發起消息,事務消費方在收到消息后處理消息,該方案強調的是雙方最終的數據一致性。
如下圖所示,訂單服務將消息發送給訂單服務隊列,庫存服務監聽訂閱了訂單服務的消息隊列,并從消息隊列中消費信息。由此可以看到,從事務的發起方到消息中間件,再到事務的消費方,中間都會通過網絡,由于網絡的不可靠性,導致分布式事務的數據不一致性。
基于這種網絡的不可靠性,依靠本地消息表和可靠消息隊列的模式可以解決分布式事務的不一致性。而這種模式,對業務的侵入性相對較小,在實現復雜度上也比較可控,國內很多互聯網公司都采用了可靠消息模式來解決分布式事務的一致性問題。而這一方案的提出和思路來源于Ebay,之后這種模式在業內被廣泛使用。這種分布式事務模式的本質是本地事務+可靠消息,以達到最終事務的一致性。我們來看可靠消息模式的具體實現原理,如下圖所示。
可靠消息模式的思想是:事務的發起方需要額外創建一個本地消息表,將本地消息表和業務數據放在同一個事務中提交執行。也就是說,二者要么同時成功,要么同時失敗。例如,將訂單創建事務和庫存驗證及庫存出庫事件消息日志放入同一個事務中,下面是偽代碼舉例:
從上述偽代碼可知,本地數據庫與庫存出庫事務處于同一事務中,二者的綁定操作具備了原子性,工作時序如下:
(1)本地事務提交后,可以使用觸發的方式對本地消息表進行查詢和消息推送,或者使用定時器的方式輪詢本地消息表進行消息推送。
(2)消費者的消息可以使用可靠的消息中間件機制,例如RabbitMQ中的ACK(消息確認機制)保證消費者可以一定消費到消息;
又如庫存服務在接收到消息并且完成消息業務處理后,回復ACK,說明此時庫存服務已經正確同步數據;如果庫存服務沒有回復ACK,則消息中間件在沒收到ACK消息時,將保留消息,并重復投遞此消息。
(3)當消息中間件反饋消費成功后,庫存服務可以回調一個訂單服務的確認API,這時訂單服務可以從本地事務表中刪除對應的消息隊列。
(4)在訂單服務中,如果定時任務重復把本地事務表中的消息發到庫存服務,則需要消息消費方(庫存服務)提供消息的冪等性支持。
冪等性
簡單來說,冪等性的概念就是:除了錯誤或者過期的請求(換言之就是成功的請求),無論多次調用還是單次調用,最終得到的效果都是一致的。通俗來說,只要有一次調用成功,再采用相同的請求參數,無論調用多少次(重復提交),都應該返回成功。
例如庫存服務對外提供服務接口,必須承諾實現接口的冪等性,這一點在分布式系統中極其重要。
● 對于HTTP調用,承諾冪等性可以避免表單或者請求操作重復提交,造成業務數據重復。
● 對于異步消息調用,承諾冪等性通過對消息去重處理也是為了避免重復消費造成業務數據重復。
下面是幾種常用的冪等性處理設計方案。
● 數據庫表設計對邏輯上唯一的業務鍵唯一索引,這是在數據庫層面做最后的保障。
● 業務邏輯上的防重,例如創建訂單的接口,首先通過訂單號查詢庫表中是否已經存在對應的訂單,如果存在,則不做處理,直接返回成功。
補償方案
在可靠消息事務方案中,事務發起方需要確保消息發送到消息隊列中,而消息隊列成為系統的瓶頸。當消息隊列異常,或者消息消費失敗導致數據不一致時,需要采取補償措施,而常用的補償方案由消息消費方負責。之所以使用消費方補償模式有下面兩個主要理由:
● 一般來說,數據不一致大概率發生在消息消費異常場景,如果由消息發送方補償錯誤,往往無法解決消費異常問題;同時一個消息可能存在多個消費方,如果消息補償模塊在所有上游服務中編寫,可能無法滿足所有消費方的異常補償場景。
● 當消費方出現問題時,需要定位事務發起方,然后才能通過上游來補償,這種方式會增加處理生產問題的復雜度。
異步消息交互時,采取的補償措施通常統一由消息消費方實現,這種方式將類似本地事件表的方式,在消息消費失敗后,將失敗消息寫入本地數據庫,然后啟動定時任務進行重試,當達到重試上限時,進行預警和人工干預。
RabbitMQ可靠消息傳輸實踐
在眾多消息隊列中,RabbitMQ最重要的特性就是將消息的可靠性作為傳輸消息考慮的第一要素。目前,RabbitMQ已經成為金融行業中消息隊列的標配。我們將通過RabbitMQ的幾個關鍵因素講解RabbitMQ如何保證消息的可靠傳輸。RabbitMQ流程如下圖所示。
確認機制
網絡異常、機器異常、程序異常等多種情況都可能導致業務丟失消息。對消息進行確認可以解決消息的丟失問題,確認成功意味著消息已被驗證并被正確處理。確認機制能用在兩個方向:允許消費者告訴服務器(Broker)已經收到了消息,也允許服務器告訴生產者接收到了消息。前者就是我們常說的消費者ACK,后者就是我們常說的生產者Confirm。
RabbitMQ使用生產者消息確認、消費者消息確認機制來提供可靠交付功能。
● 生產者消息確認:生產者向RabbitMQ發送消息后,等待它回復確認成功;否則生產者向RabbitMQ重發該消息。此過程可以異步進行,生產者持續發送消息,RabbitMQ將消息批量處理后再回復確認;生產者通過識別確認返回中的ID來確定哪些消息被成功處理。開啟生產者消息確認機制:
● 消費者消息確認:RabbitMQ向消費者投遞消息后,等待消費者回復確認成功;否則RabbitMQ重新向消費者投遞該消息。該過程同樣可以異步處理,RabbitMQ持續投遞消息,消費者批量處理完后回復確認。可以看出,RabbitMQ/AMQP提供的是“至少一次交付”(at-least-once delivery)的策略,異常情況下消息會被重復投遞或消費。開啟消費者消息確認機制:
持久化機制
設置交換機、隊列和消息都為持久化,它可以在服務器重啟時保證消息不丟失信息,集群節點提供冗余能力,對解決Broker的單點故障至關重要。在RabbitMQ集群中,所有的定義都可以被冗余處理,例如交換器和綁定關系等,而隊列只存在于一個節點上。對于隊列而言,可以通過配置把隊列鏡像到多個節點上。
● 交換機的持久化(通過查看源碼易知,默認是支持持久化的),代碼如下:
● 隊列的持久化(通過查看源碼易知,默認是支持持久化的),代碼如下:
● 消 息 的 持 久 化 。 當 我 們 使 用 RabbitTemplate 調 用convertAndSend(String exchange,String routingKey,final Object object)方法時,默認是持久化模式。
生產者
當使用確認機制時,生產者從連接或者Channel故障中恢復過來,會重發沒有被Broker確認簽收的消息。如此一來,消息就可能被重復發送,可能是由于網絡故障等原因,Broker發送了確認,但是生產者沒有收到而已。或者消息根本就沒有發送到Broker。正因為生產者為了可靠性可能會重發消息,所以在消費者消費消息處理業務時,還需要去重,或者對接收的消息做冪等處理(推薦冪等處理)。生產者增加確認機制非常簡單,Channel開啟Confirm模式,然后增加監聽即可。
說 明 : RabbitMQ 還 有 事 務 機 制 ( txSelect 、 txCommit 、txRollback),也能保障消息的發送。不過事務機制是“同步阻塞”的,所以不推薦使用。而Confirm模式是“異步”機制。通過事務機制與Confirm模式的TPS性能對比,我們可以很明顯地看到,事務機制是性能最差的。
RabbitMQ支持的4種交換器類型中,只有fanout模式不存在路由不到隊列的情況。因為它會自動路由到所有隊列中,跟綁定Key沒有任何關系。所以,在滿足業務的前提下,筆者建議,盡可能使用fanout模式的類型交換器。
DLX(Dead Letter Exchange,死信郵箱或死信交換機)就是一個普通的交換機,與一般的交換機沒有任何區別。當消息在一個隊列中變成死信時,通過這個交換機將死信發送到死信隊列中。
我們可以通過DLX來解決這個問題,假設一些消息沒有被消費,那么它就會被轉移到綁定的DLX上。對于這類消息,我們消費并處理死信隊列即可。
消費者
只有消費者確認的消息,RabbitMQ才會刪除它,不確認就不會被刪除。所以,在消費端,建議關閉自動確認機制。應該在收到消息、處理完業務后,手動確認消息。消費者手動確認消息的實現代碼如下:
注意上面方法中void basicAck(long deliveryTag,booleanmultiple)的第二個參數multiple。要說明這個參數的含義,首先需要清楚“deliveryTag”概念,即投遞消息的唯一標識符,它是一個“單調遞增”的Long型正整數。假設此次basicAck的tag為123456,如果 multiple=false , 則 表 示 只 確 認 簽 收 這 一 條 消 息 。 如 果multiple=true,則表示確認簽收tag小于或等于123456的所有消息。
小結
在微服務架構下,我們強調要根據微服務的數據類型和業務場景選擇合適的后端數據存儲類型。對于微服務架構下分布式應用中的數據一致性管理,不推薦使用分布式事務,微服務數據架構通過放棄分布式網絡的強一致性,來提升微服務之間的交互性能。另外,在微服務數據架構中,我們介紹了常見的TCC、Saga、可靠消息模式,可以作為保證數據之間最終一致性的解決方案。