如何通過事務消息保障搶購業務的分佈式一致性?

作者:山獵,阿里雲解決方案架構師

前言



在電商領域,搶購和秒殺是非常普遍業務模式,搶購類業務在快速拉昇用戶流量併爲消息者帶來實惠的同時,也給電商系統帶來了巨大考驗。在高併發、大流量的衝擊下,系統的性能和穩定性至關重要,任何一個環節出現故障,都會影響整體的購物體驗,甚至造成電商系統的大面積崩潰。和電商領域搶購場景極爲類似的業務模式還有很多,比如大型賽事和在線教育的報名系統,以及各類購票系統等。

針對搶購類業務在技術上帶來的挑戰,業界有一系列解決方案,通過不同維度來提升系統的性能與穩定性,包括動靜分離、定時上架、異步處理、令牌隊列、多級緩存、作弊行爲偵測、流量防護、全鏈路壓測等。

本文重點聚焦在如何確保搶購類業務的一致性上,分佈式事務一直是IT界老大難的問題,而搶購業務所具備的高併發、大流量特徵,更是成倍增加了分佈式一致性的實現難度。以下將介紹如何通過事務消息構建滿足搶購類業務要求的高性能高可用分佈式一致性機制。

事務一致性原理回顧



事務是應用程序中一系列嚴密的操作,這一系列操作是一個不可分割的工作單位,它們要麼全部執行成功,要麼一個都不做。事務具有四個特徵:原子性( Atomicity )、一致性( Consistency )、隔離性( Isolation )和持續性( Durability ),這四個特性簡稱爲 ACID 特性。

在非併發狀態下,保證事務的 ACID 特性是輕而易舉的事情,如果某一個操作執行不成功,把前面的操作全部回滾就 OK 了。而在併發狀態下,由於有多個事務同時操作同一個資源,對於事務 ACID 特性的保證就會困難一些,如果考慮得不周全,就會遇到如下幾個問題:

  • 髒讀:事務 A 讀到了事務 B 還沒有提交的數據。

  • 不可重複讀:在一個事務裏面對某個數據讀取了兩次,讀出來的數據不一致。

  • 幻讀:在一個事務對某個數據集用同樣的方式讀取了兩次,數據集的條目數量不一致。


爲了應對上述併發情況下出現的問題,就需要通過一定的事務隔離級別來解決。當事務的隔離級別越高的時候,上述問題發生的機會就越小,但是性能消耗也會越大。所以在實際生產過程中,要根據實際需求去確定隔離級別:

  • READ_UNCOMMITTED(讀未提交):最低的隔離級別,可以讀到未提交的數據,無法解決髒讀、不可重複讀、幻讀中的任何一種。

  • READ_COMMITED (讀已提交):能夠防止髒讀,但是無法解決不可重複讀和幻讀的問題。

  • REPEATABLE_READ (重複讀取):對同一條數據的多次重複讀取能保持一致,解決了髒讀、不可重複讀的問題,但是幻讀的問題還是無法解決。

  • SERLALIZABLE ( 串行化):最高的事務隔離級別,避免了事務的並行執行,解決了髒讀、不可重複讀和幻讀的問題,但性能最低。


關係型數據庫提供了對於事務的支持,能夠通過不同隔離級別的設置,確保併發狀態下事務的ACID特性。但關係型數據庫提供的能力是基於單機事務的,一旦遇到分佈式事務場景,就需要通過更多其他技術手段來解決問題。


搶購業務中的分佈式事務



有如下三種情況可能會產生分佈式事務:

1、一個事務操作包含對兩個數據庫的操作:數據庫所提供的事務保證僅能侷限在對於自身的操作上,無法跨越到其他數據庫。


2、一個事務包含對多個數據分片的操作:具體的分片規則由分庫分表中間件或者分庫分表 SDK 來實現,有可能跨越多個數據庫或同一個數據庫的多個表。對於業務邏輯而言,底層的數據分片情況是不透明的,因此也沒有辦法依賴於數據庫提供的單機事務機制。


3、一個事務包括對多個服務的調用:在微服務領域,這是極爲常見的場景,不同的服務使用不同的數據資源,甚至涉及到更爲複雜的調用鏈路。在這種情況下,數據庫提供的單機事務機制,僅僅能保證其中單一環節的 ACID 特性,沒有辦法延伸到全局。


