分散式冪等問題解決方案三部曲

来源:https://www.cnblogs.com/404p/archive/2019/08/11/11334825.html
-Advertisement-
Play Games

歡迎關註公眾號:404P(技術無涯),作者是螞蟻金服的一線開發,分享自己的成長和思考之路。內容涉及數據、工程、演算法。 綱要 文章目的:本文旨在提煉一套分散式冪等問題的思考框架,而非解決某個具體的分散式冪等問題。在這個框架體系內,會有一些方案舉例說明。文章目標:希望讀者能通過這套思考框架設計出符合自己 ...




歡迎關註公眾號:404P(技術無涯),作者是螞蟻金服的一線開發,分享自己的成長和思考之路。內容涉及數據、工程、演算法。

 


綱要

文章目的:本文旨在提煉一套分散式冪等問題的思考框架,而非解決某個具體的分散式冪等問題。在這個框架體系內,會有一些方案舉例說明。
文章目標:希望讀者能通過這套思考框架設計出符合自己業務的完備的冪等解決方案。
文章內容
(1)背景介紹,為什麼會有冪等。
(2)什麼是冪等,這個定義非常重要,決定了整個思考框架。
(3)解決冪等問題的三部曲,也是作者的思考框架。
(4)總結

一 背景

分散式系統由眾多微服務組成,微服務之間必然存在大量的網路調用。下圖是一個服務間調用異常的例子,用戶提交訂單之後,請求到A服務,A服務落單之後,開始調用B服務,但是在A調用B的過程中,存在很多不確定性,例如B服務執行超時了,RPC直接返回A請求超時了,然後A返回給用戶一些錯誤提示,但實際情況是B有可能執行是成功的,只是執行時間過長而已。

 

用戶看到錯誤提示之後,往往會選擇在界面上重覆點擊,導致重覆調用,如果B是個支付服務的話,用戶重覆點擊可能導致同一個訂單被扣多次錢。不僅僅是用戶可能觸發重覆調用,定時任務、消息投遞和機器重新啟動都可能會出現重覆執行的情況。在分散式系統里,服務調用出現各種異常的情況是很常見的,這些異常情況往往會使得系統間的狀態不一致,所以需要容錯補償設計,最常見的方法就是調用方實現合理的重試策略,被調用方實現應對重試的冪等策略。

二 什麼是冪等

對於冪等,有一個很常見的描述是:對於相同的請求應該返回相同的結果,所以查詢類介面是天然的冪等性介面。舉個例子:如果有一個查詢介面是查詢訂單的狀態,狀態是會隨著時間發生變化的,那麼在兩次不同時間的查詢請求中,可能返回不一樣的訂單狀態,這個查詢介面還是冪等介面嗎?

冪等的定義直接決定了我們如何去設計冪等方案,如果冪等的含義是相同請求返回相同結果,那實際上只需要緩存第一次的返回結果,即可在後續重覆請求時實現冪等了。但問題真的有這麼簡單嗎?

筆者更贊同這種定義:冪等指的是相同請求(identical request)執行一次或者多次所帶來的副作用(side-effects)是一樣的。

引自:https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state. In other words, an idempotent method should not have any side-effects (except for keeping statistics).

這個定義有一定的抽象,概括性比較強,在設計冪等方案時,其實就是將抽象部分具化。例如:什麼是相同的請求?哪些情況會有副作用?該如何避免副作用?且看三部曲。

三 解決方案三部曲

不少關於冪等的文章都稱自己的方案是通用解決方案,但筆者卻認為,不同的業務場景下,相同請求和副作用都是有差異性的,不同的副作用需要不同的方案來解決,不存在完全通用的解決方案。而三部曲旨在提煉出一種思考模式,並舉例說明,在該思考模式下,更容易設計出符合業務場景的冪等解決方案。

第一部曲:識別相同請求

