如何在微服務下保證事務的一致性

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/04/27/17359302.html
-Advertisement-
Play Games

微服務架構是將單個服務拆分成一系列小服務,且這些小服務都擁有獨立的進程,彼此獨立,很好地解決了傳統單體應用的上述問題,但是在微服務架構下如何保證事務的一致性呢? ...


作者:京東科技 苗元

背景

隨著業務的快速發展、業務複雜度越來越高,傳統單體應用逐漸暴露出了一些問題,例如開發效率低、可維護性差、架構擴展性差、部署不靈活、健壯性差等等。而微服務架構是將單個服務拆分成一系列小服務,且這些小服務都擁有獨立的進程,彼此獨立,很好地解決了傳統單體應用的上述問題,但是在微服務架構下如何保證事務的一致性呢?

1、事務的介紹

1.1 事務

1.1.1 事務的產生

資料庫中的數據是共用資源,因此資料庫系統通常要支持多個用戶的或不同應用程式的訪問,並且各個訪問進程都是獨立執行的,這樣就有可能出現併發存取數據的現象,這裡有點類似Java開發中的多線程安全問題(解決共用變數安全存取問題),如果不採取一定措施會出現數據異常的情況。列舉一個簡單的經典案例:比如用戶用銀行卡的錢還京東白條,銀行卡扣款成功了,但是白條因為網路或者系統問題沒有還款成功,就會出大問題,這時候我們就需要使用事務。

1.1.2 事務的概念

事務是資料庫操作的最小工作單元,是作為單個邏輯工作單元執行的一系列操作;這些操作作為一個整體一起向系統提交,要麼都執行、要麼都不執行;事務是一組不可再分割的操作集合(工作邏輯單元)。例如:在關係資料庫中,一個事務可以是一條SQL語句,一組SQL語句或整個程式。

1.1.3 事務的特性

事務的四大特征主要是:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability),這四大特征大家或多或少都聽說過,這裡我做下簡單介紹。

(1)原子性(Atomicity):事務內的操作要麼全部成功,要麼全部失敗,不會在中間的某個環節結束。假如所有的操作都成功了,那麼事務是成功的,只要其中任何一個操作失敗,那麼事務會進行回滾,回滾到操作最初的狀態。

begin transaction;

update activity_acount set money = money-100 where name = '小明';

update activity_acount set money = money+100 where name = '小紅';

commit transaction;

(2)一致性(Consistency):事務的執行使數據從一個狀態轉換為另一個狀態,但是對於整個數據的完整性保持穩定。換一種說法是數據按照預期生效,數據的狀態是預期的狀態。比如資料庫在一個事務執行之前和執行之後,都必須處於一致性狀態,如果事務執行失敗,那麼需要自動回滾到原始狀態,也就是事務一旦提交,其他事務查看到的結果一致,事務一旦回滾,其他事務也只能看到回滾前的狀態。

舉個通俗一點的例子:小明給小紅轉賬100元,轉賬前和轉賬後數據是正確的狀態,這叫一致性,如果小紅沒有收到100元或者收到金額少於100元,這就出現數據錯誤,就沒有達到一致性。

(3)隔離性(Isolation):在併發環境中,不同事務同事修改相同的數據時,一個未完成的事務不會影響另外一個未完成的事務。

例如當多個用戶併發訪問資料庫時,比如操作同一張表時,資料庫為每一個用戶開啟的事務,不能被其他事務的操作所干擾,多個併發事務之間要相互隔離。

(4)持久性(Durability):事務一旦提交,其修改的數據將永遠保存到資料庫中,改變是永久性,即使接下來資料庫發生故障也不應對其有任何影響。

通俗一點例子:A卡裡有2000塊錢,當A從卡裡取出500,在不考慮外界因素干擾的情況下,那麼A的卡裡只能剩1500。不存在取了500塊錢後,卡裡一會剩1400,一會剩1500,一會剩1600的情況。

1.1.4 Mysql隔離級別

如果不考慮事務隔離性產生問題:臟讀、不可重覆讀和幻讀。

Mysql隔離級別分為4種:Read Uncommitted(讀取未提交的)、Read Committed(讀取提交的)、Repeatable Red(可重覆讀)、Serializaable(串列化)

(1)Read Uncommitted是隔離級別最低的一種事務級別。在這種隔離級別下,一個事務會讀到另一個事務更新後但未提交的數據,如果另一個事務回滾,那麼當前事務讀到的數據就是臟數據,這就是臟讀(Dirty Read)。

