日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

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

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

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

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

Seata已經(jīng)為我們提供了兩種實(shí)現(xiàn)分布式模式:

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

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

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

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

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

問題

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

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

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

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

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

方法

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

重做記錄

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

CREATE TABLE `transaction_record` ( `orderId` int(11) unsigned NOT NULL AUTO_INCREMENT, `op_total` int(11) NOT NULL COMMENT '本次操作資源操作數(shù)量', `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;

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

有了這個重做記錄之后我們只需要在每一次執(zhí)行記錄下我們的當(dāng)前資源的transaction_record,在回滾的時候根據(jù)我們的OrderId將所有的資源回滾,我們優(yōu)化之后代碼可以如下:

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){ //這里進(jìn)行回滾 accountService.rollback(orderId, userId); coinService.rollback(orderId, userId); couponService.rollback(orderId, userId); updateOrderStatus(orderId, FAILED); }

這里我們將創(chuàng)建好的初始化訂單,當(dāng)作參數(shù)傳遞給我們的資源服務(wù)記錄,最后再進(jìn)行狀態(tài)更新,如果發(fā)生了異常,那么我們需要進(jìn)行 手動 回滾并將訂單數(shù)據(jù)變?yōu)镕AILED, 回滾的依據(jù)就是我們的訂單Id。對于我們的支付和回滾的偽代碼有如下:

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

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

重試機(jī)制

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

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

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

冪等

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

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

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

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

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

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

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

其他的代碼不變,通過這種形式我們完成了冪等。這時候有可能會有同學(xué)會問到,如果TransactionRecord不存在怎么辦?因?yàn)槲覀冎卦嚨臅r候我們怎么知道他的Try是否成功,我們這里是不知道的,所以我們這里還有策略保證我們的邏輯不會出現(xiàn)空指針,這里有兩種策略來做這個事:

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

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

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

總結(jié)

到這里我們整個構(gòu)建分布式事務(wù)基本大功告成了,通過這種方式基本上以后遇到相關(guān)分布式事務(wù)的業(yè)務(wù)問題的時候都可以解決。這里我們再回顧一下我們的三個要點(diǎn):

  • 重試記錄:通過數(shù)據(jù)記錄保存。
  • 重試機(jī)制:定時任務(wù)或者消息隊(duì)列自帶的重試。
  • 冪等:通過狀態(tài)機(jī)加數(shù)據(jù)庫行鎖。

我們只要能掌握好這三個點(diǎn),其實(shí)不僅僅是對分布式事務(wù)這一塊有幫助,對其他的業(yè)務(wù)同樣也有很大的提升。

end:如果你覺得本文對你有幫助的話,記得點(diǎn)贊轉(zhuǎn)發(fā),你的支持就是我更新動力。

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。