日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

seata 如何开启tcc事物_如何能在实战中完成分布式事务?知道这些点很重要

發布時間:2025/3/19 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 seata 如何开启tcc事物_如何能在实战中完成分布式事务?知道这些点很重要 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在這篇文章中我詳細介紹了分布式事務是什么,實現分布式事務有哪些常用的方案,但是其中的東西很多是偏于理論,很多讀者對其真正在實戰上的使用可能還是有點差距。所以在前幾次文章的更新中,我介紹了很多關于Seata(一款由阿里開源的分布式事務框架)的內容,如果大家對Seata不是很熟悉的可以閱讀下面的內容:

  • 解密分布式事務框架-Seata
  • 深度剖析一站式分布式事務方案Seata-Server
  • 深度剖析一站式分布式事務方案Seata-Cient

Seata已經為我們提供了兩種實現分布式模式:

  • AT:自動模式,通過我們記錄運行sql的undolog,來完成事務失敗時的自動重做。
  • TCC:TCC模式,這種模式彌補我們AT模式只能支持ACID數據庫的場景。

大多數時候Seata已經足夠了,但是很多時候不同場景下我們沒辦法選擇Seata這類TCC框架:

  • 改造困難,目前Seata支持的通信框架不多只有Dubbo和Spring-Cloud-Alibaba,如果使用的是其他框架,或者直接是簡單的HTTP,甚至有些公司可能目前系統中都沒有支持Trace。
  • 維護成本高,Seata需要一個單獨的集群去維護,一般在公司都需要分配一定的資源(人員資源,機器資源)去管理維護Seata,很多時候不可能為了幾個分布式事務去花費這么大的成本,當然這一塊的話未來可以上云解決。

而我最近在做一些分布式事務的事的時候也遇到了這些問題,由于一般使用分布式事務是業務方,你需要驅動做RPC組件的同事支持,并且我們并不是純金融服務的公司,搭建一套類似Seata的分布式事務中間件也是比較耗費資源。

之前介紹的方案大多數都比較籠統,俗話說授人以魚不如授人以漁,所以接下來我將會一步一步的教大家如何不用框架,而是我們自己去編碼去實現分布式事務。

問題

為了更好的講解如何在實戰中完成分布式事務,這里直接舉一個大家都熟悉的例子:用戶下單的時候,可以選擇三種資產,分別是儲值余額,積分,券,這個場景幾乎在每個應用都能看見,而這個場景在我們的后端可以映射為4個服務,如下圖所示:

在這個場景下大多數人的代碼基本會按照下面的寫,在訂單服務中有如下步驟,這里為了簡單沒有設置過多的訂單狀態:

  • Step 1:創建訂單狀態為初始化,并檢查用戶所有資源是否足夠
  • Step 2:支付儲值余額
  • Step 3:支付券
  • Step 4:支付金幣
  • Step 5:更新訂單狀態為已完成

差不多這里就是簡簡單單4行,有很多人會把這5步直接放進事務之中,也就是加上@Transactional注解,但其實加上這個注解不僅沒有起到事務作用,而且還讓我們的事務變成了長事務,我們這里的Step2-4都是RPC遠程調用,一旦某個RPC出現了Timeout,那么我們的數據庫連接會被長期持有不被釋放,有可能導致我們系統雪崩。

既然這里加上事務沒有用,我們可以看看會出現什么問題,如果Step2支付成功,Step3失敗,那么就會導致數據不一致。其實很多人就會有僥幸心理,默認我們的Step 2-4會成功,如果出現問題我們人工修復就是了。人工修復的成本太高,你就想如果你在外面旅游突然叫你修復數據,那你是不是會氣得吐血?所以我們這里一步一步的教大家如何逐漸的把這段業務邏輯優化成能保證我們數據一致的。

方法

一般來說任何一個分布式事務框架都離不開三個關鍵字:重做記錄,重試機制,冪等。而在我們的業務中同樣也離不開這三個關鍵字。

重做記錄

我們想想我們mysql的事務回滾是依靠什么的?依靠的是undolog,我們的undolog保存了事務發生之前的數據的一個版本,那么我們發生回滾的時候直接利用這個版本的數據回滾即可。這里我們首先需要添加我們的重做記錄,我們沒必要叫undolog,我們再各個資源服務中需要添加一個事務記錄表:

CREATE TABLE `transaction_record` ( `orderId` int(11) unsigned NOT NULL AUTO_INCREMENT, `op_total` int(11) NOT NULL COMMENT '本次操作資源操作數量', `status` int(11) NOT NULL COMMENT '1:代表支付成功 2:代表支付取消', `resource_id` int(11) NOT NULL COMMENT '本次操作資源的Id', `user_id` int(11) NOT NULL COMMENT '本次操作資源的用戶Id', PRIMARY KEY (`orderId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

在我們的分布式事務中有一個全局事務ID,而我們 orderId 就能很好的適應這個角色,這里我們每個資源的事務記錄表都需要記錄這個OrderId,用于和全局事務進行關聯,并且我們這里直接將其作為主鍵,也表明了這個表中只會出現一次全局事務ID。這里的 op_total 用于記錄本次操作資源的數量,用于后續回滾,哪怕不回滾我們也可以用于后續記錄的查詢。 status 用于記錄我們當前這條記錄的狀態如何,這里用了兩個狀態,后續我們可以擴展更多的狀態,解決更多的分布式事務問題。

有了這個重做記錄之后我們只需要在每一次執行記錄下我們的當前資源的transaction_record,在回滾的時候根據我們的OrderId將所有的資源回滾,我們優化之后代碼可以如下:

int orderId = createInitOrder(); checkResourceEnough(); try { accountService.payAccount(orderId, userId, opTotal); coinService.payCoin(orderId, userId, opTotal); couponService.payCoupon(orderId, userId, couponId); updateOrderStatus(orderId, PAID); }catch (Exception e){ //這里進行回滾 accountService.rollback(orderId, userId); coinService.rollback(orderId, userId); couponService.rollback(orderId, userId); updateOrderStatus(orderId, FAILED); }

這里我們將創建好的初始化訂單,當作參數傳遞給我們的資源服務記錄,最后再進行狀態更新,如果發生了異常,那么我們需要進行 手動 回滾并將訂單數據變為FAILED, 回滾的依據就是我們的訂單Id。對于我們的支付和回滾的偽代碼有如下:

@Transactional void payAccount(int orderId, int userId, int opTotal){ account.payAccount(userId, opTotal); // 實際的去我們account表扣減 transactionRecordStorage.save(orderId, userId, opTotal, account.getId()); //保存事務記錄表 } @Transactional void rollback(int orderId, int userId){ TransactionRecord tr = transactionRecordStorage.get(orderId); //從記錄表中查詢 account.rollbackBytr(tr); // 根據記錄回滾 }

這里的版本是比較簡略的,問題還比較多后面會講優化。

重試機制

有些同學可能會問好像我們上面的代碼基本能保證分布式事務了吧?的確上面的代碼能保證我們在沒有宕機或者其他更加嚴重的情況下基本上是沒有問題的,但是如果出現了宕機,比如我們剛剛把account給支付完了,然后支付coin的時候我們的訂單機器宕機了,沒有發出去這個請求,這里就不會走到我們的手動回滾請求,所以我們的account將會永遠不會回滾,又只得靠我們的人工回滾,如果你此時還在旅游,又叫你回滾,估計你會繼續氣暈?;蛘哒f我們再回滾的時候出現錯誤,怎么辦?我們沒有有效的手段進行針對回滾的回滾。

所以我們需要額外的重試機制來保證,首先我們需要定義什么樣的數據需要重試,這里的話我們根據業務差不多一分鐘能將所有的都資源都支付完,如果我們的訂單狀態為init 并且 創建時間超過一分鐘,那么就認為發生了上述錯誤的事件。接下來可以通過我們的重試機制進行回滾,這里有兩個常見重試機制:

  • 定時任務:定時任務是我們最常見的重試機制,基本所有的分布式事務框架中也都是通過定時任務去做的,這里我們需要使用分布式的定時任務,分布式的定時任務可以使用單機任務+分布式鎖 或者 直接使用開源的分布式任務中間件如elastic-job。我們在分布式任務的邏輯中每次查詢我們的處于訂單狀態為init 并且 創建時間超過一分鐘的訂單,我們對其進行回滾,回滾完成之后將訂單狀態置為FAILED。
  • 消息隊列:目前我們業務上使用的是消息隊列,將下單操作放入消息隊列中去做,如果我們出現了各種異常,那么我們依靠消息隊列的重試機制,一般來說現在當前隊列進行重試,再丟給死信隊列去重試。這里的邏輯就需要改一下,在我們創建訂單的時候有可能訂單已經存在,如果存在的話我們判斷他的狀態(init+1min)是否應該被直接rollback,如果是則直接1min。為什么我們選擇了消息隊列進行重試? 因為我們的業務邏輯是依靠消息隊列的,我們就不需要引入定時任務,直接依靠消息隊列即可。

冪等

判斷一個程序猿經驗是否老道可以從他寫代碼的時候能否考慮到冪等就可以看出。很多年輕的程序員根本不會考慮冪等的存在,甚至都不知道冪等是什么。這里先解釋一下冪等的概念:可以簡單的認為任意多次執行所產生的影響和一次執行的影響相同。

為什么我們完成分布式事務的時候需要冪等?大家可以想想如果在執行回滾操作的時候宕機了,我們上面的重試機制就會開始工作,比如我們的券這個資源已經回滾,但是我們重試操作的時候我并不知道券已經回滾了,這個時候就再次嘗試回滾券,如果沒有做冪等操作會怎么辦,有可能導致用戶資產會多增加,這樣就會對公司造成很多損失。

所以冪等在我們重試的時候非常重要,實現冪等的關鍵是什么?我們想讓多次操作和一次操作是一樣的,那么我們只需要比較第一次已經做過了,而這個標記通過什么來完成呢?這里我們可以使用我們狀態機轉換的手段完成標記。只有標記這里還是不夠,為什么呢這里我們用個例子來說明一下,把上面的rollback簡單優化一下:

@Transactional void rollback(int orderId, int userId){ TransactionRecord tr = transactionRecordStorage.get(orderId); if(tr.isCanceled()){ return; //如果已經被取消了那么直接返回 } //從記錄表中查詢 account.rollbackBytr(tr); // 根據記錄回滾 }

上面代碼我們通過判斷狀態如果是已經被取消了,也就是被回滾了那么我們就直接返回,這里就完成了我們所說的冪等。但是這里還有個問題是如果有兩個rollback同時執行怎么辦?你可能會問什么樣的情況可能會有兩個rollback,這里舉一個場景當第一次rollback的時候請求在阻塞了,這個時候調用方已經觸發超時了,然后一段時間之后第二次rollback來了,這個時候恰好第一次也不阻塞了,那么這里就會有兩個rollback請求發出,當執行狀態判斷的時候,如果兩個請求同時執行狀態判斷,那么都會繞過這個檢查,最后用戶就會退兩次錢,這樣的情況我們一定要避免。

那么怎么才能避免呢?聰明的同學馬上就會想到使用分布式鎖呀,一提到分布式鎖馬上想到的就是Redis加鎖,ZK加鎖等等,我在這篇文章也做了介紹:聊聊分布式鎖,但是我們這里直接使用數據庫行鎖即可,也就是用下面的sql語句查詢:

select * from transaction where orderId = "#{orderId}" for update;

其他的代碼不變,通過這種形式我們完成了冪等。這時候有可能會有同學會問到,如果TransactionRecord不存在怎么辦?因為我們重試的時候我們怎么知道他的Try是否成功,我們這里是不知道的,所以我們這里還有策略保證我們的邏輯不會出現空指針,這里有兩種策略來做這個事:

  • 如果為空我們直接返回即可。
  • 如果為空,我們保存一條Status為已執行空回滾狀態的TransactionRecord。

上面的第一個策略比較簡單,但是我們這里需要選擇第二個策略,為什么呢因為我們還需要預防一個事情:防懸掛,我們再說rollback冪等的時候,如果第一個rollback發生網絡阻塞,那么這里我們將rollback替換成我們第一次支付的時候發生了阻塞,導致了pay在rollback之后到達我們的客戶端,如果我們采用第一種方式,我們這個阻塞的Pay請求時無法感知整個事務因為rollback,然后繼續pay導致我們這個pay永遠得不到回滾,這就是懸掛。所以我們這里采用第二個策略,保存一條記錄,我們在pay也會檢查有沒有這條記錄,所以優化之后的代碼為:

@Transactional void payAccount(int orderId, int userId, int opTotal){ TransactionRecord tr = transactionRecordStorage.getForUpdate(orderId); if(tr != null){ return; //如果已經有數據了,這里直接返回 } account.payAccount(userId, opTotal); // 實際的去我們account表扣減 transactionRecordStorage.save(orderId, userId, opTotal, account.getId()); //保存事務記錄表 } @Transactional void rollback(int orderId, int userId){ TransactionRecord tr = transactionRecordStorage.getForUpdate(orderId); if(tr == null){ saveNullCancelTr(orderId, userId); //保存空回滾的記錄 } if(tr.isCanceled() || tr.isNullCancel()){ return; //如果已經被取消了那么直接返回 } //從記錄表中查詢 account.rollbackBytr(tr); // 根據記錄回滾 }

總結

到這里我們整個構建分布式事務基本大功告成了,通過這種方式基本上以后遇到相關分布式事務的業務問題的時候都可以解決。這里我們再回顧一下我們的三個要點:

  • 重試記錄:通過數據記錄保存。
  • 重試機制:定時任務或者消息隊列自帶的重試。
  • 冪等:通過狀態機加數據庫行鎖。

我們只要能掌握好這三個點,其實不僅僅是對分布式事務這一塊有幫助,對其他的業務同樣也有很大的提升。

end:如果你覺得本文對你有幫助的話,記得點贊轉發,你的支持就是我更新動力。

總結

以上是生活随笔為你收集整理的seata 如何开启tcc事物_如何能在实战中完成分布式事务?知道这些点很重要的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。