(2)在Read Committed隔離級別下,一個事務可能會遇到不可重覆讀(Non Repeatable Read)的問題。不可重覆讀是指,在一個事務內,多次讀同一數據,在這個事務還沒有結束時,如果另一個事務恰好修改了這個數據,那麼,在第一個事務中,兩次讀取的數據就可能不一致。

(3)在Repeatable Read隔離級別下,一個事務可能會遇到幻讀(Phantom Read)的問題。幻讀是指,在一個事務中,第一次查詢某條記錄,發現沒有,但是,當試圖更新這條不存在的記錄時,竟然能成功,並且,再次讀取同一條記錄,它就神奇地出現了,就好象發生了幻覺一樣。

(4)Serializable是最嚴格的隔離級別。在Serializable隔離級別下,所有事務按照次序依次執行,因此,臟讀、不可重覆讀、幻讀都不會出現。雖然Serializable隔離級別下的事務具有最高的安全性,但是,由於事務是串列執行,所以效率會大大下降,應用程式的性能會急劇降低。如果沒有特別重要的情景,一般都不會使用Serializable隔離級別。

如果沒有指定隔離級別,資料庫就會使用預設的隔離級別。在MySQL中,如果使用InnoDB,預設的隔離級別是Repeatable Read。

1.1.5 啟動事務

在說明啟動事務之前,首先大家先想一下事務的傳播行為,事務傳播行為用於解決兩個被事務管理的方法互相調用問題。實際開發中將事務在service控制,如以下方法調用存在傳播行為,如果serviceB也會產生一個代理對象,同時也會進行事務管理,執行serviceA和serviceB分別開啟事務,上邊的serviceA中funA方法內容不處於一個事務中了。

class serviceA{
    //此方法進行事務控制
    funA(){
        //在此方法中操作多個dao的操作,處於一個事務中
        userDao.insertUser();
        orderDao.insertOrder();
        //如果在這裡調用另一個service的方法,此時存在事務傳播
        serviceB.funB();
    }
}
class serviceB{
    funB(){
    }
}



解決方案就是,在啟動類上添加註解 @EnableTransactionManagement,在執行事務的方法上面使用 @Transactional(isolation = Isolation.DEFAULT,propagation = Propagation.REQUIRED)設置隔離界別與事務傳播。預設就是REQUIRED。

Spring的聲明式事務為事務傳播定義了幾個級別,預設傳播級別就是REQUIRED,它的意思是,如果當前沒有事務,就創建一個新事務,如果當前有事務,就加入到當前事務中執行。其餘的還有:

1.SUPPORTS:表示如果有事務,就加入到當前事務,如果沒有,那也不開啟事務執行。這種傳播級別可用於查詢方法,因為SELECT語句既可以在事務內執行,也可以不需要事務;

2.MANDATORY:表示必須要存在當前事務並加入執行,否則將拋出異常。這種傳播級別可用於核心更新邏輯,比如用戶餘額變更,它總是被其他事務方法調用,不能直接由非事務方法調用;

3.REQUIRES_NEW:表示不管當前有沒有事務,都必須開啟一個新的事務執行。如果當前已經有事務,那麼當前事務會掛起,等新事務完成後,再恢復執行;

4.NOT_SUPPORTED:表示不支持事務,如果當前有事務,那麼當前事務會掛起,等這個方法執行完成後,再恢復執行;

5.NEVER:和NOT_SUPPORTED相比,它不但不支持事務,而且在監測到當前有事務時,會拋出異常拒絕執行;

6.NESTED:表示如果當前有事務,則開啟一個嵌套級別事務,如果當前沒有事務,則開啟一個新事務。

1.2 本地事務

1.2.1 本地事務定義

定義:在單體應用中,我們執行多個業務操作使用的是同一個連接,操作同一個資料庫,操作不同表,一旦有異常我們可以整體回滾。

其實在介紹事務的定義中,也介紹了一部分本地事務。本地事務通過ACID保證數據的強一致性,在我們實際開發過程中,我們或多或少都使用了本地事務。例如,MySQL事務處理使用begin開始事務、rollback回滾事務、commit確認事務。事務提交後,通過redo log記錄變更,通過undo log 在失敗時進行回滾,保證事務原子性。在我們日常使用Java語言開發時,都接觸過Spring,Spring使用@Transactional註解就可以實現事務功能,前面我們也介紹過了。事實上,Spring封裝了這些細節,在生成相關的Bean的時候,在需要註入相關的帶有@Transactional註解的Bean時候用代理去註入,在代理中開啟提交/回滾事務。

1.2.2 本地事務的缺點