微服務技術在電商領域的普及程度是非常高的,比較大型的電商應用還會通過中臺思想將共性業務能力進行沉澱,因此搶購業務中的很多環境都屬於跨服務的分佈式調用,會涉及到上述第三種分佈式事務形態。比如在訂單支付成功後,交易中心會調用訂單中心的服務把訂單狀態更新,並調用物流中心的服務通知商品發貨,同時還要調用積分中心的服務爲用戶增加相應的積分。如何保障分佈式事務一致性,成爲了確保搶購業務穩定運行的核心訴求之一。



分佈式事務的實現方式


傳統分佈式事務


傳統的分佈式事務通過 XA 模型實現,通過一個事務協調者,站在全局的角度將多個子事務合併成一個分佈式事務。XA 模型之所以能在分佈式事務領域得到廣泛使用,是因爲其具有如下兩個方面的優勢:

  • 提供了強一致性保證,在業務執行的任何時間點都能確保事務一致性。

  • 使用簡單。常見的關係型數據庫都提供了對XA協議的支持,通過引入事務協調器,業務代碼跟使用單機事務相比基本上沒有差別。


但是在互聯網領域,XA 模型的分佈式事務實現存在很多侷限性,在搶購業務這樣的高併發大流量場景中更是被完全棄用。我們拿 XA 分佈式協議中最普遍的兩階式提交方案,來說明爲什麼 XA 模型並不適合互聯網場景。


1、性能問題。在兩段式提交的執行過程中,所有參與節點都是事務阻塞型的,需要長時間鎖定資源。這會導致系統整體的併發吞吐量變低,在搶購業務中是不可接受的。
2、單點故障問題。事務協調者在鏈路中有着至關重要的作用,一旦協調者發生故障,參與者會一直阻塞下去,整個系統將無法工作,因此需要投入巨大的精力來保障事務協調者的高可用性。
3、數據不一致問題。在階段二中,如果協調者向參與者發送 commit 請求之後,發生了網絡異常,會導致只有一部分參與者接收到了 commit 請求,沒有接收到 commit 請求的參與者最終會執行回滾操作,從而造成數據不一致現象。在搶購業務中,這樣的數據不一致有可能會對企業或消費者造成巨大的經濟損失。

因此 XA 模型是一個理想化的分佈式事務模型,並沒有考慮到高併發、網絡故障等實際因素,即便是在兩階段提交的基礎上,誕生了三階段提交這樣的實現方式,也沒有辦法從根本上解決性能和數據不一致的問題。


柔性事務


針對傳統分佈式事務方案在互聯網領域的侷限性,業界提出了 CAP 理論以及 BASE 理論,在此基礎上誕生了在大型互聯網應用中廣泛使用的柔性事務。柔性事務的核心思想是放棄傳統分佈式事務中對於嚴格一致性的要求,允許在事務執行過程中存在數據不一致的中間狀態,在業務上需要容忍中間狀態的存在。柔性事務會提供完善的機制,保證在一段時間的中間狀態後,系統能走向最終一致狀態。

遵循 BASE 理論的柔性事務放棄了隔離性,減小了事務中鎖的粒度,使得應用能夠更好的利用數據庫的併發性能,實現吞吐量的線性擴展。異步執行方式可以更好地適應分佈式環境,在網絡抖動、節點故障的情況下能夠儘量保障服務的可用性。因此在高併發、大流量的搶購業務中,柔性事務是最佳的選擇。


傳統分佈式事務
柔性事務
業務改造
一致性
強一致性
最終一致
回滾
支持    
實現回退接口
隔離性
支持    
放棄隔離性或實現資源鎖定接口
併發性能    
低    
適合場景    
低併發、短事務
高併發、長事務

柔性事務有多種實現方式,包括TCC、Saga、事務消息、最大努力通知等,本文將重點介紹通過事務消息實現柔性事務。


事務消息原理分析


搶購業務場景拆解


我們結合搶購業務的真實場景,分析如何通過事務消息實現分佈式一致性。在搶購業務中,有兩個非常關鍵的階段,需要引入分佈式事務機制,分別是訂單創建階段和付款成功階段。

從字面含義來看,搶購業務就隱含了一個重要的前提:庫存是有限的。因此在訂單創建的時候,需要預先檢查庫存情況,並相對應的庫存進行鎖定,以防止商品超賣。如果庫存鎖定操作失敗,代表庫存不足,必須確保訂單不能被成功創建。在鎖定庫存後,如果因爲某種異常情況導致訂單創建失敗,必須及時將之前鎖定的庫存進行釋放操作,以便讓其他用戶可以重新爭奪對應的商品。

如果搶購系統實現了購物車機制,在訂單創建的同時,則需要從購買車中將相應的條目刪除。


