自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

分布式事務(wù)的六種解決方案,寫(xiě)得非常好!

開(kāi)發(fā) 后端 分布式
在分布式系統(tǒng)、微服務(wù)架構(gòu)大行其道的今天,服務(wù)間互相調(diào)用出現(xiàn)失敗已經(jīng)成為常態(tài)。如何處理異常,如何保證數(shù)據(jù)一致性,成為微服務(wù)設(shè)計(jì)過(guò)程中,繞不開(kāi)的一個(gè)難題。

[[407285]]

介紹

在分布式系統(tǒng)、微服務(wù)架構(gòu)大行其道的今天,服務(wù)間互相調(diào)用出現(xiàn)失敗已經(jīng)成為常態(tài)。如何處理異常,如何保證數(shù)據(jù)一致性,成為微服務(wù)設(shè)計(jì)過(guò)程中,繞不開(kāi)的一個(gè)難題。 

在不同的業(yè)務(wù)場(chǎng)景下,解決方案會(huì)有所差異,常見(jiàn)的方式有:

  1.  阻塞式重試;
  2.  2PC、3PC 傳統(tǒng)事務(wù);
  3.  使用隊(duì)列,后臺(tái)異步處理;
  4.  TCC 補(bǔ)償事務(wù);
  5.  本地消息表(異步確保);
  6.  MQ 事務(wù)。

本文側(cè)重于其他幾項(xiàng),關(guān)于 2PC、3PC 傳統(tǒng)事務(wù),網(wǎng)上資料已經(jīng)非常多了,這里不多做重復(fù)。

阻塞式重試

在微服務(wù)架構(gòu)中,阻塞式重試是比較常見(jiàn)的一種方式。