隨著業務的高速發展,面對海量數據,例如,上千萬甚至上億的數據,查詢一次所花費的時間會變長,甚至會造成資料庫的單點壓力。因此,我們就要考慮分庫與分表方案了。分庫與分表的目的在於,減小資料庫的單庫單表負擔,提高查詢性能,縮短查詢時間。這裡,我們先來看下單庫拆分的場景。事實上,分表策略可以歸納為垂直拆分和水平拆分。垂直拆分,把表的欄位進行拆分,即一張欄位比較多的表拆分為多張表,這樣使得行數據變小。一方面,可以減少客戶端程式和資料庫之間的網路傳輸的位元組數,因為生產環境共用同一個網路帶寬,隨著併發查詢的增多,有可能造成帶寬瓶頸從而造成阻塞。另一方面,一個數據塊能存放更多的數據,在查詢時就會減少 I/O 次數。水平拆分,把表的行進行拆分。因為表的行數超過幾百萬行時,就會變慢,這時可以把一張的表的數據拆成多張表來存放。水平拆分,有許多策略,例如,取模分表,時間維度分表等。這種場景下,雖然我們根據特定規則分表了,我們仍然可以使用本地事務。

但是,庫內分表,僅僅是解決了單表數據過大的問題,但並沒有把單表的數據分散到不同的物理機上,因此並不能減輕 MySQL 伺服器的壓力,仍然存在同一個物理機上的資源競爭和瓶頸,包括 CPU、記憶體、磁碟 IO、網路帶寬等。對於分庫拆分的場景,它把一張表的數據劃分到不同的資料庫,多個資料庫的表結構一樣。此時,如果我們根據一定規則將我們需要使用事務的數據路由到相同的庫中,可以通過本地事務保證其強一致性。但是,對於按照業務和功能劃分的垂直拆分,它將把業務數據分別放到不同的資料庫中。這裡,拆分後的系統就會遇到數據的一致性問題,因為我們需要通過事務保證的數據分散在不同的資料庫中,而每個資料庫只能保證自己的數據可以滿足 ACID 保證強一致性,但是在分散式系統中,它們可能部署在不同的伺服器上,只能通過網路進行通信,因此無法準確的知道其他資料庫中的事務執行情況。

此外,不僅僅在跨庫調用存在本地事務無法解決的問題,隨著微服務的落地中,每個服務都有自己的資料庫,並且資料庫是相互獨立且透明的。那如果服務 A 需要獲取服務 B 的數據,就存在跨服務調用,如果遇到服務宕機,或者網路連接異常、同步調用超時等場景就會導致數據的不一致,這個也是一種分散式場景下需要考慮數據一致性問題。

當業務量級擴大之後的分庫,以及微服務落地之後的業務服務化,都會產生分散式數據不一致的問題。既然本地事務無法滿足需求,因此就需要分散式事務。

2、分散式事務定義

分散式事務定義:我們可以簡單地理解,它就是為了保證不同資料庫的數據一致性的事務解決方案。這裡,我們有必要先來瞭解下 CAP 原則和 BASE 理論。CAP 原則是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分區容錯性)的縮寫,它是分散式系統中的平衡理論。在分散式系統中,一致性要求所有節點每次讀操作都能保證獲取到最新數據;可用性要求無論任何故障產生後都能保證服務仍然可用;分區容錯性要求被分區的節點可以正常對外提供服務。事實上,任何系統只可同時滿足其中二個,無法三者兼顧。對於分散式系統而言,分區容錯性是一個最基本的要求。那麼,如果選擇了一致性和分區容錯性,放棄可用性,那麼網路問題會導致系統不可用。如果選擇可用性和分區容錯性,放棄一致性,不同的節點之間的數據不能及時同步數據而導致數據的不一致。

此時,BASE 理論針對一致性和可用性提出了一個方案,BASE 是 Basically Available(基本可用)、Soft-state(軟狀態)和 Eventually Consistent(最終一致性)的縮寫,它是最終一致性的理論支撐。簡單地理解,在分散式系統中,允許損失部分可用性,並且不同節點進行數據同步的過程存在延時,但是在經過一段時間的修複後,最終能夠達到數據的最終一致性。BASE 強調的是數據的最終一致性。相比於 ACID 而言,BASE 通過允許損失部分一致性來獲得可用性。

現在比較常用的分散式事務解決方案,包括強一致性的兩階段提交協議,三階段提交協議,以及最終一致性的可靠事件模式、補償模式,TCC 模式。

3、分散式事務-強一致性解決方案

3.1 二階段提交協議

