Spring異常處理?bug的問題記錄(同一份代碼,結(jié)果卻不一樣)
1. 背景
在上周遇到一個spring bug的問題,將其記錄一下。簡化的代碼如下:
public void insert() {
try {
Person person = new Person();
person.setId(3581L);// 這個是主鍵,擁有唯一索引**
personDao.insert(person);
} catch (DuplicateKeyException e) {
log.error("DuplicateKeyException e = {}", e.getMessage(), e);
// DuplicateKeyException 其他邏輯處理
} catch (DataIntegrityViolationException e) {
log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
// DataIntegrityViolationException 其他邏輯處理
} catch (Exception e) {
log.error("Exception e = {}", e.getMessage(), e);
}
}然而同一份代碼,部署在不同機器(數(shù)據(jù)庫只有一個, 不存在分庫分表情況),遇到的情況不一樣。
A機器:如果主鍵沖突,則拋出DuplicateKeyException異常,進入第7行的邏輯
B機器:如果主鍵沖突,則拋出DataIntegrityViolationException異常,進入第11行的邏輯
甚至我將B機器重啟,如果主鍵沖突,則拋出DuplicateKeyException異常,進入第7行的邏輯
非常的奇怪,我們一一細說
2. 數(shù)據(jù)庫異常分析
2.1 spring對java標(biāo)準(zhǔn)異常的包裝
| 異常類型/屬性 | 所屬框架或技術(shù)棧 | 觸發(fā)場景 |
|---|---|---|
| SQLIntegrityConstraintViolationException | 屬于 JDBC 標(biāo)準(zhǔn)異常體系,是 java.sql.SQLException 的子類。 | 當(dāng)數(shù)據(jù)庫操作違反了完整性約束(如主鍵沖突、外鍵約束、唯一性約束等)時,JDBC 驅(qū)動會拋出此異常。 |
| DuplicateKeyException | 是 Spring 框架中定義的異常,屬于 Spring Data 或 Spring JDBC 的封裝異常。 | 通常在插入或更新數(shù)據(jù)時,違反了數(shù)據(jù)庫表的主鍵或唯一索引約束(即嘗試插入重復(fù)的主鍵或唯一鍵值)。 |
| DataIntegrityViolationException | 是 Spring 框架中的異常,屬于 Spring 數(shù)據(jù)訪問層的通用異常體系 | 是一個更通用的異常,表示任何違反數(shù)據(jù)完整性的操作,包括但不限于主鍵沖突、外鍵約束、非空約束等。 |
從表格中我們可以明顯看出,SQLIntegrityConstraintViolationException是屬于Java體系的標(biāo)準(zhǔn)異常,當(dāng)主鍵沖突,外鍵約束,非空等情況正常都會拋出這個異常
然后spring框架對這個異常進行了一個封裝,比如違反唯一索引會拋出DuplicateKeyException異常,其他的情況會拋出DataIntegrityViolationException異常。
2.2 spring代碼包裝
在spring中會有一個SQLErrorCodesFactory類,會加載下面路徑下的資源。也就是說,每個數(shù)據(jù)庫廠商對于不同異常返回的錯誤碼不同,spring進行了一個包裝
public static final String SQL_ERROR_CODE_DEFAULT_PATH
= "org/springframework/jdbc/support/sql-error-codes.xml";