偽代碼示例: 

  1. :db.Insert(sql)  
  2. err :request(B-Service,m)  
  3. func request(url string,body interface{}){  
  4.   for i:=0; i<3; i ++ {  
  5.     result, err = request.POST(url,body)  
  6.     if err == nil {  
  7.         break   
  8.     }else {  
  9.       log.Print()  
  10.     }  
  11.   }  

如上,當(dāng)請(qǐng)求 B 服務(wù)的 API 失敗后,發(fā)起最多三次重試。如果三次還是失敗,就打印日志,繼續(xù)執(zhí)行下或向上層拋出錯(cuò)誤。

這種方式會(huì)帶來(lái)以下問(wèn)題:

  1.  調(diào)用 B 服務(wù)成功,但由于網(wǎng)絡(luò)超時(shí)原因,當(dāng)前服務(wù)認(rèn)為其失敗了,繼續(xù)重試,這樣 B 服務(wù)會(huì)產(chǎn)生 2 條一樣的數(shù)據(jù)。
  2.  調(diào)用 B 服務(wù)失敗,由于 B 服務(wù)不可用,重試 3 次依然失敗,當(dāng)前服務(wù)在前面代碼中插入到 DB 的一條記錄,就變成了臟數(shù)據(jù)。
  3.  重試會(huì)增加上游對(duì)本次調(diào)用的延遲,如果下游負(fù)載較大,重試會(huì)放大下游服務(wù)的壓力。

第一個(gè)問(wèn)題:通過(guò)讓 B 服務(wù)的 API 支持冪等性來(lái)解決。

第二個(gè)問(wèn)題:可以通過(guò)后臺(tái)定時(shí)腳步去修正數(shù)據(jù),但這并不是一個(gè)很好的辦法。

第三個(gè)問(wèn)題:這是通過(guò)阻塞式重試提高一致性、可用性,必不可少的犧牲。

阻塞式重試適用于業(yè)務(wù)對(duì)一致性要求不敏感的場(chǎng)景下。如果對(duì)數(shù)據(jù)一致性有要求的話,就必須要引入額外的機(jī)制來(lái)解決。

異步隊(duì)列

在解決方案演化的過(guò)程中,引入隊(duì)列是個(gè)比較常見(jiàn)也較好的方式。如下示例: 

  1. :db.Insert(sql)  
  2. err :mq.Publish("B-Service-topic",m) 

在當(dāng)前服務(wù)將數(shù)據(jù)寫(xiě)入 DB 后,推送一條消息給 MQ,由獨(dú)立的服務(wù)去消費(fèi) MQ 處理業(yè)務(wù)邏輯。和阻塞式重試相比,雖然 MQ 在穩(wěn)定性上遠(yuǎn)高于普通的業(yè)務(wù)服務(wù),但在推送消息到 MQ 中的調(diào)用,還是會(huì)有失敗的可能性,比如網(wǎng)絡(luò)問(wèn)題、當(dāng)前服務(wù)宕機(jī)等。這樣還是會(huì)遇到阻塞式重試相同的問(wèn)題,即 DB 寫(xiě)入成功了,但推送失敗了。

理論上來(lái)講,分布式系統(tǒng)下,涉及多個(gè)服務(wù)調(diào)用的代碼都存在這樣的情況,在長(zhǎng)期運(yùn)行中,調(diào)用失敗的情況一定會(huì)出現(xiàn)。這也是分布式系統(tǒng)設(shè)計(jì)的難點(diǎn)之一。另外,MQ 系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺(tái)發(fā)送:面試,可以在線閱讀。

TCC 補(bǔ)償事務(wù)

在對(duì)事務(wù)有要求,且不方便解耦的情況下,TCC 補(bǔ)償式事務(wù)是個(gè)較好的選擇。

TCC 把調(diào)用每個(gè)服務(wù)都分成 2 個(gè)階段、 3 個(gè)操作:

  •  階段一、Try 操作:對(duì)業(yè)務(wù)資源做檢測(cè)、資源預(yù)留,比如對(duì)庫(kù)存的檢查、預(yù)扣。
  •  階段二、Confirm 操作:提交確認(rèn) Try 操作的資源預(yù)留。比如把庫(kù)存預(yù)扣更新為扣除。
  •  階段二、Cancel 操作:Try 操作失敗后,釋放其預(yù)扣的資源。比如把庫(kù)存預(yù)扣的加回去。

TCC 要求每個(gè)服務(wù)都實(shí)現(xiàn)上面 3 個(gè)操作的 API,服務(wù)接入 TCC 事務(wù)前一次調(diào)用就完成的操作,現(xiàn)在需要分 2 階段完成、三次操作來(lái)完成。

比如一個(gè)商城應(yīng)用需要調(diào)用 A 庫(kù)存服務(wù)、B 金額服務(wù)、C 積分服務(wù),如下偽代碼: 

  1. :db.Insert(sql)  
  2. aResult, aErr :A.Try(m)  
  3. bResult, bErr :B.Try(m)  
  4. cResult, cErr :C.Try(m)  
  5. if cErr != nil {  
  6.     A.Cancel()  
  7.     B.Cancel()  
  8.  C.Cancel()  
  9. } else {  
  10.     A.Confirm()  
  11.     B.Confirm()  
  12.     C.Confirm()  

代碼中分別調(diào)用 A、B、C 服務(wù) API 檢查并保留資源,都返回成功了再提交確認(rèn)(Confirm)操作;如果 C 服務(wù) Try 操作失敗后,則分別調(diào)用 A、B、C 的 Cancel API 釋放其保留的資源。

TCC 在業(yè)務(wù)上解決了分布式系統(tǒng)下,跨多個(gè)服務(wù)、跨多個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)一致性問(wèn)題。但 TCC 方式依然存在一些問(wèn)題,實(shí)際使用中需要注意,包括上面章節(jié)提到的調(diào)用失敗的情況。

空釋放

上面代碼中如果 C.Try() 是真正調(diào)用失敗,那下面多余的 C.Cancel() 調(diào)用會(huì)出現(xiàn)釋放并沒(méi)有鎖定資源的行為。這是因?yàn)楫?dāng)前服務(wù)無(wú)法判斷調(diào)用失敗是不是真的鎖定 C 資源了。如果不調(diào)用,實(shí)際上成功了,但由于網(wǎng)絡(luò)原因返回失敗了,這會(huì)導(dǎo)致 C 的資源被鎖定,一直得不到釋放。

空釋放在生產(chǎn)環(huán)境經(jīng)常出現(xiàn),服務(wù)在實(shí)現(xiàn) TCC 事務(wù) API 時(shí),應(yīng)支持空釋放的執(zhí)行。

時(shí)序

上面代碼中如果 C.Try() 失敗,接著調(diào)用 C.Cancel() 操作。因?yàn)榫W(wǎng)絡(luò)原因,有可能會(huì)出現(xiàn) C.Cancel() 請(qǐng)求會(huì)先到 C 服務(wù),C.Try() 請(qǐng)求后到,這會(huì)導(dǎo)致空釋放問(wèn)題,同時(shí)引起 C 的資源被鎖定,一直得不到釋放。