在分散式系統中,每個資料庫只能保證自己的數據可以滿足 ACID 保證強一致性,但是它們可能部署在不同的伺服器上,只能通過網路進行通信,因此無法準確的知道其他資料庫中的事務執行情況。因此,為瞭解決多個節點之間的協調問題,就需要引入一個協調者負責控制所有節點的操作結果,要麼全部成功,要麼全部失敗。其中,XA 協議是一個分散式事務協議,它有兩個角色:事務管理者和資源管理者。這裡,我們可以把事務管理者理解為協調者,而資源管理者理解為參與者。

XA 協議通過二階段提交協議保證強一致性。

二階段提交協議,顧名思義,它具有兩個階段:第一階段準備,第二階段提交。這裡,事務管理者(協調者)主要負責控制所有節點的操作結果,包括準備流程和提交流程。第一階段,事務管理者(協調者)向資源管理者(參與者)發起準備指令,詢問資源管理者(參與者)預提交是否成功。如果資源管理者(參與者)可以完成,就會執行操作,並不提交,最後給出自己響應結果,是預提交成功還是預提交失敗。第二階段,如果全部資源管理者(參與者)都回覆預提交成功,資源管理者(參與者)正式提交命令。如果其中有一個資源管理者(參與者)回覆預提交失敗,則事務管理者(協調者)向所有的資源管理者(參與者)發起回滾命令。舉個案例,現在我們有一個事務管理者(協調者),三個資源管理者(參與者),那麼這個事務中我們需要保證這三個參與者在事務過程中的數據的強一致性。首先,事務管理者(協調者)發起準備指令預判它們是否已經預提交成功了,如果全部回覆預提交成功,那麼事務管理者(協調者)正式發起提交命令執行數據的變更。

註意的是,雖然二階段提交協議為保證強一致性提出了一套解決方案,但是仍然存在一些問題。其一,事務管理者(協調者)主要負責控制所有節點的操作結果,包括準備流程和提交流程,但是整個流程是同步的,所以事務管理者(協調者)必須等待每一個資源管理者(參與者)返回操作結果後才能進行下一步操作。這樣就非常容易造成同步阻塞問題。其二,單點故障也是需要認真考慮的問題。事務管理者(協調者)和資源管理者(參與者)都可能出現宕機,如果資源管理者(參與者)出現故障則無法響應而一直等待,事務管理者(協調者)出現故障則事務流程就失去了控制者,換句話說,就是整個流程會一直阻塞,甚至極端的情況下,一部分資源管理者(參與者)數據執行提交,一部分沒有執行提交,也會出現數據不一致性。此時,讀者會提出疑問:這些問題應該都是小概率情況,一般是不會產生的?是的,但是對於分散式事務場景,我們不僅僅需要考慮正常邏輯流程,還需要關註小概率的異常場景,如果我們對異常場景缺乏處理方案,可能就會出現數據的不一致性,那麼後期靠人工干預處理,會是一個成本非常大的任務,此外,對於交易的核心鏈路也許就不是數據問題,而是更加嚴重的資損問題。

3.2 三階段提交協議

二階段提交協議諸多問題,因此三階段提交協議就要登上舞臺了。三階段提交協議是二階段提交協議的改良版本,它與二階段提交協議不同之處在於,引入了超時機制解決同步阻塞問題,此外加入了預備階段儘可能提早發現無法執行的資源管理者(參與者)並且終止事務,如果全部資源管理者(參與者)都可以完成,才發起第二階段的準備和第三階段的提交。否則,其中任何一個資源管理者(參與者)回覆執行失敗或者超時等待,那麼就終止事務。總結一下,三階段提交協議包括:第一階段預備,第二階段準備,第二階段提交。

這裡可能大家有點蒙,我再詳細講解一下三階段提交的整體流程。

3PC主要是為瞭解決兩階段提交協議的單點故障問題和縮小參與者阻塞範圍。 引入參與節點的超時機制之外,3PC把2PC的準備階段分成事務詢問(該階段不會阻塞)和事務預提交,則三個階段分別為CanCommit、PreCommit、DoCommit。

(1)第一階段(CanCommit 階段)

類似於2PC的準備(第一)階段。協調者向參與者發送commit請求,參與者如果可以提交就返回Yes響應,否則返回No響應。

1.事務詢問:
	協調者向參與者發送CanCommit請求。詢問是否可以執行事務提交操作。然後開始等待參與者的響應。
2.響應反饋
	參與者接到CanCommit請求之後,正常情況下,
	如果其自身認為可以順利執行事務,則返回Yes響應,併進入預備狀態。
	否則反饋No。

(2)第二階段(PreCommit 階段)