基於微服務架構的業務拆分,訂單創建階段的 3 個行爲很有可能通過 3 個不同的微服務應用完成,因此需要通過分佈式事務來保證數據一致性。

訂單創建完成後,會等待用戶付款,一旦付款成功,就會觸發付款成功階段的執行邏輯。這個階段同樣是通過分佈式事務來完成,包含修改訂單狀態、扣減庫存、通知發貨、增加積分這4個子事務,它們要麼全部不執行,要麼全部執行成功。


當然,在真實的搶購業務中,情況有可能會更加的複雜,本文列出的只是其中最具代表性的幾類業務行爲。

引入消息異步通知機制


傳統的分佈式事務存在一個很大的弊端是參與節點都是事務阻塞型的,需要長時間鎖定資源。以鎖定庫存 ->創建訂單這個流程爲例,藉助於 Redis 等緩存系統,單純鎖定庫存的操作只需要花費毫秒級的時間,可以承載非常高的併發量。但如果把創建訂單的操作也考慮進來,加上不同微服務應用之間相互通訊的時候,整體耗時有可能超過1秒,導致性能急劇下降。

假設存在一種異步消息機制,讓分佈式事務的第一個參與方在執行完本地事務後,通過觸發一筆消息通知事務的其他參與方完成後續的操作,就能將大事務拆解爲多個本地子事務來分開執行。在這種模式下,事務的多個參與方之間之間並不需要彼此阻塞和等待,就能極大程度地提升併發吞吐能力。

對於庫存中心而言,在高併發場景下,只需要不斷的執行鎖定庫存記錄操作,並不斷通過異步消息通知訂單中心創建訂單,只要異步消息機制能確保消息一定送達,並得到正確處理,就能夠實現分佈式最終一致性。


先執行本地事務,還是先發送異步消息?


在這個模型中,異步消息的發送交給了分佈式事務的第一個參與方來完成,這個參與方就擁有了兩個職責:執行本地事務發送異步消息。到底應該先執行本地事務,還是先發異步消息呢?

第一種方案是先發送異步消息,再執行本地事務。這樣做肯定是不行的,如果本地事務沒有執行成功,異步消息已經發出去了,其他事務參與方收到消息後會執行對應的遠程事務,造成數據不一致。

第二種方案是先執行本地事務,再發送異步消息。這樣做能夠解決本地事務執行失敗導致的數據不一致問題,因爲只有在本地事務執行成功的情況下,纔會發送異步消息。但如果事務的參與方在執行本地事務成功後,自己宕機了,就再有沒有機會發送異步消息了,因此這樣做同樣會造成數據不一致的問題。記住:在真實場景中,任何一個應用節點都不是 100% 可靠的,都存在宕機的可能性。

一個可行的方案是引入可以處理事務消息的消息隊列集羣,用於異步消息的中轉。一個事務消息包含兩種形態:

1、首先,事務的參與方發送一筆半事務消息到消息隊列,表示自己即將執行本地事務,消息隊列集羣在收到這個半事務消息後,不會馬上進行投遞,而是進行暫存。

2、在執行完本地事務後,事務的參考方再發送一筆確認消息到消息隊列集羣,告知本地事務的執行狀態。如果本地事務執行成功,消息隊列集羣會將之前收到的半事務消息進行投遞;如果本地事務執行失敗,消息隊列集羣直接刪除之前收到的半事務消息,這樣遠程事務就不會被執行,從而保證了最終一致性。

同樣,如果事務參與方在執行完本地事務後宕機了怎麼辦呢?這就需要消息隊列集羣具備回查機制:如果收到半事務消息後,在特定時間內沒有再收到確認消息,會反過來請求事務參與方查詢本地事務的執行狀態,並給予反饋。這樣,即便錯過了確認消息,消息隊列集羣也有能力瞭解到本地事務的執行狀態,從而決定是否將消息進行投遞。

在一個微服務應用中,會存在多個對等的應用實例,這也就代表着即便一個事務參與方的實例在執行完本地事務後宕機了,消息隊列集羣依然可以通過這個實例的兄弟實例瞭解到本地事務執行的最終狀態。


如何確保遠程事務能執行成功?


如果一切本地事務的執行,以及異步消息的投遞都一切順利的話,接下來還會存在另外兩種數據不一致的可能性:

  • 消息隊列集羣在將異步消息投遞到遠程事務參與方的時候,由於網絡不穩定,消息沒能投遞成功。

  • 消息投遞成功了,但遠程事務參與方還沒來得及執行遠程事務,就宕機了。