冪等是為瞭解決重覆執行同一請求的問題,那如何識別一個請求有沒有和之前的請求重覆呢?有的方案是通過請求中的某個流水號欄位來識別的,同一個流水號表示同一個請求。也有的方案是通過請求中某幾個欄位甚至全部欄位進行比較,從而來識別是否為同一個請求。所以在方案設計時,明確定義具體業務場景下什麼是相同請求,這是第一部曲。

方案舉例:token機制識別前端重覆請求

在一條調用鏈路的後端系統中,一般都可以通過上游系統傳遞的reqNo+source來識別是否是為重覆的請求。如下圖,B系統是依賴於A系統傳遞的reqNo+source來識別相同請求的,但是A系統是直接和前端頁面交互的系統,如何識別用戶發起的請求是相同的呢?比如用戶在支付界面上點擊了多次,A系統怎麼識別這是一次重覆操作呢?

 

前端可以在第一次點擊完成時,將按鈕設置為disable,這樣用戶無法在界面上重覆點擊第二次,但這隻是提升體驗的前端解決方案,不是真正安全的解決方案。

常見的服務端解決方案是採用token機制來實現防重覆提交。如下圖,

 

(1)當用戶進入到表單頁面的時候,前端會從服務端申請到一個token,並保存在前端。
(2)當用戶第一次點擊提交的時候,會將該token和表單數據一併提交到服務端,服務端判斷該token是否存在,如果存在則執行業務邏輯。
(3)當用戶第二次點擊提交的時候,會將該token和表單數據一併提交到服務端,服務端判斷該token是否存在,如果不存在則返回錯誤,前端顯示提交失敗。

這個方案結合前後端,從前端視角,這是用於防止重覆請求,從服務端視角,這個用於識別前端相同請求。服務端往往基於類似於redis之類的分散式緩存來實現,保證生成token的唯一性和操作token時的原子性即可。核心邏輯如下。

// SETNX keyName value: 如果key存在,則返回0,如果不存在,則返回1

// step1. 申請token
String token = generateUniqueToken();

// step2. 校驗token是否存在
if(redis.setNx(token, 1) == 1){
  // do business
} else {
 // 冪等邏輯
}

第二部曲:列出並減少副作用的分析維度

相同的請求重覆執行業務邏輯,如果處理不當,會給系統帶來副作用。那什麼是副作用?就是業務無法接受的非預期結果。最常見的有重覆入庫、數據被錯誤變更等,大多數冪等方案就是圍繞解決這類問題來設計的。而系統往往可能在多個維度都存在副作用,例如:
(1)調用下游維度:重覆調用下游會怎樣?如果下游沒有冪等,重覆調用會帶來什麼副作用?
(2)返回上游維度:例如第一次返回上游異常,第二次返回上游被冪等了?會給上游帶來什麼副作用?
(3)併發執行維度:併發重覆執行會怎樣?會有什麼副作用?
(4)分散式鎖維度:引入分散式鎖來防止併發執行?但是如果鎖出現不一致性,會有什麼副作用?
(5)交互時序維度:有沒有非同步交互,是否存在時序問題?會有什麼副作用?
(6)客戶體驗維度:從數據不一致到最終一致,必須在多少時間內完成?如果該時間內沒有完成,會有什麼副作用?例如大量客訴(秉承客戶第一的原則,在支付寶,客訴量太大會定級為生產環境故障)。
(7)業務核對維度:重覆調用是否存在覆蓋核對標識的情況,帶來無法正常核對的副作用?在金融系統中,資金鏈路無法核對是無法接受的。
(8)數據質量維度:是否存在重覆記錄?如果存在會有什麼副作用?

 

上面是一些常見的分析維度,不同行業的系統中會存在不一樣的維度,儘可能地總結出這些維度,併列入系統分析時的checklist中,能夠更好地完善冪等解決方案。沒有副作用才算是完備的冪等解決方案,但是副作用的維度太多,會提高冪等方案的複雜度。所以在能夠達成業務的前提下,減少一些分析維度,能夠使得冪等方案實現起來更加經濟有效。例如:如果有專門的冪等表存儲返回給上游的冪等結果,第(2)維度不用考慮了,如果用鎖來防止併發,第(3)個維度不考慮了,如果用單機鎖代替分散式鎖,第(4)個維度不考慮了。