協調者根據參與者的反應情況來決定是否可以記性事務的PreCommit操作。根據響應情況,有以下兩種可能:

如果響應Yes,則:

1.發送預提交請求:
	協調者向參與者發送PreCommit請求,併進入Prepared階段。

2.事務預提交
	參與者接收到PreCommit請求後,會執行事務操作,並將undo和redo信息記錄到事務日誌中。

3.響應反饋
	如果參與者成功的執行了事務操作,則返回ACK響應,同時開始等待最終指令。



假如有任何一個參與者向協調者發送了No響應,或者等待超時之後,協調者都沒有接到參與者的響應,那麼就執行事務的中斷。則有:

1.發送中斷請求:
	協調者向所有參與者發送abort請求。

2.中斷事務
	參與者收到來自協調者的abort請求之後(或超時之後,仍未收到協調者的請求),執行事務的中斷。



(3)第三階段(doCommit 階段)

該階段進行真正的事務提交,也可以分為執行提交和中斷事務兩種情況。

如果執行成功,則有如下操作:

1.發送提交請求
	協調者接收到參與者發送的ACK響應,那麼它將從預提交狀態進入到提交狀態。
	並向所有參與者發送doCommit請求。

2.事務提交
	參與者接收到doCommit請求之後,執行正式的事務提交。
	併在完成事務提交之後釋放所有事務資源。

3.響應反饋
	事務提交完之後,向協調者發送ACK響應。

4.完成事務
	協調者接收到所有參與者的ACK響應之後,完成事務。



協調者沒有接收到參與者發送的ACK響應(可能是接受者發送的不是ACK響應,也可能響應超時),那麼就會執行中斷事務(註意這是沒有收到二段段最後的ACK,這裡要理解清楚)。則有如下操作:

1.發送中斷請求
	協調者向所有參與者發送abort請求

2.事務回滾
	參與者接收到abort請求之後,利用其在階段二記錄的undo信息來執行事務的回滾操作,
	併在完成回滾之後釋放所有的事務資源。

3.反饋結果
	參與者完成事務回滾之後,向協調者發送ACK消息

4.中斷事務
	協調者接收到參與者反饋的ACK消息之後,執行事務的中斷。



最關鍵的****:在doCommit階段,如果參與者無法及時接收到來自協調者的doCommit或者rebort請求時(1、協調者出現問題;2、協調者和參與者出現網路故障),會在等待超時之後,會繼續進行事務的提交。(其實這個應該是基於概率來決定的,當進入第三階段時,說明參與者在第二階段已經收到了PreCommit請求,那麼協調者產生PreCommit請求的前提條件是他在第二階段開始之前,收到所有參與者的CanCommit響應都是Yes。(一旦參與者收到了PreCommit,意味他知道大家其實都同意修改了)所以,一句話概括就是,當進入第三階段時,由於網路超時等原因,雖然參與者沒有收到commit或者abort響應,但是它有理由相信:成功提交的幾率很大)

三階段提交協議很好的解決了二階段提交協議帶來的問題,是一個非常有參考意義的解決方案。但是,極小概率的場景下可能會出現數據的不一致性。因為三階段提交協議引入了超時機制,一旦參與者無法及時收到來自協調者的信息之後,他會預設執行commit。而不會一直持有事務資源並處於阻塞狀態。但是這種機制也會導致數據一致性問題,因為,由於網路原因,協調者發送的abort響應沒有及時被參與者接收到,那麼參與者在等待超時之後執行了commit操作。這樣就和其他接到abort命令並執行回滾的參與者之間存在數據不一致的情況。

4、分散式事務-最終一致性解決方案

4.1 TCC模式

二階段提交協議和三階段提交協議很好的解決了分散式事務的問題,但是在極端情況下仍然存在數據的不一致性,此外它對系統的開銷會比較大,引入事務管理者(協調者)後,比較容易出現單點瓶頸,以及在業務規模不斷變大的情況下,系統可伸縮性也會存在問題。註意的是,它是同步操作,因此引入事務後,直到全局事務結束才能釋放資源,性能可能是一個很大的問題。因此,在高併發場景下很少使用。因此,需要另外一種解決方案:TCC 模式。註意的是,很多讀者把二階段提交等同於二階段提交協議,這個是一個誤區,事實上,TCC 模式也是一種二階段提交。

TCC 模式將一個任務拆分三個操作:Try、Confirm、Cancel。假如,我們有一個 func() 方法,那麼在 TCC 模式中,它就變成了 tryFunc()、confirmFunc()、cancelFunc() 三個方法。