這兩種情況都會導致遠程事務執行失敗,所以需要建立一種消息重試機制,讓遠程事務參與方在完成任務後(實際上對遠程事務參與方而言,這個任務是它要執行的本地任務),給予消息隊列集羣一個反饋,告知異步消息已經得到了正確的處理。否則,消息隊列會在一定時間後,週期性的重複投遞消息,直到它收到了來自遠程事務參與方的反饋,以確保遠程事務一定能執行成功。


和事務回查機制類似,遠程事務參與方也有多個對等的微服務實例,即便某個實例在沒來得及執行遠程事務的時候宕機,消息隊列也可以將任務交給這個實例的兄弟實例來完成。

完整流程




事務消息實戰



瞭解到事務消息的原理後,我們不難得出一個結論:消息隊列集羣在整個流程中起着至關重要的作用,如果消息隊列集羣不可用,所有涉及到分佈式事務的業務都將中止!因此,我們需要一個高可用的消息隊列集羣,能夠始終保持在工作狀態,即便其某個組件出現故障,也能夠在短時間內自動恢復,不會影響業務,還能確保接收到的消息不丟失。

消息隊列 RocketMQ


消息隊列 RocketMQ 版是阿里雲基於 Apache RocketMQ 構建的低延遲、高併發、高可用、高可靠的分佈式消息中間件。該產品最初由阿里巴巴自研並捐贈給 Apache 基金會,服務於阿里集團 13 年,覆蓋全集團所有業務,包括種類金融級場景。作爲雙十一交易核心鏈路的官方指定產品,支撐千萬級併發、萬億級數據洪峯,歷年刷新全球最大的交易消息流轉記錄。


阿里雲消息隊列 RocketMQ 提供了對於事務消息機制最完整實現,包括半事務消息、確認消息、事務回查機制、消息重試等重要功能。此外,消息隊列 RocketMQ 還提供了極強的高可用能力以及數據可靠性,可以確保在各種極端場景下都能提供穩定的服務,並確保消息不丟失。

對於開發者而言,使用雲上的消息隊列 RocketMQ,可以免除消息隊列集羣的搭建和維護工作,將更多的精力投入到實現業務邏輯的工作中。當消息隊列集羣的性能不能滿足要求時,還可以非常方便的進行集羣一鍵擴容,以獲得更高的併發吞吐量。


開通 RocketMQ 服務