所以 C 服務(wù)應(yīng)拒絕釋放資源之后的 Try() 操作。具體實(shí)現(xiàn)上,可以用唯一事務(wù)ID來(lái)區(qū)分第一次 Try() 還是釋放后的 Try()。

調(diào)用失敗

Cancel 、Confirm 在調(diào)用過(guò)程中,還是會(huì)存在失敗的情況,比如常見(jiàn)的網(wǎng)絡(luò)原因。

Cancel() 或 Confirm() 操作失敗都會(huì)導(dǎo)致資源被鎖定,一直得不到釋放。這種情況常見(jiàn)解決方案有:

  1.  阻塞式重試。但有同樣的問(wèn)題,比如宕機(jī)、一直失敗的情況。
  2.  寫(xiě)入日志、隊(duì)列,然后有單獨(dú)的異步服務(wù)自動(dòng)或人工介入處理。但一樣會(huì)有問(wèn)題,寫(xiě)日志或隊(duì)列時(shí),會(huì)存在失敗的情況。

理論上來(lái)講非原子性、事務(wù)性的二段代碼,都會(huì)存在中間態(tài),有中間態(tài)就會(huì)有失敗的可能性。

本地消息表

本地消息表最初是 ebay 提出的,它讓本地消息表與業(yè)務(wù)數(shù)據(jù)表處于同一個(gè)數(shù)據(jù)庫(kù)中,這樣就能利用本地事務(wù)來(lái)滿足事務(wù)特性。

具體做法是在本地事務(wù)中插入業(yè)務(wù)數(shù)據(jù)時(shí),也插入一條消息數(shù)據(jù)。然后在做后續(xù)操作,如果其他操作成功,則刪除該消息;如果失敗則不刪除,異步監(jiān)聽(tīng)這個(gè)消息,不斷重試。

本地消息表是一個(gè)很好的思路,可以有多種使用方式:

配合MQ