在 TCC 模式中,主業務服務負責發起流程,而從業務服務提供 TCC 模式的 Try、Confirm、Cancel 三個操作。其中,還有一個事務管理器的角色負責控制事務的一致性。例如,我們現在有三個業務服務:交易服務,庫存服務,支付服務。用戶選商品,下訂單,緊接著選擇支付方式進行付款,然後這筆請求,交易服務會先調用庫存服務扣庫存,然後交易服務再調用支付服務進行相關的支付操作,然後支付服務會請求第三方支付平臺創建交易並扣款,這裡,交易服務就是主業務服務,而庫存服務和支付服務是從業務服務。

我們再來梳理下,TCC 模式的流程。第一階段主業務服務調用全部的從業務服務的 Try 操作,並且事務管理器記錄操作日誌。第二階段,當全部從業務服務都成功時,再執行 Confirm 操作,否則會執行 Cancel 逆操作進行回滾。

註意****:我們要特別註意操作的冪等性。冪等機制的核心是保證資源唯一性,例如重覆提交或服務端的多次重試只會產生一份結果。支付場景、退款場景,涉及金錢的交易不能出現多次扣款等問題。事實上,查詢介面用於獲取資源,因為它只是查詢數據而不會影響到資源的變化,因此不管調用多少次介面,資源都不會改變,所以是它是冪等的。而新增介面是非冪等的,因為調用介面多次,它都將會產生資源的變化。因此,我們需要在出現重覆提交時進行冪等處理。

那麼,如何保證冪等機制呢?事實上,我們有很多實現方案。其中,一種方案就是常見的創建唯一索引。在資料庫中針對我們需要約束的資源欄位創建唯一索引,可以防止插入重覆的數據。但是,遇到分庫分表的情況是,唯一索引也就不那麼好使了,此時,我們可以先查詢一次資料庫,然後判斷是否約束的資源欄位存在重覆,沒有的重覆時再進行插入操作。註意的是,為了避免併發場景,我們可以通過鎖機制,例如悲觀鎖與樂觀鎖保證數據的唯一性。這裡,分散式鎖是一種經常使用的方案,它通常情況下是一種悲觀鎖的實現。但是,很多人經常把悲觀鎖、樂觀鎖、分散式鎖當作冪等機制的解決方案,這個是不正確的。除此之外,我們還可以引入狀態機,通過狀態機進行狀態的約束以及狀態跳轉,確保同一個業務的流程化執行,從而實現數據冪等。

4.2 補償模式

我們提到了重試機制。事實上,它也是一種最終一致性的解決方案:我們需要通過最大努力不斷重試,保證資料庫的操作最終一定可以保證數據一致性,如果最終多次重試失敗可以根據相關日誌並主動通知開發人員進行手工介入。註意的是,被調用方需要保證其冪等性。重試機制可以是同步機制,例如主業務服務調用超時或者非異常的調用失敗需要及時重新發起業務調用。重試機制可以大致分為固定次數的重試策略與固定時間的重試策略。除此之外,我們還可以藉助消息隊列和定時任務機制。消息隊列的重試機制,即消息消費失敗則進行重新投遞,這樣就可以避免消息沒有被消費而被丟棄,例如 JMQ 可以預設允許每條消息最多重試 多少 次,每次重試的間隔時間可以進行設置。定時任務的重試機制,我們可以創建一張任務執行表,並增加一個“重試次數”欄位。這種設計方案中,我們可以在定時調用時,獲取這個任務是否是執行失敗的狀態並且沒有超過重試次數,如果是則進行失敗重試。但是,當出現執行失敗的狀態並且超過重試次數時,就說明這個任務永久失敗了,需要開發人員進行手工介入與排查問題。

除了重試機制之外,也可以在每次更新的時候進行修複。例如,對於社交互動的點贊數、收藏數、評論數等計數場景,也許因為網路抖動或者相關服務不可用,導致某段時間內的數據不一致,我們就可以在每次更新的時候進行修複,保證系統經過一段較短的時間的自我恢復和修正,數據最終達到一致。需要註意的是,使用這種解決方案的情況下,如果某條數據出現不一致性,但是又沒有再次更新修複,那麼其永遠都會是異常數據。

