Java 异常大全系列 · Part 5 数据库与 ORM 异常
本 Part 涵盖第 21-24 章。第21章的 SQLException 实测部分使用真实 H2 数据库(内存模式)编译运行验证,
SQLState/ErrorCode/异常消息均为真实输出。
当前 Part 内容:
- 第21章 JDBC 异常全集与连接池诊断
- 第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试
- 第23章 JPA / Hibernate 异常
- 第24章 分布式事务异常初探
第21章 JDBC 异常全集与连接池诊断
21.1 SQLException 的三个关键字段
java
public class SQLException extends Exception {
private String SQLState; // 标准化的错误状态码(遵循 XOPEN/SQL:2003 规范,跨数据库厂商基本一致的前两位分类)
private int vendorCode; // 数据库厂商私有错误码(不同数据库完全不同,需要查各自文档)
private SQLException next; // SQLException 支持链式串联多个异常(一次操作可能对应多个错误)
}
getSQLState() 是排查跨数据库通用问题的关键 ------它的前两位是标准化分类(比如 23 开头都表示"完整性约束违反",42 开头都表示"语法错误或访问规则违反"),只要记住这几个高频前缀,看到任何数据库(MySQL/PostgreSQL/Oracle/H2)抛出的 SQLException,都能快速判断问题大类,而不需要死记每个数据库私有的 vendorCode。
21.2 实测:四类高频 SQLException 及其精确 SQLState
用 H2 数据库(内存模式)实际编译运行验证(这里选用 H2 是因为它遵循标准 SQL 语义且 SQLState 编码规范,MySQL/PostgreSQL 在同类场景下的 SQLState 前缀基本一致):
场景1:唯一约束冲突
sql
INSERT INTO person VALUES (2, 'Alice') -- name 字段有 UNIQUE 约束,'Alice' 已存在
实测输出:
sql
SQLState: 23505
Message: Unique index or primary key violation: "PUBLIC.CONSTRAINT_INDEX_8 ...
23505 中的 23 前缀(Integrity Constraint Violation)在 MySQL/PostgreSQL 中同样适用(MySQL 对应 Duplicate entry 报错,SQLState 同样以 23 开头)。
场景2:字段长度超限
sql
INSERT INTO person VALUES (3, 'ThisNameIsWayTooLong') -- name 字段定义为 VARCHAR(10)
实测输出:
vbnet
SQLState: 22001
Message: Value too long for column "NAME CHARACTER VARYING(10)": "'ThisNameIsWayTooLong' (20)"
22 前缀表示 Data Exception(数据本身的问题,如格式、长度、类型转换失败等)。
场景3:SQL 语法错误
sql
SELCT * FROM person -- 关键字拼写错误
实测输出:
vbnet
SQLState: 42001
Message: Syntax error in SQL statement "[*]SELCT * FROM person"; expected "SAVEPOINT, SCRIPT, SHUTDOWN"
42 前缀表示 Syntax Error or Access Rule Violation(语法或权限问题)。值得一提的是 H2 的错误消息里用 [*] 精确标出了解析器认为出错的具体位置,这比很多数据库只给出笼统的"语法错误附近"更精确。
场景4:查询不存在的表
sql
SELECT * FROM not_exist_table
实测输出:
sql
SQLState: 42S02
Message: Table "NOT_EXIST_TABLE" not found
同属 42 前缀大类(访问对象不存在也算广义的"访问规则违反")。
21.3 BatchUpdateException:批量执行中部分失败的实测行为
批量插入中如果某一条数据违反约束,实测验证具体行为:
java
st.addBatch("INSERT INTO t VALUES (1)");
st.addBatch("INSERT INTO t VALUES (1)"); // 主键冲突
st.addBatch("INSERT INTO t VALUES (2)");
st.executeBatch(); // 抛出 BatchUpdateException
实测输出:
ini
BatchUpdateException: Unique index or primary key violation: ...
update count=1
update count=-3
update count=-3 ← Statement.EXECUTE_FAILED 常量值
update count=1
关键发现 :H2 在批量执行中,第二条失败后仍然继续执行了第三条 (update count=1 对应第三条成功插入),getUpdateCounts() 返回的数组里,失败的那一条对应值是 -3(即 Statement.EXECUTE_FAILED 常量),成功的条目对应真实受影响行数。
重要提醒 :这个"失败后是否继续执行剩余批次"的行为因数据库/驱动实现而异 ,不能想当然认为所有数据库都会"继续执行剩余语句"------某些数据库驱动在遇到第一个错误时会直接中止整个批次,getUpdateCounts() 可能只返回已成功执行的那部分计数。排查批量操作异常时,务必先查阅具体数据库驱动的官方文档确认这一行为,不能凭经验假设。
java
try {
st.executeBatch();
} catch (BatchUpdateException e) {
int[] counts = e.getUpdateCounts();
for (int i = 0; i < counts.length; i++) {
if (counts[i] == Statement.EXECUTE_FAILED) {
log.error("第 {} 条批量语句执行失败", i);
}
}
}
21.4 连接池诊断:HikariCP 参数与常见故障
HikariCP 是 Spring Boot 2.x+ 默认的连接池实现,核心参数及其对应的故障场景:
yaml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 连接池最大连接数
minimum-idle: 5 # 最小空闲连接数
connection-timeout: 30000 # 从池中获取连接的最长等待时间(毫秒),超时抛 SQLTransientConnectionException
idle-timeout: 600000 # 空闲连接超过此时间会被回收(前提是当前连接数 > minimum-idle)
max-lifetime: 1800000 # 连接的最大生命周期,超过后即使正在使用也会被标记淘汰(避免用到数据库主动断开的僵尸连接)
leak-detection-threshold: 60000 # 连接借出超过此时间未归还,打印警告日志(连接泄漏检测)
connection-timeout 触发的典型异常:
csharp
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
触发场景排查优先级:
- 连接泄漏 :代码中获取了
Connection但没有在finally/try-with-resources中正确关闭(比如异常路径遗漏了close()),导致池内可用连接持续减少。leak-detection-threshold是排查此类问题最直接的手段------一旦某个连接借出后长时间未归还,日志会打印出当时借出连接的调用堆栈,直接定位到问题代码位置。 - 慢查询占用连接过久:大量慢 SQL 长时间占用连接不释放,导致新请求排队等待连接。需要结合数据库慢查询日志定位。
- 瞬时流量超过池容量 :业务高峰期并发数超过
maximum-pool-size,属于容量规划问题,需要评估是否需要调大连接池,或者从业务侧做限流。
max-lifetime 必须小于数据库侧的连接超时设置 (如 MySQL 的 wait_timeout),否则会出现一种隐蔽故障:连接池认为连接还"年轻"(未到 max-lifetime),但数据库那一端已经主动断开了这条连接(超过了 wait_timeout),下次使用这条"僵尸连接"执行 SQL 时会抛出:
bash
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
排查这类问题的关键日志线索是:Communications link failure 发生在长时间没有数据库操作之后(比如凌晨低峰期后的第一笔请求),这基本可以确定是连接生命周期配置和数据库超时配置不匹配导致的。
21.5 Druid 连接池的差异化诊断能力
阿里的 Druid 连接池相比 HikariCP 提供了更丰富的内置监控能力(代价是性能略低于 HikariCP,两者是"功能丰富度"和"极致性能"之间的典型权衡):
yaml
spring:
datasource:
druid:
max-active: 20
test-while-idle: true # 空闲时定期检测连接有效性,主动剔除已失效的连接
validation-query: SELECT 1 # 检测连接是否存活的探测 SQL
time-between-eviction-runs-millis: 60000 # 检测间隔
filters: stat,wall # stat=SQL监控统计, wall=SQL防火墙(可拦截危险SQL)
test-while-idle + validation-query 这套机制专门用来解决 HikariCP max-lifetime 方案没有覆盖的场景:主动探测 连接是否仍然有效(而不是被动等待"生命周期到了"或者"用的时候才发现已经断开"),能更早地把失效连接从池中剔除,减少业务请求撞上"僵尸连接"的概率。Druid 内置的 StatViewServlet(监控页面)还能直接可视化查看每个 SQL 的执行次数、耗时分布、慢 SQL 排名,对排查连接池相关问题提供了比单纯看日志更直观的手段。