這是解決冪等問題的第二部曲:列出並減少副作用的分析維度。在這部曲中,涉及的解決方案往往是解決某一個維度的副作用問題,適合以通用組件的形式存在,作為團隊內部的一個公共技術套路。

方案舉例:加鎖避免併發重覆執行

很多冪等解決方案都和防併發有關,那麼冪等和併發到底有什麼關聯呢?兩者的聯繫是:冪等解決的是重覆執行的問題,重覆執行既有串列重覆執行(例如定時任務),也有併發重覆執行。如果重覆執行的業務邏輯沒有共用變數和數據變更操作時,併發重覆執行是沒有副作用的,可以不考慮併發的問題。對於包含共用變數、涉及變更操作的服務(實際上這類服務居多),併發問題可能導致亂序讀寫共用變數,重覆插入數據等問題。特別是併發讀寫共用變數,往往都是發生生產故障後才被感知到。

所以在併發執行的維度,將併發重覆執行變成串列重覆執行是最好的冪等解決方案。支付寶最常見的方法就是:一鎖二判三更新,如下圖。當一個請求過來之後:一鎖,鎖住要操作的資源;二判,識別是否為重覆請求(第一部曲要定義的問題)、判斷業務狀態是否正常;三更新:執行業務邏輯。

 

Q&A
小A:鎖可能造成性能影響,先判後鎖再執行,可以提升效能。
大明:這樣可能會失去防併發的效果。還記得double check實現單例模式嗎?在加鎖前判斷了下,那加鎖後為啥還要判斷下?實際上第二次check才是必須的。想想看?
小A畫圖思考中...
小A:明白了,一鎖二判三更新,鎖和判的順序是不能變的,如果鎖衝突比較高,可以在鎖之前判斷下,提高效率,所以稱之為double check。
大明:是的,聰明。這兩個場景不一樣,但併發思路是一樣的。

private volatile static Girl theOnlyGirl;

// 實現單例時做了 double check
public static Girl getTheOnlyGirl() {

    if (theOnlyGirl == null) {   // 加鎖前check
        synchronized (Girl.class) {
            if (theOnlyGirl == null) {  // 加鎖後check
                theOnlyGirl = new Girl();    // 變更執行
            }
        }
    }

    return theOnlyGirl;
}

鎖的實現可以是分散式鎖,也是可以是資料庫鎖。分散式鎖本身會帶來鎖的一致性問題,需要根據業務對系統穩定性的要求來考量。支付寶的很多系統是通過在業務資料庫中新建一個鎖記錄表來實現業務鎖組件,其分表邏輯和業務表的分表邏輯一致,就可以實現單機資料庫鎖。如果沒有鎖組件,悲觀鎖鎖住業務單據也是可以滿足條件的,悲觀鎖要在事務中用select for update來實現,要註意死鎖問題,且where條件中必須命中索引,否則會鎖表,不鎖記錄。

併發維度幾乎是一個分散式冪等的通用分析維度,所以一個通用的鎖組件是很有必要的。但這也只是解決了併發這一個維度的副作用。雖然沒有了併發重覆執行的情況,但串列重覆執行的情況依舊存在,重覆執行才是冪等核心要解決的問題,重覆執行如果還存在其它副作用,冪等問題就是沒有解決掉。

加鎖後業務的性能會降低,這個怎麼解決?筆者認為,大多數情況下架構的穩定性比系統性能的優先順序更高,況且對於性能的優化有太多地方可以去實現,減少壞代碼、去除慢SQL、優化業務架構、水平擴展資料庫資源等方式。通過系統壓測來實現一個滿足SLA的服務才是評估全鏈路性能的正確方法。