在阿里雲官方網站開通消息隊列服務後方可開始使用消息隊列 RocketMQ,如果使用RAM用戶訪問RocketMQ,還必須先爲RAM用戶進行授權。在完成阿里雲賬戶註冊以及實名認證後,打開消息隊列 RocketMQ 版產品頁( https://www.aliyun.com/product/rocketmq ),點擊 免費開通,頁面跳轉至消息隊列 RocketMQ 版控制檯,在彈出的提示對話框中,完成 RocketMQ 服務的開通。


接下來,登錄 RAM 控制檯( https://signin.aliyun.com/1880770869023420.onaliyun.com/login.htm ),在左側導航欄選擇 人員管理 > 用戶,在 用戶 頁面,單擊目標 RAM 用戶 操作列 的 添加權限,在 添加權限 面板,單擊需要授予 RAM 用戶的權限策略,單擊 確定。消息隊列 RocketMQ 提供多種系統策略,可以根據權限範圍爲RAM用戶授予相關權限。爲了簡單起見,我們先開通 AliyunMQFullAccess 權限策略,授予該 RAM 用戶所有消息收發權限和控制檯所有功能操作權限。

創建資源


在調用 SDK 收發消息前,需在消息隊列 RocketMQ 控制檯創建相關資源,在調用SDK時需填寫這些資源信息。首先,我們要創建 RocketMQ 實例,實例是用於消息隊列 RocketMQ 服務的虛擬機資源,相當於一個獨立的消息隊列集羣,會存儲消息 Topic 和客戶端 Group ID 信息。

我們還需要注意,只有在同一個地域下的同一個實例中的 Topic 和 Group ID 才能互通,例如,某 Topic 創建在華東1(杭州)地域的實例 A 中,那麼該 Topic 只能被在華東1(杭州)地域的實例 A 中創建的 Group ID 對應的生產端和消費端訪問。

登錄到消息隊列 RocketMQ 控制檯( http://ons.console.aliyun.com ),在左側導航欄,單擊 實例列表,在頂部菜單欄,選擇地域,如 華東1(杭州),在 實例列表 頁面,單擊 創建實例,在 創建 RocketMQ 實例 面板,完成實例的創建。


接下來,在實例所在頁面的左側導航欄,單擊 Topic 管理。在 Topic 管理頁面,單擊 創建 Topic,在 創建 Topic面板,輸入 名稱 和 描述,選擇該Topic的 消息類型 爲 事務消息,完成Topic的創建。

Topic 是消息隊列 RocketMQ 版裏對消息的一級歸類,例如創建 Topic_Trade 這一 Topic 來識別交易類消息,消息生產者將消息發送到 Topic_Trade,而消息消費者則通過訂閱該 Topic 來獲取和消費消息。


創建完實例和 Topic 後,需要爲消息的消費者和或生產者創建客戶端 ID,即 Group ID 作爲標識。在事務消息的場景中,需要創建 2 個不同的 Group ID,分別代表本地事務參與方和遠程事務參與方。在實例所在頁面的左側導航欄,單擊 Group 管理,在 Group 管理 頁面,選擇 TCP 協議 > 創建 Group ID,在 創建可用於 TCP 協議的 Group 面板,完成本地事務客戶端 Group ID 的創建。重複此操作,完成遠程事務參與方 Group ID 的創建。


本地事務參與方的業務代碼


本文將通過 Java 代碼介紹如何實現事務消息相關的業務邏輯,爲了簡化業務邏輯,我們繼續基於 鎖定庫存 - > 創建訂單 這個流程來演示,在這個流程中,僅有 2 個事務參與方。


1、初始化 TransactionProducer


我們先通過 Maven 引入消息隊列 RocketMQ 的 SDK,優先使用阿里雲官方提供的 TCP 版 SDK。

  
  
  
<dependency>  <groupId>com.aliyun.openservices</groupId>  <artifactId>ons-client</artifactId>  <version>1.8.7.2.Final</version></dependency>

順利引入 Log4j2 用於日誌輸出。

  
  
  
<dependency>  <groupId>org.slf4j</groupId>  <artifactId>slf4j-api</artifactId>  <version>1.7.7</version></dependency><dependency>  <groupId>org.apache.logging.log4j</groupId>  <artifactId>log4j-slf4j-impl</artifactId>  <version>2.13.1</version></dependency>

在庫存中心的代碼中,我們需要初始化一個TransactionProducer,用於異步消息的發送,需要填入如下信息:

  • Group ID:之前創建的用於本地事務參與方的 Group ID。
  • Access key和Secret Key:RAM 用戶對應的密鑰信息,從 RAM 用戶控制檯獲得。
  • Nameserver Address:RocketMQ 實例的接入點信息,從 RocketMQ 控制檯獲得。

  
  
  
Properties properties = new Properties();// 您在控制檯創建的Group ID。注意:事務消息的Group ID不能與其他類型消息的Group ID共用。properties.put(PropertyKeyConst.GROUP_ID, "XXX");// AccessKey ID阿里雲身份驗證,在阿里雲RAM控制檯創建。properties.put(PropertyKeyConst.AccessKey, "XXX");// AccessKey Secret阿里雲身份驗證,在阿里雲RAM控制檯創建。properties.put(PropertyKeyConst.SecretKey, "XXX");// 設置TCP接入域名,進入消息隊列RocketMQ版控制檯的實例詳情頁面的TCP協議客戶端接入點區域查看。properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");// LocalTransactionCheckerImpl本地事務回查類的實現TransactionProducer producer = ONSFactory.createTransactionProducer(properties,    new LocalTransactionCheckerImpl());producer.start();

TransactionProducer 是線程安全的,啓動後能在多線程環境中複用。


2、獲取全局唯一的交易流水號


在發送半事務消息以及執行本地事務之前,我們需要先獲取一個全局唯一的交易流水號,訂單與交易流水號一一對應,接下來的事務消息機制都會依賴於這個這個交易流水號。我們可以通過引入第三方 ID 生成組件,或者在本地通過 Snowflake 算法實現。

3、實現本地事務回查邏輯


創建一個實現了LocalTransactionChecker接口的LocalTransactionCheckerImpl類,實現其中的check(Message)方法,該方法返回本地事務的最終狀態。至於具體的業務邏輯如何實現,不在本文討論的範圍之前,我們將其封裝在BusinessService類中。

  
  
  
package transaction;import com.aliyun.openservices.ons.api.Message;import com.aliyun.openservices.ons.api.transaction.LocalTransactionChecker;import com.aliyun.openservices.ons.api.transaction.TransactionStatus;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class LocalTransactionCheckerImpl implements LocalTransactionChecker {    private static Logger LOGGER = LoggerFactory.getLogger(LocalTransactionCheckerImpl.class);    private static BusinessService businessService = new BusinessService();    @Override    public TransactionStatus check(Message msg) {        // 從消息體中獲得的交易ID        String transactionKey = msg.getKey();        TransactionStatus transactionStatus = TransactionStatus.Unknow;        try {            boolean isCommit = businessService.checkbusinessService(transactionKey);            if (isCommit) {                transactionStatus = TransactionStatus.CommitTransaction;            } else {                transactionStatus = TransactionStatus.RollbackTransaction;            }        } catch (Exception e) {            LOGGER.error("Transaction Key:{}", transactionKey, e);        }        LOGGER.warn("Transaction Key:{}transactionStatus:{}", transactionKey, transactionStatus.name());        return transactionStatus;    }}

4、執行本地事務併發送異步消息


我們先組裝一條異步消息,其中包含了全局交易 ID,消息將要發往的 Topic,以及消息體。遠程事務參與方將通過這個消息體中獲取執行遠程事務所必須的數據信息。

接下來,將這條異步消息連同一個實現了LocalTransactionExecuter接口的匿名類,通過send方法進行發送,這就是本地事務參與方所需要實現的所有業務代碼了。當然,這個匿名類實現了TransactionStatus execute.execute()方法,其中包含了對於本地事務的執行。完整代碼如下:
  
  
  

package transaction;import com.aliyun.openservices.ons.api.Message;import com.aliyun.openservices.ons.api.ONSFactory;import com.aliyun.openservices.ons.api.PropertyKeyConst;import com.aliyun.openservices.ons.api.SendResult;import com.aliyun.openservices.ons.api.transaction.TransactionProducer;import com.aliyun.openservices.ons.api.transaction.TransactionStatus;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Properties;import java.util.concurrent.TimeUnit;public class TransactionProducerClient {    private static Logger LOGGER = LoggerFactory.getLogger(TransactionProducerClient.class);    private static final BusinessService businessService = new BusinessService();    private static final String TOPIC = "create_order";    private static final TransactionProducer producer = null;    static {        Properties properties = new Properties();        // 您在控制檯創建的Group ID。注意:事務消息的Group ID不能與其他類型消息的Group ID共用。        properties.put(PropertyKeyConst.GROUP_ID, "XXX");        // AccessKey ID阿里雲身份驗證,在阿里雲RAM控制檯創建。        properties.put(PropertyKeyConst.AccessKey, "XXX");        // AccessKey Secret阿里雲身份驗證,在阿里雲RAM控制檯創建。        properties.put(PropertyKeyConst.SecretKey, "XXX");        // 設置TCP接入域名,進入消息隊列RocketMQ版控制檯的實例詳情頁面的TCP協議客戶端接入點區域查看。        properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");        // LocalTransactionCheckerImpl本地事務回查類的實現        TransactionProducer producer = ONSFactory.createTransactionProducer(properties,                new LocalTransactionCheckerImpl());        producer.start();    }    public static void main(String[] args) throws InterruptedException {        String transactionKey = getGlobalTransactionKey();        String messageContent = String.format("lock inventory for: %s", transactionKey);        Message message = new Message(TOPIC, null, transactionKey, messageContent.getBytes());        try {            SendResult sendResult = producer.send(message, (msg, arg) -> {                // 此處用Lambda表示,實際是實現TransactionStatus execute(final Message msg, final Object arg)方法                TransactionStatus transactionStatus = TransactionStatus.Unknow;                try {                    boolean localTransactionOK = businessService.execbusinessService(transactionKey);                    if (localTransactionOK) {                        transactionStatus = TransactionStatus.CommitTransaction;                    } else {                        transactionStatus = TransactionStatus.RollbackTransaction;                    }                } catch (Exception e) {                    LOGGER.error("Transaction Key:{}", transactionKey, e);                }                LOGGER.warn("Transaction Key:{}", transactionKey);                return transactionStatus;            }, null);            LOGGER.info("send message OK, Transaction Key:{}, result:{}", message.getKey(), sendResult);        } catch (Exception e) {            LOGGER.info("send message failed, Transaction Key:{}", message.getKey());        }        // demo example防止進程退出        TimeUnit.MILLISECONDS.sleep(Integer.MAX_VALUE);    }    private static String getGlobalTransactionKey() {        // TODO        return "";    }}

得益於 RocketMQ SDK 優秀的封裝,發送半事務消息、發送確認消息、事務回查等重要步驟都已經完整實現,不需要開發者再編寫代碼了,這將爲用戶帶來特別順暢開發體驗。

遠程事務參與方的業務代碼


相對本地事務參與方而言,遠程事務參與方的代碼更加簡單,只需要從異步消息中提取出對應信息,完成對遠程事務的執行即可。

package transaction;import com.aliyun.openservices.ons.api.Action;import com.aliyun.openservices.ons.api.Consumer;import com.aliyun.openservices.ons.api.ONSFactory;import com.aliyun.openservices.ons.api.PropertyKeyConst;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Properties;public class TransactionConsumerClient {    private static Logger LOGGER = LoggerFactory.getLogger(TransactionProducerClient.class);    private static final BusinessService businessService = new BusinessService();    private static final String TOPIC = "create_order";    private static final Consumer consumer = null;    static {        Properties properties = new Properties();        // 在控制檯創建的Group ID,不同於本地事務參與方使用的Group ID        properties.put(PropertyKeyConst.GROUP_ID, "XXX");        // AccessKey ID阿里雲身份驗證,在阿里雲RAM控制檯創建。        properties.put(PropertyKeyConst.AccessKey, "XXX");        // Accesskey Secret阿里雲身份驗證,在阿里雲服RAM控制檯創建。        properties.put(PropertyKeyConst.SecretKey, "XXX");        // 設置TCP接入域名,進入控制檯的實例詳情頁面的TCP協議客戶端接入點區域查看。        properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");        Consumer consumer = ONSFactory.createConsumer(properties);        consumer.start();    }    public static void main(String[] args) {        consumer.subscribe(TOPIC, "*", (message, context) -> {                    LOGGER.info("Receive: " + message);                    businessService.doBusiness(message);                    // 返回CommitMessage,代表給予消息隊列集羣異步消息已經得到正常處理的回饋                    return Action.CommitMessage;                }        );    }}

事務回滾


是否存在這樣的情況:當本地事務執行成功後,因爲遠程事務沒有辦法執行,而導致本地事務需要進行回滾操作呢?在事務消息原理分析一節,我們已經介紹過如何通過消息重試,確保遠程事務能夠執行成功,這是不是已經說明只要異步消息被確認,遠程事務就一定可以執行成功,從而不存在對本地事務的回滾呢?

實際生產情況下,確實存在遠程事務無法正常執行的情況。比如在付款成功階段,當本地事務“修改訂單狀態”執行完成後,在執行遠程事務“通知發貨”的時,因爲訂單地址有誤而被物流公司拒絕,這種情況下就必須對訂單狀態進行回退操作,併發起退款流程。

所以在執行遠程事務的時候,我們有必要區分如下兩種完全不同的異常:

  • 技術異常:遠程事務參與方宕機、網絡故障、數據庫故障等。

  • 業務異常:遠程邏輯在業務上無法執行、代碼業務邏輯錯誤等。


簡單來講,當遠程事務執行失敗的時候,能夠通過消息重試的方式解決問題的,屬於技術異常;否則,屬於業務異常。基於事務消息的分佈式事務機制不能實現自動回滾,當業務異常發生的時候,必須通過回退流程確保已經完成的本地事務得到恢復。比如在修改訂單狀態 -> 通知發貨這個場景中,如果由於業務異常導致無法發貨的時候,需要通過額外的回退流程,將訂單狀態設置爲“已取消”,並執行退款流程。


在事務消息機制中,回退流程相當於遠程事務參與方和本地事務參與方調換了角色,和正常流程一樣,同樣也可以通過事務消息來完成分佈式事務。由於正常流程和回退流程的業務邏輯是完全不一樣的,所以最理想的方式是建立另外一個 Topic 來實現。這也就說明,我們在創建事務消息 Topic 的時候,要充分考慮到這個 Topic 背後的業務含義,並在 Topic 命名上儘可能的與真實業務相匹配

多個事務參與方


本節展示的示例中,都只涉及到 2 個事務參與方,但在真實世界中,分佈式事務往往涉及到更多的事務參與方,比如之前提到的付款成功環節,有修改訂單狀態->扣減庫存->通知發貨->增加積分這 4 個需要同時進行的操作,涉及到 4 個事務參與方。這種情況下如何通過事務消息來實現分佈式事務呢?

我們依然可以繼續使用之前的架構,只需要加入多個遠程事務參與方就行了。可以通過 RocketMQ 的多消費組關聯多個遠程事務參與方,每一個參與方對應一個 Group ID,在這種情況下,同一個異步消息會複製成多份投遞給不同的事務參與方。


需要特別引起注意的是,當某個遠程事務參與方遇到業務異常的時候,需要通知其他所有事務參與方執行回退流程,這無疑會增加業務邏輯的整體複雜度。爲了簡化事務消息的執行流程,我們可以對業務邏輯預先進行梳理,將子事務分爲如下兩類:

  • 有可能發生業務異常的:比如鎖定庫存的操作,有可能因爲庫存不足而執行失敗。又比如扣除積分的操作,有可能因爲用戶積分不足而無法扣除。

  • 不太可能發生業務異常的:比如刪除購物車條目的操作,除非是技術類故障,一定可以執行成功,即便對應的條目並不存在,也沒有關係。又比如積分增加的操作,只要對應的用戶沒有註銷,是不可能遇到業務異常的。


我們儘量將第一類事務作爲本地事務而實現,將第二類事務作爲遠程事務而實現,這樣就可以最大程度避免回退流程。



其他注意事項



消息冪等


RocketMQ 能保證消息不丟失,但不能保證消息不重複,所以消費者在接收到消息後,有必要根據業務上的唯一 Key 對消息做冪等處理。在搶購業務中,唯一 Key 當然就是全局唯一的交易流水號,具體冪等處理方法在互聯網上有很多文章供讀者參考。

當然,不是每一種業務遠程事務都需要確保消息的冪等性,比如刪除購物車指定條目這樣的操作,在業務上是可以容忍多次反覆執行的,就沒有必要引入額外的冪等處理了。

每日對賬


不同於傳統事務的強一致性保證,柔性事務需要經歷一箇中間狀態,纔到達成事務的最終一致性。有某些特殊情況下,這個中間狀態會持續非常長的時間,甚至需要人工主動介入才能實現最終一致性:

1、消息重試多次後,依然不成功:當消費者完全無法正常工作的時候,RocketMQ 不可能永無止境地重試消息,事實上,如果16次重試後異步消息依然沒有辦法被正常處理,RocketMQ 會停止嘗試,將消息放到一個特殊的隊列中。

2、未處理的業務異常:比如給某個賬號加積分的時候,發現此賬號被註銷了,這是一個非常罕見的業務現象,有可能事先對此並沒有健壯的處理機制。

3、冪等校驗失敗:處理冪等所依賴的系統比如 Redis 發生了故障,導致某些消息被重複處理。

4、其他嚴重的系統故障:比如網絡長時間中斷,留下了大量執行到一半的事務。

5、其他漏網之魚。


這些情況下,我們都有需要通過定期對賬機制來進行排查,在必要的時候發起人工主動介入流程,修復不一致的數據。事實上,在任何柔性事務的實現中,每日對賬都是必不可少的數據安全保障性手段。


總結



在柔性事務的多種實現中,事務消息是最爲優雅易用的一種。基於阿里雲 RocketMQ 高性能、高可用的特點,完全可以勝任搶購業務這類高併發大流量的場景。在阿里巴巴自身的業務中,事務消息也廣泛使用於雙 11 這樣的大型營銷活動中,有着非常高的通用性。

但在IT領域,沒有任何一種技術是銀彈,引入事務消息機制需要針對性的修改業務邏輯,還需要藉助於每日對賬等額外的手段確保數據安全,在實現高性能的同時,也增加了整體的業務複雜度。我們需要對業務場景進行充分評估,對比多種不同的技術實現方案,從中挑選與自身業務特點最爲匹配的一種,才能更好地發揮柔性事務的價值。

乾貨分享

最近將個人學習筆記整理成冊,使用PDF分享。關注我,回覆如下代碼,即可獲得百度盤地址,無套路領取!

001:《Java併發與高併發解決方案》學習筆記;002:《深入JVM內核——原理、診斷與優化》學習筆記;003:《Java面試寶典》004:《Docker開源書》005:《Kubernetes開源書》006:《DDD速成(領域驅動設計速成)》007:全部008:加技術羣討論

近期熱文

LinkedBlockingQueue vs ConcurrentLinkedQueue解讀Java 8 中爲併發而生的 ConcurrentHashMapRedis性能監控指標彙總最全的DevOps工具集合,再也不怕選型了!微服務架構下,解決數據庫跨庫查詢的一些思路聊聊大廠面試官必問的 MySQL 鎖機制

關注我

喜歡就點個"在看"唄^_^

本文分享自微信公衆號 - IT牧場(itmuch_com)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章