2.3 問題產(chǎn)生的原因
在spring異常處理中,有一個非常核心的類 SQLErrorCodeSQLExceptionTranslator,但遇到主鍵沖突,非空約束等異常的時候,spring會使用這個類進行轉(zhuǎn)化。
if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx);
}
else if (Arrays.binarySearch( this .sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if // xxx 省略我們可以從上面代碼中可以看到,他其中是從sqlErrorCodes中,進行二分查找,是否存在相應(yīng)的code碼,然后返回給上游不同的錯誤,那么sqlErrorCodes是從哪里獲取的呢。
try {
String name = JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName");
if (StringUtils.hasLength(name)) {
return registerDatabase(dataSource, name);
}
}
catch (MetaDataAccessException ex) {
logger.warn("Error while extracting database name - falling back to empty error codes", ex);
}
// Fallback is to return an empty SQLErrorCodes instance.
return new SQLErrorCodes();從上面代碼我們可以看出,會通過JdbcUtils.extractDatabaseMetaData方法來獲取sqlErrorCodes,是哪個廠商,并且獲取到Connection進行連接,然后返回相應(yīng)的sqlErrorCodes碼
但是在第7行,如果此時Connection數(shù)據(jù)庫鏈接有異常,則會報錯,然后返回11行一個空的sqlErrorCodes,那么問題就出在這里了!?。?/p>
也就是說,如果在第一次獲取sqlErrorCodes,如果出了問題,那么這個字段就會為空,上面代碼的轉(zhuǎn)化異常邏輯就會判斷錯誤。就會走到else兜底退避的策略。
具體退避的策略在SQLExceptionSubclassTranslator類中,所以當(dāng)走到了退避策略,所有SQLIntegrityConstraintViolationException異常都會返回DataIntegrityViolationException異常
if (ex instanceof SQLNonTransientConnectionException) {
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLDataException) {
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLIntegrityConstraintViolationException) {
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if // 省略3. 問題復(fù)現(xiàn)
3.1 錯誤復(fù)現(xiàn)
我們從2.3分析中,可以清楚的知道,根因是SQLErrorCodeSQLExceptionTranslator類中sqlErrorCodes字段為空導(dǎo)致主鍵沖突退避返回了DataIntegrityViolationException異常。
那么我們就可以模擬鏈接異常,比如連接被關(guān)閉了,導(dǎo)致首次初始化的時候?qū)е聅qlErrorCodes失敗,代碼如下 (注意這塊代碼必須在項目啟動 首先第一次執(zhí)行)
@Transactional
public void testConnect() {
try {
Connection connection = DataSourceUtils.getConnection(dataSource);
connection.close(); // 強制關(guān)閉連接,破壞事務(wù)一致性
personDao.selectById(1L);
} catch (DuplicateKeyException e) {
log.error("DuplicateKeyException e = {}", e.getMessage(), e);
} catch (DataIntegrityViolationException e) {
log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
} catch (Exception e) {
log.error("Exception e = {}", e.getMessage(), e);
}
}在上面代碼中,我們獲取了鏈接,并且強制關(guān)閉了,那么就會導(dǎo)致初始化的時候走2.3那塊代碼就會報錯,此時sqlErrorCodes就會為空。
如果后面sql遇到了唯一索引,返回如下:

3.2 正確復(fù)現(xiàn)
將上面代碼connection.close()去掉,那么第一次緩存就正常了。再次執(zhí)行,如果遇到了唯一索引,返回如下:

4. 解決辦法
在github上面已經(jīng)有人提出此問題,并且標(biāo)記為了bug,鏈接如下:https://github.com/spring-projects/spring-framework/issues/25681
并且修復(fù)pull request如下 (此代碼已合并到v5.2.9.RELEASE分支)

4.1 辦法1
升級spring版本到5.2.9.release+,可以徹底解決此問題
4.2 辦法2
第一步在項目啟動的時候,獲取SQLErrorCodes,如果為空,則打印error日志并且告警。讓開發(fā)同學(xué)知道有這么一個問題 (可重啟也可不重啟)
public class DatabaseMetadataPreloader {
@PostConstruct
public void init() {
try {
SQLErrorCodes errorCodes = errorCodesFactory.getErrorCodes(dataSource);
log.info("Database metadata preloaded successfully errorCodes = {}", GsonUtils.toJson(errorCodes));
String[] duplicateKeyCodes = errorCodes.getDuplicateKeyCodes();
if (ArrayUtils.isEmpty(duplicateKeyCodes)) {
log.error("No duplicate key codes found in database metadata 請重啟服務(wù)");
}
} catch (Exception e) {
log.error("Failed to preload database metadata", e);
}
}
}第二步重新查詢一遍數(shù)據(jù)庫
如果有數(shù)據(jù)則表明是索引沖突,如果沒有數(shù)據(jù),則可能是其他異常引起的,走原有的老邏輯
catch (DuplicateKeyException e) {
log.error("DuplicateKeyException e = {}", e.getMessage(), e);
}
catch (DataIntegrityViolationException e) {
log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
// 重新查一遍數(shù)據(jù)庫,如果有數(shù)據(jù),說明是唯一索引沖突
Person p = select(xxxx)
if (p != null) {
// 唯一索引沖突
} else {
// 其他異常引起的
}
}相關(guān)文章
Python import與from import使用及區(qū)別介紹
Python程序可以調(diào)用一組基本的函數(shù)(即內(nèi)建函數(shù)),比如print()、input()和len()等函數(shù)。接下來通過本文給大家介紹Python import與from import使用及區(qū)別介紹,感興趣的朋友一起看看吧2018-09-09
Python使用PyCrypto實現(xiàn)AES加密功能示例
這篇文章主要介紹了Python使用PyCrypto實現(xiàn)AES加密功能,結(jié)合具體實例形式分析了PyCrypto實現(xiàn)AES加密的操作步驟與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-05-05
Django-xadmin+rule對象級權(quán)限的實現(xiàn)方式
今天小編就為大家分享一篇Django-xadmin+rule對象級權(quán)限的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03

