Seata分布式事務(wù)出現(xiàn)ABA問(wèn)題解決
前言
兄弟們,最近處理了一個(gè)seata的issue,關(guān)于seata分布式事務(wù)長(zhǎng)期回滾失敗后,突然回滾成功了:
這個(gè)問(wèn)題的出現(xiàn)需要以下兩個(gè)契機(jī):
- 在執(zhí)行分布式事務(wù)期間,有本地事務(wù)與分布式事務(wù)操作同一張表中的數(shù)據(jù)導(dǎo)致臟寫產(chǎn)生;
- 在回滾時(shí),seata對(duì)比
afterImage
與當(dāng)前數(shù)據(jù)不一致,導(dǎo)致回滾失敗,此時(shí)會(huì)一直重試; - 當(dāng)手工校準(zhǔn)數(shù)據(jù)后,某一時(shí)刻
afterImage
與當(dāng)前數(shù)據(jù)一致,此時(shí)回滾重試成功,ABA問(wèn)題產(chǎn)生;
從源碼中定位原因
為了避免ABA
問(wèn)題的產(chǎn)生,通過(guò)與seata社區(qū)的大佬討論,最終決定在回滾時(shí),如果對(duì)比afterImage
與當(dāng)前數(shù)據(jù)不一致的情況下,不再嘗試回滾重試。這樣的話,即使后續(xù)通過(guò)人工校準(zhǔn)后,也不會(huì)回滾了。但是這樣有另一個(gè)問(wèn)題,就是人工校準(zhǔn)后,這個(gè)分布式事務(wù)就一直遺留在數(shù)據(jù)庫(kù)中無(wú)法刪除了。針對(duì)這個(gè)問(wèn)題,seata應(yīng)該要提供一個(gè)restful api
讓開發(fā)人員在數(shù)據(jù)校準(zhǔn)后能夠刪除掉對(duì)應(yīng)的分布式事務(wù)數(shù)據(jù)。
在seata源碼中,如果校驗(yàn)afterImage
與當(dāng)前數(shù)據(jù)不一致后,會(huì)拋出SQLException
,最終會(huì)被上層代碼捕獲包裝成BranchTransactionException
異常,但是里面的code
屬性是BranchRollbackFailed_Retriable
,這也是導(dǎo)致seata一直重試回滾的根本原因:
Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords); if (!afterEqualsCurrentResult.getResult()) { // 先比較afterImage與當(dāng)前數(shù)據(jù),如果不一致,那么再比較當(dāng)前數(shù)據(jù)和beforeImage是否一致 Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords); // 如果當(dāng)前數(shù)據(jù)和beforeImage一致,那么不需要回滾了,因?yàn)橄喈?dāng)于已經(jīng)回滾了 if (beforeEqualsCurrentResult.getResult()) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Stop rollback because there is no data change " + "between the before data snapshot and the current data snapshot."); } // no need continue undo. return false; } else { // 否則,直接拋出SQLException,并告知undo log臟寫了 if (LOGGER.isInfoEnabled()) { if (StringUtils.isNotBlank(afterEqualsCurrentResult.getErrMsg())) { LOGGER.info(afterEqualsCurrentResult.getErrMsg(), afterEqualsCurrentResult.getErrMsgParams()); } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("check dirty data failed, old and new data are not equal, " + "tableName:[" + sqlUndoLog.getTableName() + "]," + "oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," + "newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "]."); } throw new SQLException("Has dirty records when undo."); } }
在上層調(diào)用代碼中,我們可以找到這樣一段:
catch (Throwable e) { if (conn != null) { try { conn.rollback(); } catch (SQLException rollbackEx) { LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx); } } // 包裝異常 throw new BranchTransactionException(BranchRollbackFailed_Retriable, String .format("Branch session rollback failed and try again later xid = %s branchId = %s %s", xid, branchId, e.getMessage()), e); }
根據(jù)源碼分析,我們發(fā)現(xiàn)在數(shù)據(jù)校驗(yàn)后拋出的SQLException
會(huì)被包裝成code屬性為BranchRollbackFailed_Retriable
的BranchTransactionException
異常,這樣會(huì)導(dǎo)致seata不斷重試回滾操作。
如何處理
我們需要將這個(gè)SQLException
調(diào)整為一個(gè)更加具體的異常,比如SQLUndoDirtyException
這種能夠明確地表示undo log
被臟寫的異常,另外我們?cè)谏蠈哟a中同樣需要針對(duì)SQLUndoDirtyException
做特殊處理,比如包裝成new BranchTransactionException(BranchRollbackFailed_Unretriable)
不可重試的狀態(tài)。
先創(chuàng)建自定義的異常:SQLUndoDirtyException
import java.io.Serializable; import java.sql.SQLException; /** * @author zouwei */ class SQLUndoDirtyException extends SQLException implements Serializable { private static final long serialVersionUID = -5168905669539637570L; SQLUndoDirtyException(String reason) { super(reason); } }
調(diào)整SQLException
為SQLUndoDirtyException
:
Result<Boolean> afterEqualsCurrentResult = DataCompareUtils.isRecordsEquals(afterRecords, currentRecords); if (!afterEqualsCurrentResult.getResult()) { // 先比較afterImage與當(dāng)前數(shù)據(jù),如果不一致,那么再比較當(dāng)前數(shù)據(jù)和beforeImage是否一致 Result<Boolean> beforeEqualsCurrentResult = DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords); // 如果當(dāng)前數(shù)據(jù)和beforeImage一致,那么不需要回滾了,因?yàn)橄喈?dāng)于已經(jīng)回滾了 if (beforeEqualsCurrentResult.getResult()) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Stop rollback because there is no data change " + "between the before data snapshot and the current data snapshot."); } // no need continue undo. return false; } else { // 否則,直接拋出SQLException,并告知undo log臟寫了 if (LOGGER.isInfoEnabled()) { if (StringUtils.isNotBlank(afterEqualsCurrentResult.getErrMsg())) { LOGGER.info(afterEqualsCurrentResult.getErrMsg(), afterEqualsCurrentResult.getErrMsgParams()); } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("check dirty data failed, old and new data are not equal, " + "tableName:[" + sqlUndoLog.getTableName() + "]," + "oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," + "newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "]."); } // 替換為具體的SQLUndoDirtyException異常 throw new SQLUndoDirtyException("Has dirty records when undo."); } }
這樣的話,我們?cè)谏蠈哟a中,就可以針對(duì)性地處理了:
catch (Throwable e) { if (conn != null) { try { conn.rollback(); } catch (SQLException rollbackEx) { LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx); } } // 如果捕捉的異常為SQLUndoDirtyException,那么包裝為BranchRollbackFailed_Unretriable if (e instanceof SQLUndoDirtyException) { throw new BranchTransactionException(BranchRollbackFailed_Unretriable, String.format( "Branch session rollback failed because of dirty undo log, please delete the relevant undolog after manually calibrating the data. xid = %s branchId = %s", xid, branchId), e); } throw new BranchTransactionException(BranchRollbackFailed_Retriable, String.format("Branch session rollback failed and try again later xid = %s branchId = %s %s", xid, branchId, e.getMessage()), e); }
我們?cè)谏蠈诱{(diào)用代碼中捕捉指定的SQLUndoDirtyException
,直接包裝為BranchRollbackFailed_Unretriable
狀態(tài)的BranchTransactionException
,這樣我們的分布式事務(wù)就不會(huì)一直重試回滾操作了。
下一步就需要開發(fā)人員人工介入校準(zhǔn)數(shù)據(jù)后刪除對(duì)應(yīng)的undo log
,在一系列操作處理完畢后,另外還需要seata tc端提供對(duì)應(yīng)的restful api
開放對(duì)應(yīng)的手工觸發(fā)回滾的操作,以便保證校準(zhǔn)后的分布式事務(wù)正常結(jié)束。
小結(jié)
我們根據(jù)seata使用人員反饋的問(wèn)題,通過(guò)源碼分析找到了造成問(wèn)題的原因:
- 開發(fā)人員在使用seata的時(shí)候,對(duì)于同一張表的操作沒(méi)有使用
@GlobalTransactional
注解覆蓋到,導(dǎo)致了undo log
被臟寫; - 當(dāng)產(chǎn)生回滾時(shí),在進(jìn)行數(shù)據(jù)校驗(yàn)時(shí),發(fā)現(xiàn)
afterImage
與當(dāng)前數(shù)據(jù)不一致進(jìn)而無(wú)法正?;貪L,拋出SQLException
,最終包裝成BranchRollbackFailed_Retriable
異常,導(dǎo)致seata一直重試回滾; - 在數(shù)據(jù)校準(zhǔn)后,某一刻的數(shù)據(jù)與
afterImage
一致,此時(shí)seata就回滾成功,形成ABA
問(wèn)題;
該pr將在1.6版本后解決seata分布式事務(wù)一直嘗試回滾的問(wèn)題,可以避免ABA
問(wèn)題的產(chǎn)生,后續(xù)還需要提供一些其他功能輔助開發(fā)人員回滾數(shù)據(jù)。
以上就是Seata分布式事務(wù)出現(xiàn)ABA問(wèn)題解決的詳細(xì)內(nèi)容,更多關(guān)于Seata分布式事務(wù)ABA的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
PowerJob的DesignateServer工作流程源碼解讀
這篇文章主要介紹了PowerJob的DesignateServer工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01SpringBoot結(jié)合ElasticSearch實(shí)現(xiàn)模糊查詢的項(xiàng)目實(shí)踐
本文主要介紹了SpringBoot結(jié)合ElasticSearch實(shí)現(xiàn)模糊查詢的項(xiàng)目實(shí)踐,主要實(shí)現(xiàn)模糊查詢、批量CRUD、排序、分頁(yè)和高亮功能,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03Springboot?hibernate-validator?6.x快速校驗(yàn)示例代碼
這篇文章主要介紹了Springboot?hibernate-validator?6.x校驗(yàn),本文以6.2.1.Final版本為例解決了log4j版本的漏洞問(wèn)題,通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-12-12SpringCloud Finchley+Spring Boot 2.0 集成Consul的方法示例(1.2版本)
這篇文章主要介紹了SpringCloud Finchley+Spring Boot 2.0 集成Consul的方法示例(1.2版本),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08Java實(shí)現(xiàn)簡(jiǎn)單的模板渲染
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)單的模板渲染的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12SpringBoot整合iText7導(dǎo)出PDF及性能優(yōu)化方式
在SpringBoot項(xiàng)目中整合iText7庫(kù)以導(dǎo)出PDF文件,不僅能夠滿足報(bào)告生成需求,而且可以處理復(fù)雜的文檔布局與樣式,整合步驟包括添加Maven依賴、編寫PDF生成代碼,性能優(yōu)化方面,建議使用流式處理、緩存樣式與字體、優(yōu)化HTML/CSS結(jié)構(gòu)、采用異步處理2024-09-09PowerJob的HashedWheelTimer工作流程源碼解讀
這篇文章主要為大家介紹了PowerJob的HashedWheelTimer工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01詳解Java的readBytes是怎么實(shí)現(xiàn)的
眾所周知,Java是一門跨平臺(tái)語(yǔ)言,針對(duì)不同的操作系統(tǒng)有不同的實(shí)現(xiàn),下面小編就來(lái)從一個(gè)非常簡(jiǎn)單的api調(diào)用帶大家來(lái)看看Java具體是怎么做的吧2023-07-07