示例偽代碼: 

  1. messageTx :tc.NewTransaction("order")  
  2. messageTxSql :tx.TryPlan("content")  
  3. m,err :db.InsertTx(sql,messageTxSql)  
  4. if err!=nil {  
  5.  return err  
  6.  
  7. aErr :mq.Publish("B-Service-topic",m) 
  8. if aErr!=nil { // 推送到 MQ 失敗  
  9.  messageTx.Confirm() // 更新消息的狀態(tài)為 confirm  
  10. }else {  
  11.  messageTx.Cancel() // 刪除消息  
  12.   
  13. // 異步處理 confirm 的消息,繼續(xù)推送  
  14. func OnMessage(task *Task){  
  15.    err :mq.Publish("B-Service-topic", task.Value()) 
  16.    if err==nil {  
  17.      messageTx.Cancel()  
  18.    }  

上面代碼中其 messageTxSql 是插入本地消息表的一段 SQL : 

  1. insert into `tcc_async_task` (`uid`,`name`,`value`,`status`)   
  2. values ('?','?','?','?') 

它和業(yè)務(wù) SQL 在同一個(gè)事務(wù)中去執(zhí)行,要么成功,要么失敗。

成功則推送到隊(duì)列,推送成功,則調(diào)用 messageTx.Cancel() 刪除本地消息;推送失敗則標(biāo)記消息為 confirm。本地消息表中 status 有 2 種狀態(tài) try、confirm, 無(wú)論哪種狀態(tài)在 OnMessage 都可以監(jiān)聽(tīng)到,從而發(fā)起重試。

本地事務(wù)保障消息和業(yè)務(wù)一定會(huì)寫(xiě)入數(shù)據(jù)庫(kù),此后的執(zhí)行無(wú)論宕機(jī)還是網(wǎng)絡(luò)推送失敗,異步監(jiān)聽(tīng)都可以進(jìn)行后續(xù)處理,從而保障了消息一定會(huì)推到 MQ。

而 MQ 則保障一定會(huì)到達(dá)消費(fèi)者服務(wù)中,利用 MQ 的 QOS 策略,消費(fèi)者服務(wù)一定能處理,或繼續(xù)投遞到下一個(gè)業(yè)務(wù)隊(duì)列中,從而保障了事務(wù)的完整性。

配合服務(wù)調(diào)用

示例偽代碼: 

  1. messageTx :tc.NewTransaction("order")  
  2. messageTxSql :tx.TryPlan("content")  
  3. body,err :db.InsertTx(sql,messageTxSql)  
  4. if err!=nil {  
  5.     return err  
  6.  
  7. aErr :request.POST("B-Service",body)  
  8. if aErr!=nil { // 調(diào)用 B-Service 失敗  
  9.  messageTx.Confirm() // 更新消息的狀態(tài)為 confirm  
  10. }else {  
  11.  messageTx.Cancel() // 刪除消息  
  12.  
  13. // 異步處理 confirm 或 try 的消息,繼續(xù)調(diào)用 B-Service   
  14. func OnMessage(task *Task){  
  15.   // request.POST("B-Service",body)  

這是本地消息表 + 調(diào)用其他服務(wù)的例子,沒(méi)有 MQ 的引入。這種使用異步重試,并用本地消息表保障消息的可靠性,解決了阻塞式重試帶來(lái)的問(wèn)題,在日常開(kāi)發(fā)中比較常見(jiàn)。

如果本地沒(méi)有要寫(xiě) DB 的操作,可以只寫(xiě)入本地消息表,同樣在 OnMessage中處理: 

  1. messageTx :tc.NewTransaction("order")  
  2. messageTx :tx.Try("content")  
  3. aErr :request.POST("B-Service",body) 
  4. // .... 

消息過(guò)期

配置本地消息表的 Try 和 Confirm 消息的處理器: 

  1. TCC.SetTryHandler(OnTryMessage())  
  2. TCC.SetConfirmHandler(OnConfirmMessage()) 

在消息處理函數(shù)中要判斷當(dāng)前消息任務(wù)是否存在過(guò)久,比如一直重試了一小時(shí),還是失敗,就考慮發(fā)郵件、短信、日志告警等方式,讓人工介入。 

  1. func OnConfirmMessage(task *tcc.Task) {  
  2. if time.Now().Sub(task.CreatedAt) > time.Hour {  
  3.     err :task.Cancel()  // 刪除該消息,停止重試。  
  4.    // doSomeThing() 告警,人工介入  
  5.     return  
  6.  }  

在 Try 處理函數(shù)中,還要單獨(dú)判斷當(dāng)前消息任務(wù)是否存在過(guò)短,因?yàn)?Try狀態(tài)的消息,可能才剛剛創(chuàng)建,還沒(méi)被確認(rèn)提交或刪除。這會(huì)和正常業(yè)務(wù)邏輯的執(zhí)行重復(fù),意味著成功的調(diào)用,也會(huì)被重試;為盡量避免這種情況,可以檢測(cè)消息的創(chuàng)建時(shí)間是否很短,短的話可以跳過(guò)。

重試機(jī)制必然依賴下游 API 在業(yè)務(wù)邏輯上的冪等性,雖然不處理也可行,但設(shè)計(jì)上還是要盡量避免干擾正常的請(qǐng)求。另外,推薦 Java 核心技術(shù)教程和示例源碼:https://github.com/javastacks/javastack

獨(dú)立消息服務(wù)

獨(dú)立消息服務(wù)是本地消息表的升級(jí)版,把本地消息表抽離成一個(gè)獨(dú)立的服務(wù)。所有操作之前先在消息服務(wù)添加個(gè)消息,后續(xù)操作成功則刪除消息,失敗則提交確認(rèn)消息。

然后用異步邏輯去監(jiān)聽(tīng)消息,做對(duì)應(yīng)的處理,和本地消息表的處理邏輯基本一致。但由于向消息服務(wù)添加消息,無(wú)法和本地操作放到一個(gè)事務(wù)里,所以會(huì)存在添加消息成功,后續(xù)失敗,則此時(shí)的消息就是個(gè)無(wú)用消息。

如下示例場(chǎng)景: 

  1. err :request.POST("Message-Service",body)  
  2. if err!=nil {  
  3.   return err  
  4.  
  5. aErr :request.POST("B-Service",body)  
  6. if aErr!=nil { 
  7.   return aErr  

這個(gè)無(wú)用的消息,需要消息服務(wù)去確認(rèn)這個(gè)消息是否執(zhí)行成功,沒(méi)有則刪除,有繼續(xù)執(zhí)行后續(xù)邏輯。相比本地事務(wù)表 try 和 confirm ,消息服務(wù)在前面多了一種狀態(tài) prepare。

MQ 事務(wù)

有些 MQ 的實(shí)現(xiàn)支持事務(wù),比如 RocketMQ 。MQ 的事務(wù)可以看作獨(dú)立消息服務(wù)的一種具體實(shí)現(xiàn),邏輯完全一致。

所有操作之前先在 MQ 投遞個(gè)消息,后續(xù)操作成功則 Confirm 確認(rèn)提交消息,失敗則Cancel刪除消息。MQ 事務(wù)也會(huì)存在 prepare狀態(tài),需要 MQ 的消費(fèi)處理邏輯來(lái)確認(rèn)業(yè)務(wù)是否成功。

總結(jié)

從分布式系統(tǒng)實(shí)踐中來(lái)看,要保障數(shù)據(jù)一致性的場(chǎng)景,必然要引入額外的機(jī)制處理。

TCC 的優(yōu)點(diǎn)是作用于業(yè)務(wù)服務(wù)層,不依賴某個(gè)具體數(shù)據(jù)庫(kù)、不與具體框架耦合、資源鎖的粒度比較靈活,非常適用于微服務(wù)場(chǎng)景下。缺點(diǎn)是每個(gè)服務(wù)都要實(shí)現(xiàn) 3 個(gè) API,對(duì)于業(yè)務(wù)侵入和改動(dòng)較大,要處理各種失敗異常。開(kāi)發(fā)者很難完整處理各種情況,找個(gè)成熟的框架可以大大降低成本,比如阿里的 Fescar。

本地消息表的優(yōu)點(diǎn)是簡(jiǎn)單、不依賴其他服務(wù)的改造、可以很好的配合服務(wù)調(diào)用和 MQ 一起使用,在大多業(yè)務(wù)場(chǎng)景下都比較實(shí)用。缺點(diǎn)是本地?cái)?shù)據(jù)庫(kù)多了消息表,和業(yè)務(wù)表耦合在一起。文中本地消息表方式的示例,來(lái)源于作者寫(xiě)的一個(gè)庫(kù),有興趣的同學(xué)可以參考下 https://github.com/mushroomsir/tcc

MQ 事務(wù)和獨(dú)立消息服務(wù)的優(yōu)點(diǎn)是抽離出一個(gè)公共的服務(wù)來(lái)解決事務(wù)問(wèn)題,避免每個(gè)服務(wù)都有消息表和服務(wù)耦合在一起,增加服務(wù)自身的處理復(fù)雜性。缺點(diǎn)是支持事務(wù)的 MQ 很少;且每次操作前都先調(diào)用 API 添加個(gè)消息,會(huì)增加整體調(diào)用的延遲,在絕大多數(shù)正常響應(yīng)的業(yè)務(wù)場(chǎng)景下,是一種多余的開(kāi)銷(xiāo)。

 

責(zé)任編輯:龐桂玉 來(lái)源: Java技術(shù)棧
相關(guān)推薦

2025-04-29 04:00:00

分布式事務(wù)事務(wù)消息

2024-03-26 12:08:53

分布式事務(wù)存儲(chǔ)

2019-07-25 15:32:35

分布式事務(wù)微服務(wù)系統(tǒng)架構(gòu)

2023-09-14 15:44:46

分布式事務(wù)數(shù)據(jù)存儲(chǔ)

2020-05-28 09:35:05

分布式事務(wù)方案

2020-03-31 16:13:26

分布式事務(wù)方案TCC

2019-04-28 11:01:05

云安全云計(jì)算CASB

2025-04-28 00:44:04

2010-07-21 13:53:41

SQL Server分

2025-05-07 00:10:00

分布式事務(wù)TCC模式

2022-07-06 12:03:55

事務(wù)解決方案

2023-11-30 07:19:08

.NET開(kāi)源

2019-01-11 18:22:07

阿里巴巴技術(shù)開(kāi)源

2021-09-28 09:43:11

微服務(wù)架構(gòu)技術(shù)

2024-06-13 08:04:23

2021-04-30 20:10:46

類加載Java代碼

2021-06-28 10:03:44

分布式數(shù)據(jù)庫(kù)架構(gòu)

2024-12-09 09:35:00

2019-09-09 10:09:51

分布式事務(wù) 數(shù)據(jù)庫(kù)

2023-03-05 18:23:38

分布式ID節(jié)點(diǎn)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)