第三部:識別細粒度副作用,針對性設計解決方案

在解決了部分維度的副作用之後,就需要針對單個粒度的副作用進行逐一識別並解決了。在數據質量維度上,最大的一個副作用是重覆數據。在交互維度上,最大的一個副作用是業務亂序執行。一般這類問題不設計成通用組件,可以開發人員自由發揮。本節用兩個常見方案做為例子。

方案舉例1:唯一性約束避免重覆落庫

在數據表設計時,設計兩個欄位:source、reqNo,source表示調用方,seqNo表示調用方發送過來的請求號。source和reqNo設置為組合唯一索引,保證單據不會重覆落兩次。如果調用方沒有source和reqNo這兩個欄位,可以根據業務實際情況將請求中的某幾個業務參數生成一個md5作為唯一性欄位落到唯一性欄位中來避免重覆落庫。

 

核心邏輯如下:

try {
    dao.insert(entity);    
    // do business
} catch (DuplicateKeyException e) {
    dao.select(param);
    // 冪等返回
}

這裡直接insert單據,若果成功則表示沒請求過,舉行執行業務邏輯,如果拋出DuplicateKeyException異常,則表示已經執行過,做冪等返回,簡單的服務通過這種方式也可以識別是否為重覆請求(第一部曲)。

利用資料庫唯一索引來避免重覆記錄,需要註意以下幾個問題:
(1)因為存在讀寫分離的設計,有可能insert操作的是主庫,但select查詢的卻是從庫,如果主備同步不及時,有可能select查出來也是空的。
(2)在資料庫有Failover機制的情況下,如果一個城市出現自然災害,很可能切換到另外一個城市的備用庫,那麼唯一性約束可能就會出現失效的情況,比如併發場景下第一次insert是在杭州的庫,然後此時failover將庫切到上海了,再一次同樣的請求insert也是成功的。
(3)資料庫擴容場景下,因為分庫規則發生變化,有可能第一次insert操作是在A庫,第二次insert操作是在B庫,唯一索引同樣不起作用。
(4)有的系統catch的是SQLIntegrityConstraintViolationException,這個是完整性約束,包含了唯一性約束,如果未給一個必填欄位設值,也會拋這個異常,所以應該catch鍵重覆異常DuplicateKeyException。
對於第(1)個問題,將insert 和select放在同一個事務中即可解決,對於(2)和(3),支付寶內部為了應對容量暴漲和FO,設計了一套基於數據複製技術的分散式數據平臺,這個case筆者瞭解不深,後續有機會再討論。

小A:如果我用唯一性約束來保證不會落重覆數據,是不是可以不加鎖防併發了?
大明:兩者沒有直接關係,加鎖防併發解決的是併發維度的副作用問題,唯一性約束只是解決重覆數據這單個副作用的問題。如果沒有唯一性約束,串列重覆執行也會導致insert重覆落數據的問題,唯一性約束本質上解決的是重覆數據問題,不是併發問題。

方案舉例2:狀態機約束解決亂序問題

一個業務的生命周期往往存在不同的狀態,用狀態機來控制業務流程中的狀態轉換是不二之選。在實際業務中單向的狀態機是比較常用的,當狀態機處於下一個狀態時,是不能回到前面的狀態的。以下場景經常會用到狀態機做校驗:
(1)調用方調用超時重試。
(2)消息投遞超時重試。
(3)業務系統發起多個任務,但是期待按照發起順序有序返回。

對於這種類問題,一般是在處理前先判斷狀態是否符合預期,如果符合預期再執行業務。當業務執行完成後,變更狀態時還會採取類似於於樂觀鎖的方式兜底校驗,例如,M狀態只能從N狀態轉換而來,那麼更新單據時,會在sql中做狀態校驗。

update apply set status = 'M' where status = 'N'

