日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

導讀:微服務架構下的支付系統,由于其需要在性能和一致性之間做很多權衡,帶來設計和實現的復雜性。Airbnb的支付系統需要對接全球很多個國家的支付系統,因此帶來很大的復雜性。本文詳細論述了Airbnb如何使用分布式事務的相關技術來保證支付系統的數據一致性和性能,十分值得一讀。

過去幾年中,Airbnb一直在將其基礎架構遷移到SOA。相比單體應用,SOA提供了許多優勢,例如支持開發人員專業化和加速迭代的能力。然而,這也對計費和支付程序提出了挑戰,因為SOA使維護數據完整性變得更加困難。對服務API的調用,該服務對下游服務進行進一步的API調用,其中每個服務改變狀態都可能具有副作用,等同于執行復雜的分布式事務。

為了確保所有服務之間的數據一致性,可以使用兩階段提交之類的協議。如果不用這樣的協議,數據一致性就難以保證。在分布式系統中請求不可避免地會失敗(連接會在某些時候丟失并超時,尤其是對于包含多個網絡請求的事務。

分布式系統中使用三種不同的常用技術來實現最終的一致性:讀修復,寫修復和異步修復。每種方法各有利弊。三種方式在我們的支付系統中都有使用。

異步修復通過服務器負責運行數據一致性檢查來實現,例如表掃描,lambda函數和cron job。此外,從服務器到客戶端的異步通知廣泛用于支付行業,以保持客戶端的一致性。異步修復以及通知可以與讀寫修復技術結合使用,提供第二道防線,并在解決方案復雜性方面起到作用。

本文中描述的解決方案使用了寫修復,其中從客戶端到服務器的每次寫入調用都嘗試修復不一致狀態。寫修復要求客戶端更加智能(稍后我們將對此進行擴展討論),并允許重復發出相同的請求,而不必維護狀態(除了重試)。因此,客戶端可以按自己的需求來達到最終的一致性,從而使他們能夠控制用戶體驗。在實現寫修復時,冪等性是一個非常重要的屬性。

 

什么是冪等?

API請求具有冪等性即客戶端可以重復進行相同的調用,結果將是相同的。換句話說,發出多個相同的請求應該與發出單個請求具有相同的效果。

這種技術通常用于涉及資金流動的計費和支付系統,即支付請求必須完全處理一次(也稱為“確切一次交付”)。重要的是,如果多次調用移動資金操作,系統最多只能移動一次資金。這對Airbnb Payments API至關重要,以避免多次支付。

冪等性允許來自客戶端的多個相同請求使用API的自動重試機制來達到最終一致性。這種方式在具有冪等性的客戶端 - 服務器中是常見的,并且在我們的系統中也是如此。

下圖說明了重復請求和理想冪等行為的簡單場景。無論收費多少,客戶最多支付一次費用。

支付核心系統設計:Airbnb的分布式事務方案簡介

 

問題描述

保證我們的支付系統最終的一致性至關重要。冪等性是在分布式系統中實現這一點的理想機制。在SOA世界中,我們將不可避免地遇到問題。例如,假如服務沒有響應,客戶端將如何恢復?如果Response丟失或客戶超時怎么辦?如果競爭條件導致用戶點擊“預訂”兩次呢?我們的需求包括:

  • 我們需要一個通用但可配置的冪等解決方案,而不是實現針對特定用例的自定義解決方案,以便在Airbnb的各種支付服務中使用。

  • 雖然正在迭代基于SOA的支付產品,但我們無法在數據一致性上妥協。

  • 我們需要超低延遲,因此構建單獨的冪等服務不能滿足延遲要求。最重要的是,該服務將遇到上述問題。

  • 隨著Airbnb使用SOA擴展其工程組織,讓每個開發人員專注于數據完整性和最終的一致性是非常低效的。我們希望業務開發免受這些麻煩,保證他們能夠專注于產品開發并更快地進行迭代。

此外,代碼可讀性,可測試性和故障排除能力的相當大的權衡被認為是非主導因素。

 

解決方案

我們希望能夠唯一地識別每個請求。此外,我們需要準確跟蹤和管理特定請求在其生命周期中的位置。

我們在多種支付服務中實施并使用了“Orpheus”,這是一種通用的冪等庫。Orpheus是傳說中的希臘神話英雄。

我們選擇了實現冪等庫作為解決方案,因為它提供低延遲,同時仍然提供高速變更的產品代碼和低速變更的系統管理代碼之間的隔離。在高層次上,它包含以下:

  • 冪等key被傳遞到框架中,表示單個冪等請求

  • 始終從主數據庫讀取和寫入(為了一致性)冪等信息表

  • 通過使用JAVA lambda組合數據庫事務,確保原子性

  • 錯誤被分類為“可重試”或“不可重試”

接下來我們將詳細說明具有冪等性保證的復雜分布式系統如何能夠自我修復并達到最終一致。我們還將介紹一些該方案應該注意的設計權衡和帶來的額外的復雜性。

 

最小化數據庫提交

冪等系統的關鍵要求之一是只產生兩個結果,即成功或失敗,具有一致性。否則,數據有偏差可能導致數小時排查錯誤時間和付款出問題。由于數據庫提供ACID屬性,因此數據庫事務可以有效地用于原子寫入,確保一致性。一次數據庫提交可以保證其作為一個單元的一致性。

Orpheus假設每個標準API請求都分為三個不同的階段:Pre-RPC,RPC和Post-RPC。

“RPC”是指客戶端向遠程服務器發出請求并等待該服務器響應的過程。在支付API的上下文中,我們將RPC稱為對下游服務的請求,其可以包括外部支付服務和收單銀行等。簡而言之,如下是每個階段發生的事情:

  • Pre-RPC:付款請求的詳細信息記錄在數據庫中。

  • RPC:請求通過網絡對外部服務進行實時處理,并收到響應。這是一個執行冪等計算或RPC的過程(例如,如果是重試嘗試,則首先查詢事務狀態)。

  • Post-RPC:來自外部服務的響應的詳細信息記錄在數據庫中,包括其是否成功以及錯誤請求是否可重試。

為了保持數據完整性,我們遵循兩個基本規則:

  • 在Pre-RPC和Post-RPC階段,沒有遠程服務交互

  • RPC階段中沒有數據庫交互

我們希望避免將網絡調用與數據庫操作混在一起。在pre-RPC和post-RPC階段網絡調用(RPC)易受攻擊的,并可能導致連接池快速耗盡和性能下降之類的不良后果。簡而言之,網絡調用本質上是不可靠的。因此,我們將Pre和Post-RPC階段處理數據庫事務。

我們還想要說明單個API請求可能包含多個RPC。 Orpheus支持多RPC請求,但在這篇文章中,我們只想用簡單的單RPC案例來說明我們的思考過程。

如下面的示例圖所示,所有Pre-RPC和Post-RPC階段中的數據庫提交都合并為一個數據庫事務,這確保了原子性 。動機是系統應該以可恢復的方式出現故障。例如,如果在多次數據庫提交過程中有幾次失敗,那么系統地跟蹤每個失敗發生的位置將非常困難。請注意,所有網絡通信(RPC)都與數據庫事務明確分開。

支付核心系統設計:Airbnb的分布式事務方案簡介

這里的數據庫提交包括冪等庫的數據庫提交和應用程序數據庫提交,所有這些提交都組合在同一個代碼塊中。 如果不小心組織,這里的代碼就會非常混亂。 我們認為產品開發人員不應該負責保證冪等庫的操作。

 

Java Lambdas 組合事務

值得慶幸的是,Java lambda表達式可以將多個提交無縫地組合成單個數據庫事務,也不會影響可測試性和代碼可讀性。

下面是一個示例,簡化了Orpheus的使用,其中Java lambdas如下:

public Response processPayment(InitiatePaymentRequest request, UriInfo uriInfo) throws YourCustomException {
 return orpheusManager.process( request.getIdempotencyKey, uriInfo, // 1. Pre-RPC  -> { // Record payment request information from the request object PaymentRequestResource paymentRequestResource = recordPaymentRequest(request); return Optional.of(paymentRequestResource); }, // 2. RPC (isRetry, paymentRequest) -> { return executePayment(paymentRequest, isRetry); }, // 3. Post RPC - record response information to database (isRetry, paymentResponse) -> { return recordPaymentResponse(paymentResponse); });}
這是源代碼的簡化版本:
public <R extends Object, S extends Object, A extends IdempotencyRequest> Response process( String idempotencyKey, UriInfo uriInfo, SetupExecutable<A> preRpcExecutable, // Pre-RPC lambda ProcessExecutable<R, A> rpcExecutable, // RPC lambda PostProcessExecutable<R, S> postRpcExecutable) // Post-RPC lambda throws YourCustomException { try { // Find previous request (for retries), otherwise create IdempotencyRequest idempotencyRequest = createOrFindRequest(idempotencyKey, apiUri); Optional<Response> responseoptional = findIdempotencyResponse(idempotencyRequest);
 // Return the response for any deterministic end-states, such as // non-retryable errors and previously successful responses if (responseOptional.isPresent) { return responseOptional.get; }
 boolean isRetry = idempotencyRequest.isRetry; A requestObject = ;
 // STEP 1: Pre-RPC phase: // Typically used to create transaction and related sub-entities // Skipped if request is a retry if(!isRetry) { // Before a request is made to the external service, we record // the request and idempotency commit in a single DB transaction requestObject = dbTransactionManager.execute( tc -> { final A preRpcResource = preRpcExecutable.execute; updateIdempotencyResource(idempotencyKey, preRpcResource);
 return preRpcResource; }); } else { requestObject = findRequestObject(idempotencyRequest); }
 // STEP 2: RPC phase: // One or more network calls to the service. May include // additional idempotency logic in the case of a retry // Note: NO database transactions should exist in this executable R rpcResponse = rpcExecutable.execute(isRetry, requestObject);
 // STEP 3: Post-RPC phase: // Response is recorded and idempotency information is updated, // such as releasing the lease on the idempotency key. Again, // all in one single DB transaction S response = dbTransactionManager.execute( tc -> { final S postRpcResponse = postRpcExecutable.execute(isRetry, rpcResponse); updateIdempotencyResource(idempotencyKey, postRpcResponse);
 return postRpcResponse; });
 return serializeResponse(response); } catch (Throwable exception) { // If CustomException, return error code and response based on // ‘retryable’ or ‘non-retryable’. Otherwise, classify as ‘retryable’ // and return a 500. }}

我們沒有實現嵌套數據庫事務,而是將Orpheus和應用程序中的數據庫指令組合成單個數據庫事務,傳遞Java 閉包。

開發人員必須預先考慮好,才能保代碼的可讀性和可維護性。 他們還需要始終如一地評估適當的依賴關系和數據傳遞。 現在需要將API調用重構為三個部分,這可能會限制開發人員編寫代碼的方式。 實際上,某些復雜的API調用實際上很難有效地分解為三步。 我們的服務實現了一個有限狀態機,每次轉換都是使用StatefulJ的冪等步驟,可以在API調用中安全地復用冪等調用。

 

處理異常 - 重試還是不重試?

使用像Orpheus這樣的框架,服務器應該知道何時可以重試請求,而何時不行。要做到這一點,應該以細致的處理異常,異常被分類為“可重試”或“不可重試”兩大類。這無疑為開發人員增加了一層復雜性,如果他們錯誤使用,就會產生副作用。

例如,假設下游服務暫時宕機,經常被錯誤地標記為“不可重試”。這樣請求將無限期地“失敗”,并且后續重試請求將永遠返回不可重試錯誤。相反,如果異常被標記為“可重試”(而實際應該是“不可重試”且需要人工干預),則可能會發生雙重付款。

通常,我們認為由網絡和基礎架構問題(5XX HTTP狀態)導致的意外運行時異常是可重試的。我們希望這些錯誤是暫時的,我們希望稍后重試相同的請求最終會成功。

我們將驗證錯誤(例如無效輸入和狀態(例如,您無法退還退款))分類為不可重試(4XX HTTP狀態) - 我們預計同一請求的所有后續重試都會以相同方式失敗。因此創建了一個自定義的通用異常類來處理這些情況,默認為“不可重試”,對于其他情況,它們被歸類為“可重試”異常。

至關重要的是,每個請求的請求有效負載保持不變并且永遠不會發生變化,否則會破壞冪等請求的定義。

支付核心系統設計:Airbnb的分布式事務方案簡介

當然,需要謹慎處理更復雜的邊緣情況,例如在不同的上下文中適當地處理PointerException。 例如,由于數據庫鏈接暫時出問題而返回的空值與來自客戶端或來自第三方請求中的錯誤空字段不同。

 

客戶端

正如本文開頭所提到的,在寫修復系統中客戶端需要更加智能。 在與使用像Orpheus這樣的冪等性庫的服務進行交互時,它必須做到:

  • 為每個新請求傳遞一個唯一的冪等鍵; 重試的時候重用相同的冪等鍵。

  • 在調用服務之前將這些冪等鍵保留在數據庫中(以后用于重試)。

  • 正確成功響應后取消冪等鍵(或者置空)。

  • 確保不允許在重試中改變請求有效負載。

  • 根據業務需求仔細設計和配置自動重試策略(使用指數退避或隨機等待時間(“抖動”)以避免驚群問題)。

 

如何選擇冪等鍵?

選擇冪等鍵是至關重要的 - 客戶可以根據要選擇保證請求級冪等性或實體級冪等性。使用什么鍵將取決于業務,但請求級冪等性是最直接和最常見的。

對于請求級冪等性,應從客戶端選擇隨機且唯一的鍵,以確保整個實體集合級別的冪等性。例如,如果我們想要為預訂允許多種不同的付款方式,我們只需要確保冪等鍵是不同的。 UUID是一個很好的示例格式。

實體級冪等性比請求級冪等性更加嚴格。假設我們要確保ID為1234的10美元付款只能退還5美元,由于我們可以在技術上兩次提交5美元的退款申請,所以希望使用基于實體模型的冪等鍵來確保實體級的冪等性。示例格式為“payment-1234-refund”。因此,對于唯一付款的每個退款請求都將在實體級別保證冪等(付款1234)。

 

每個API請求都有到期租約

由于多次用戶點擊或客戶端激進的重試策略,可能會觸發多個相同的請求。 由于競態條件,可能會導致多次支付。 為了避免這些情況,在框架的幫助下,每個API請求都需要獲取冪等鍵上的數據庫行級鎖。 這授予給定請求進一步繼續的租約或許可。

租約帶有到期時間,以涵蓋服務器端存在超時的情況。 如果沒有響應,則在當前租約到期后才重試API請求。 應用程序可以根據需要配置租約到期和RPC超時時間。 經驗法則是具有比RPC超時更長的租約到期時間。

Orpheus還為冪等鍵提供了一個最大可重試窗口,以提供安全網,以避免意外系統行為導致的惡意重試。

 

記錄到Response

我們還記錄Response,以維護和監控冪等行為。 當客戶端對已達到確定性最終狀態的事務(例如,不可重試的錯誤(例如,驗證錯誤)或成功響應)發出相同的請求時,Response將記錄在數據庫中。

持久化Response確實是個性能權衡,保證客戶端能夠在后續重試時獲得快速響應,但此表將隨應用程序吞吐量增長而增長。 如果我們不小心,該表會變得很臃腫。 解決方案是定期刪除超過特定時間范圍的數據,但過早刪除數據也會產生負面影響。 除此之外,開發人員應該謹慎,不要對Response實體和結構進行向后不兼容的更改。

 

使用主庫

在使用Orpheus讀取和寫入冪等信息時,我們選擇直接從主庫執行操作。在分布式數據庫系統中,在一致性和延遲之間存在權衡。由于我們無法容忍高延遲或讀取未提交的數據,因此使用主庫對我們來說是最有意義的。如果數據庫系統沒有配置為強一致性(我們的系統由MySQL支持),那么使用副本進行這些操作實際上可能會對冪等性產生不利影響。

例如,假設支付服務將其冪等信息存儲在從庫中。客戶端向支付服務提交付款請求,該請求最終成功,但客戶端由于網絡問題而未收到響應。雖然當前存儲在服務主庫中的響應最終將最終寫入從庫,但是,由于有同步延遲,客戶端可能會進行重試,由于同步延遲,服務可能會錯誤地再次執行付款,從而導致重復付款。下面的例子說明了僅僅幾秒鐘的復制延遲可能會對Airbnb造成重大財務影響。

支付核心系統設計:Airbnb的分布式事務方案簡介

由于復制延遲導致重復支付

支付核心系統設計:Airbnb的分布式事務方案簡介

使用主庫避免重復支付

當使用單個主數據庫保證冪等性時,可伸縮性會成為主要問題。我們通過按照冪等鍵對數據庫進行分片來緩解這個問題。我們使用的冪等鍵具有高基數和均勻分布,使分片更加高效。

 

最后的想法

許多解決方案都可以緩解分布式系統中的一致性挑戰。 Orpheus是適用于我們的幾種產品之一,因為它具有普遍性和輕量級這樣的特性。開發人員可以在使用新服務時簡單地導入類庫,并且將冪等邏輯保存在獨立于應用程序之外的單獨的抽象層。

如果不引入一些復雜性,就不可能實現最終的一致性。客戶端需要存儲和處理冪等鍵并實現自動重試機制。開發人員需要額外的上下文,并且在實現Java lambda時必須如外科外科手術一樣精確。處理異常時必須慎重。此外,由于當前版本的Orpheus經過了實戰考驗,我們也在不斷尋找改進之處:改進請求負載匹配以便進行重試,改進對數據庫模式更改和嵌套遷移的支持,在RPC階段主動限制數據庫訪問等等。

原文地址:

https://medium.com/airbnb-engineering/avoiding-double-payments-in-a-distributed-payments-system-2981f6b070bb

本文由方圓翻譯。轉載本文請注明出處,歡迎更多小伙伴加入翻譯及投稿文章的行列,詳情請戳公眾號菜單「聯系我們」。

分享到:
標簽:Airbnb
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定