定時校對也是一種非常重要的解決手段,它採取周期性的進行校驗操作來保證。關於定時任務框架的選型上,業內比較常用的有單機場景下的 Quartz,以及分散式場景下 Elastic-Job、XXL-JOB、SchedulerX 等分散式定時任務中間件,咱公司有分散式調用平臺( https://schedule.jd.com/ )。關於定時校對可以分為兩種場景,一種是未完成的定時重試,例如我們利用定時任務掃描還未完成的調用任務,並通過補償機制來修複,實現數據最終達到一致。另一種是定時核對,它需要主業務服務提供相關查詢介面給從業務服務核對查詢,用於恢復丟失的業務數據。現在,我們來試想一下電商場景的退款業務。在這個退款業務中會存在一個退款基礎服務和自動化退款服務。此時,自動化退款服務在退款基礎服務的基礎上實現退款能力的增強,實現基於多規則的自動化退款,並且通過消息隊列接收到退款基礎服務推送的退款快照信息。但是,由於退款基礎服務發送消息丟失或者消息隊列在多次失敗重試後的主動丟棄,都很有可能造成數據的不一致性。因此,我們通過定時從退款基礎服務查詢核對,恢復丟失的業務數據就顯得特別重要了。

4.3 可靠事件模式

在分散式系統中,消息隊列在服務端的架構中的地位非常重要,主要解決非同步處理、系統解耦、流量削峰等問題。多個系統之間如果使用同步通信,則很容易造成阻塞,同時會將這些系統耦合在一起,因此,引入消息隊列後,一方面解決了同步通信機製造成的阻塞,另一方面通過消息隊列實現了業務解耦。

可靠事件模式,通過引入可靠的消息隊列,只要保證當前的可靠事件投遞並且消息隊列確保事件傳遞至少一次,那麼訂閱這個事件的消費者保證事件能夠在自己的業務內被消費即可。這裡是否只要引入了消息隊列就可以解決問題了呢?事實上,只是引入消息隊列並不能保證其最終的一致性,因為分散式部署環境下都是基於網路進行通信,而網路通信過程中,上下游可能因為各種原因而導致消息丟失。

其一,主業務服務發送消息時可能因為消息隊列無法使用而發生失敗。對於這種情況,我們可以讓主業務服務(生產者)發送消息,再進行業務調用來確保。一般的做法是,主業務服務將要發送的消息持久化到本地資料庫,設置標誌狀態為“待發送”狀態,然後把消息發送給消息隊列,消息隊列先向主業務服務(生產者)返回消息隊列的響應結果,然後主業務服務判斷響應結果執行之後的業務處理。如果響應失敗,則放棄之後的業務處理,設置本地的持久化消息標誌狀態為“失敗”狀態。否則,執行後續的業務處理,設置本地的持久化消息標誌狀態為“已發送”狀態。

此外,消息隊列接收消息後,也可能從業務服務(消費者)宕機而無法消費。JMQ有ACK機制,如果消費失敗,會重試,如果成功,會從消息隊列中刪除此條消息。那麼,消息隊列如果一直重試失敗而無法投遞,會在一定次數之後主動丟棄,當然我們也可以設置為一直重試,這種方式不推薦。我們需要如何解決呢?我們在上個步驟中,主業務服務已經將要發送的消息持久化到本地資料庫。因此,從業務服務消費成功後,它也會向消息隊列發送一個通知消息,此時它是一個消息的生產者。主業務服務(消費者)接收到消息後,最終把本地的持久化消息標誌狀態為“完成”狀態。這就是使用“正反向消息機制”確保了消息隊列可靠事件投遞。當然,補償機制也是必不可少的。定時任務會從資料庫掃描在一定時間內未完成的消息並重新投遞。大家也可能會說,消費成功之後可以用RPC調用主業務服務,首先這樣主業務服務要額外提供一個RPC的介面;另外也會對從業務服務造成業務的複雜度和耗時影響。這裡要註意從業務服務要保證冪等性。

瞭解了“可靠事件模式”的方法論後,現在我們來看一個真實的案例來加深理解。首先,當用戶發起退款後,自動化退款服務會收到一個退款的事件消息,此時,如果這筆退款符合自動化退款策略的話,自動化退款服務會先寫入本地資料庫持久化這筆退款快照,緊接著,發送一條執行退款的消息投遞到給消息隊列,消息隊列接受到消息後返迴響應成功結果,那麼自動化退款服務就可以執行後續的業務邏輯。與此同時,消息隊列非同步地把消息投遞給退款基礎服務,然後退款基礎服務執行自己業務相關的邏輯,執行失敗與否由退款基礎服務自我保證,如果執行成功則發送一條執行退款成功消息投遞到給消息隊列。最後,定時任務會從資料庫掃描在一定時間內未完成的消息並重新投遞。這裡,需要註意的是,自動化退款服務持久化的退款快照可以理解為需要確保投遞成功的消息,由“正反向消息機制”和“定時任務”確保其成功投遞。此外,真正的退款出賬邏輯在退款基礎服務來保證,因此它要保證冪等性。當出現執行失敗的狀態並且超過重試次數時,就說明這個任務永久失敗了,需要開發人員進行手工介入與排查問題。

總結一下,引入了消息隊列並不能保證可靠事件投遞,換句話說,由於網路等各種原因而導致消息丟失不能保證其最終的一致性,因此,我們需要通過“正反向消息機制”確保了消息隊列可靠事件投遞,並且使用補償機制儘可能在一定時間內未完成的消息並重新投遞。

5、總結

Google Chubby的作者Mike Burrows說過, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意思是世上只有一種一致性演算法,那就是Paxos,所有其他一致性演算法都是Paxos演算法的不完整版。上面都是以Paxos演算法理論為基礎具象化的方案。Google 的 Chubby、MegaStore、Spanner 等系統,ZooKeeper 的 ZAB 協議,還有更加容易理解的 Raft 協議都有Paxos演算法的影子,感興趣的可以去看Paxos演算法詳細說明,這裡就不再贅述了。

現在在做活動平臺相關項目,經常短時間要完成一個活動組件,一般沒有完整的考慮微服務下保證事務的一致性或者一套統一的標準,所以需要微服務下保證事務的一致性SOP,這樣每個可以保證每個活動快速搭建和安全運行。後續會推出活動平臺在微服務下保證事務一致性的整體方案。大家可能以前都聽過或者在寫代碼過程中或多或少都考慮過,也或多或少使用過前面提到過的這些方案,但是沒有一個系統性的瞭解或者完善的方案調研,希望通過這篇文章能讓大家有個稍微完整的瞭解,文章中有任何不足或者大家有更好的方案,歡迎一起共同探討。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 昨天才把html節點中的圖片轉成base格式的,今天就發現一個用戶體驗的問題;那麼是啥呢?就是我從左側的樹形菜單中拖拽節點的時候(滑鼠按下也是同樣問題),發現節點的圖片區域那裡會出現一個邊框,持續時間不是很長,就幾毫秒的時間,但是當你連續拖拽幾個不同節點的時候就會發現這個邊框竟然又消失不見了,如果此 ...
  • 1.<div style="z-index: 1000; position: absolute; filter: Alpha(opacity = 90); width: 100px;padding: 10px; border: 1px solid #333" id="img" align="cent ...
  • 1.創建vue項目 vue create demo demo是項目名稱 2.安裝axios 進入demo裡面打開終端(黑視窗),執行 npm install axios 3.進行config.js配置 devServer: { host: "0.0.0.0", // 是否可以被覆蓋 port: 80 ...
  • 當我們在編寫 TypeScript 代碼時,經常會遇到需要通用(Generic)的情況,這時候,泛型就是我們的好幫手了。在本篇文章中,我們將深入介紹 TypeScript 泛型的概念以及如何使用。 什麼是泛型? 在編程語言中,泛型指的是參數化類型的概念。也就是說,我們可以定義一個函數、介面或類等,能 ...
  • 大家好,我是DOM哥。我用 ChatGPT 開發了一個 Vue 的資源導航網站。不管你是資深 Vue 用戶,還是剛入門想學習 Vue 的小白,這個網站都能幫助到你。網站地址:https://dombro.site/vue#/vue ...
  • 在業務中,有這麼一種場景,表格下的某一列 ID 值,文本超長了,正常而言會是這樣: 通常,這種情況都需要超長省略溢出打點,那麼,就會變成這樣: 但是,這種展示有個缺點,3 個 ID 看上去就完全一致了,因此,PM 希望能夠實現頭部省略打點,尾部完全展示,那麼,最終希望的效果就會是這樣的: OK,很有 ...
  • CORS(跨來源資源共用)是一種用於解決跨域問題的方案。 CORS(跨來源資源共用)是一種安全機制,用於在瀏覽器和伺服器之間傳遞數據時,限制來自不同功能變數名稱的請求。在前端開發中,當通過 XMLHttpRequest(XHR)或 Fetch API 發送跨域請求時,如果伺服器沒有正確配置 CORS,瀏覽器 ...
  • 唯一不變的就是變化本身。 我們經常講的系統、子系統、模塊、組件、類、函數就是從邏輯上將軟體一步步分解為更細微的部分,即邏輯單元, 分而治之, 複雜問題拆解為若幹簡單問題, 逐個解決。 邏輯單元內部、外部的交互會產生依賴,從而產生了內聚、耦合概念。內聚主要描述邏輯單元內部,耦合主要描述邏輯單元之間的關 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...