如果狀態被設計成可逆的,就有可能產生ABA問題。即在update之前,狀態有可能做過這樣的變更:N -> M -> N。所以狀態機設成單向流轉是比較合理的。

四 總結

本文首先引出了冪等的定義:相同請求無副作用,然後提出了設計冪等方案的三部曲,並舉例說明。設計者要能夠清晰地定義相同請求,並且採用通用組件減少一些副作用的分析維度,再針對具體的副作用設計相應的解決方案,直至沒有任何副作用,才是真正完備的冪等解決方案。在實際業務中,實現三部曲不一定是嚴格的先後順序,但只要按照這三部曲來構思方案,必能開拓思路,化繁為簡。

 

 

註:轉載請註明出處。本文提到的分散式鎖、業務鎖,悲觀鎖和樂觀鎖的選型,以及基於鎖的冪等組件的實現,將另起文章介紹,若感興趣可以關註公眾號:404P,微信:it404p,歡迎交流。

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

-Advertisement-
Play Games
更多相關文章
  • HBuilderX使用Vant組件庫 HBuilderX是一款由國人開發的開發工具,其官網稱其為輕如編輯器、強如IDE的合體版本。但是官方的社區中關於Vant組件的安裝大多都是針對微信小程式開發安裝Vant Weapp,鄙人嘗試了各種方法,經歷各種錯誤後終於成功安裝vant組件庫,在這裡分享一下使用 ...
  • 四、SDK配置和模塊許可權配置 SDK 就是 Software Development Kit 的縮寫,中文意思就是“軟體開發工具包”,也就是輔助開發某一類軟體的相關文檔、範例和工具的集合都可以叫做“SDK”。HbuilderX的SDK配置可視化界面中SDK有地圖、登錄鑒權、支付、推送、分享、語音識別 ...
  • - 事件 - 表單提交(掌握) "onsubmit" - 單擊事件(掌握) "onclick" - 頁面載入成功事件(掌握) "onload" - 焦點事件:(掌握) - 獲取焦點 "onfocus" - 失去焦點 "onblur" - 表單事件(瞭解) - ondblclick 雙擊事件 ... ...
  • - 概述 - JavaScript一種直譯式腳本語言,是一種動態類型、弱類型、基於原型的語言 - 作用:給頁面添加動態效果,校驗用戶信息等. - 入門案例 - js和html的整合 - 方式1:內聯式 "通過<script></script>標簽實現,在標簽體中編寫js代碼即可" - 方式2:外聯式... ...
  • javaScript概述 什麼是javaScript:javaScript是一種直譯式腳本語言。直接解釋執行的語言。 什麼是腳本語言? . java源代碼 >編譯成.class文件 >java虛擬機中才能執行 . 腳本語言:源碼 >解釋執行 . js由我們的瀏覽器解釋執行 HTML:決定了頁面的框架 ...
  • 和單例模式相似,工廠模式同樣聚焦於在考慮整個軟體構建的情況下合理創建對象,從而保證軟體的擴展性和穩定性。 簡單工廠模式:適用客戶端無需擴展的應用場景 //工廠方法模式:適合客戶端創建單個產品的應用場景 //抽象工廠模式:適合創建多個產品(產品固定)的應用場景 ...
  • 組合模式 定義 將對象組合成樹形結構以表示“部分 整體”的層次結構。組合模式使得用戶對單個對象和組合對象的使用具有一致性。 UML圖 模板代碼 Component Composite HRDepartment FinanceDepartment 測試 測試結果 總結 組合模式定義了包含基本對象、組合 ...
  • 舉個慄子 問題描述 打游戲存進度。 簡單實現 GameRole 測試 測試結果 存在問題 在客戶端調用這段,把整個游戲角色的細節暴露了,職責太大,需要知道游戲角色的生命力、攻擊力、防禦力這些細節,還要進行備份。如果以後需要增加“魔法力”或修改現有的某種力,那這部分代碼就需要修改,同樣恢復時也是一樣的 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...