网站首页 > 技术文章 正文
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);
}
}
然而同一份代码,部署在不同机器(数据库只有一个, 不存在分库分表情况),遇到的情况不一样。
A机器:如果主键冲突,则抛出DuplicateKeyException异常,进入第7行的逻辑
B机器:如果主键冲突,则抛出
DataIntegrityViolationException异常,进入第11行的逻辑
甚至我将B机器重启,如果主键冲突,则抛出DuplicateKeyException异常,进入第7行的逻辑
非常的奇怪,我们一一细说
2. 数据库异常分析
2.1 spring对java标准异常的包装
异常类型/属性 | 所属框架或技术栈 | 触发场景 |
SQLIntegrityConstraintViolationException | 属于 JDBC 标准异常体系,是 java.sql.SQLException 的子类。 | 当数据库操作违反了完整性约束(如主键冲突、外键约束、唯一性约束等)时,JDBC 驱动会抛出此异常。 |
DuplicateKeyException | 是 Spring 框架中定义的异常,属于 Spring Data 或 Spring JDBC 的封装异常。 | 通常在插入或更新数据时,违反了数据库表的主键或唯一索引约束(即尝试插入重复的主键或唯一键值)。 |
DataIntegrityViolationException | 是 Spring 框架中的异常,属于 Spring 数据访问层的通用异常体系 | 是一个更通用的异常,表示任何违反数据完整性的操作,包括但不限于主键冲突、外键约束、非空约束等。 |
从表格中我们可以明显看出,
SQLIntegrityConstraintViolationException是属于Java体系的标准异常,当主键冲突,外键约束,非空等情况正常都会抛出这个异常
然后spring框架对这个异常进行了一个封装,比如违反唯一索引会抛出DuplicateKeyException异常,其他的情况会抛出
DataIntegrityViolationException异常。
2.2 spring代码包装
在spring中会有一个SQLErrorCodesFactory类,会加载下面路径下的资源。也就是说,每个数据库厂商对于不同异常返回的错误码不同,spring进行了一个包装
public static final String SQL_ERROR_CODE_DEFAULT_PATH
= "org/springframework/jdbc/support/sql-error-codes.xml";
2.3 问题产生的原因
在spring异常处理中,有一个非常核心的类
SQLErrorCodeSQLExceptionTranslator,但遇到主键冲突,非空约束等异常的时候,spring会使用这个类进行转化。
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中,进行二分查找,是否存在相应的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进行连接,然后返回相应的sqlErrorCodes码
但是在第7行,如果此时Connection数据库链接有异常,则会报错,然后返回11行一个空的sqlErrorCodes,那么问题就出在这里了!!!
也就是说,如果在第一次获取sqlErrorCodes,如果出了问题,那么这个字段就会为空,上面代码的转化异常逻辑就会判断错误。就会走到else兜底退避的策略。
具体退避的策略在
SQLExceptionSubclassTranslator类中,所以当走到了退避策略,所有
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. 问题复现
3.1 错误复现
我们从2.3分析中,可以清楚的知道,根因是
SQLErrorCodeSQLExceptionTranslator类中sqlErrorCodes字段为空导致主键冲突退避返回了
DataIntegrityViolationException异常。
那么我们就可以模拟链接异常,比如连接被关闭了,导致首次初始化的时候导致sqlErrorCodes失败,代码如下 (
注意这块代码必须在项目启动 首先第一次执行)
@Transactional
public void testConnect() {
try {
Connection connection = DataSourceUtils.getConnection(dataSource);
connection.close(); // 强制关闭连接,破坏事务一致性
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);
}
}
在上面代码中,我们获取了链接,并且强制关闭了,那么就会导致初始化的时候走2.3那块代码就会报错,此时sqlErrorCodes就会为空。
如果后面sql遇到了唯一索引,返回如下:
3.2 正确复现
将上面代码connection.close()去掉,那么第一次缓存就正常了。再次执行,如果遇到了唯一索引,返回如下:
4. 解决办法
在github上面已经有人提出此问题,并且标记为了bug,链接如下:
https://github.com/spring-projects/spring-framework/issues/25681
并且修复pull request如下 (此代码已合并到v5.2.9.RELEASE分支)
https://github.com/spring-projects/spring-framework/commit/670b9fd60b3b5ada69b060424d697270eeee01c2#diff-e2f38c7b7d44c3679cd585e5c81e76b3ca32313bf870caa6435cd36bfe4d9600
4.1 办法1
升级spring版本到5.2.9.release+,可以彻底解决此问题
4.2 办法2
第一步在项目启动的时候,获取SQLErrorCodes,如果为空,则打印error日志并且告警。让开发同学知道有这么一个问题 (可重启也可不重启)
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 请重启服务");
}
} catch (Exception e) {
log.error("Failed to preload database metadata", e);
}
}
}
第二步重新查询一遍数据库
如果有数据则表明是索引冲突,如果没有数据,则可能是其他异常引起的,走原有的老逻辑
catch (DuplicateKeyException e) {
log.error("DuplicateKeyException e = {}", e.getMessage(), e);
}
catch (DataIntegrityViolationException e) {
log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
// 重新查一遍数据库,如果有数据,说明是唯一索引冲突
Person p = select(xxxx)
if (p != null) {
// 唯一索引冲突
} else {
// 其他异常引起的
}
}
猜你喜欢
- 2025-07-28 Netty 解码器深入分析:调用时机与继承关系
- 2025-07-28 用户中心——比如:腾讯的QQ账号可以登录到很多应用当中 02
- 2025-07-28 多使用枚举,少使用常量类,让代码更加井然有序
- 2025-07-28 Java 设计模式之享元模式介绍(享元模式实例)
- 2025-07-28 跟我学Java基础(三):常量(java中常量怎么定义)
- 2025-07-28 简述final关键字的作用(简述使用final关键字的注意点)
- 2025-05-08 java2(JaVa2实用教程第6版Pdf)
- 2025-05-08 Java 类初始化顺序解析与高并发优化
- 2025-05-08 Java基本程序设计结构(上)(java程序设计的三种结构是什么)
- 2025-05-08 Java 阴历阳历转换(java转化日期格式)
- 08-01Linux Systemd入门
- 08-01使用 Checkmk 监控 Oracle 服务器
- 08-01核心库CPU飙到99%了!我发现很多DBA都不会看日志……
- 08-01China's CETC Kingbase Unveils AI-Powered Database Appliances Amid Rising Demand for Intelligent Data Infrastructure
- 08-01Docker安装部署Oracle/Sql Server
- 08-01Oracle数据库安装 | 步骤详细
- 08-01基于Springboot + vue实现的社团管理系统
- 08-01前端开发如何用Mock.js进行数据接口模拟
- 1520℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 623℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 526℃MySQL service启动脚本浅析(r12笔记第59天)
- 492℃启用MySQL查询缓存(mysql8.0查询缓存)
- 491℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 479℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 460℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 458℃MySQL server PID file could not be found!失败
- 最近发表
-
- Linux Systemd入门
- 使用 Checkmk 监控 Oracle 服务器
- 核心库CPU飙到99%了!我发现很多DBA都不会看日志……
- China's CETC Kingbase Unveils AI-Powered Database Appliances Amid Rising Demand for Intelligent Data Infrastructure
- Docker安装部署Oracle/Sql Server
- Oracle数据库安装 | 步骤详细
- 基于Springboot + vue实现的社团管理系统
- 前端开发如何用Mock.js进行数据接口模拟
- 使用vite为vue项目配置@别名
- 基于Springboot + vue3实现的教务管理系统
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- windowsscripthost (69)
- apt-getinstall-y (100)
- js~~ (67)
- node_modules怎么生成 (87)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)