第21章 JDBC 异常全集与连接池诊断

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.

触发场景排查优先级:

  1. 连接泄漏 :代码中获取了 Connection 但没有在 finally/try-with-resources 中正确关闭(比如异常路径遗漏了 close()),导致池内可用连接持续减少。leak-detection-threshold 是排查此类问题最直接的手段------一旦某个连接借出后长时间未归还,日志会打印出当时借出连接的调用堆栈,直接定位到问题代码位置。
  2. 慢查询占用连接过久:大量慢 SQL 长时间占用连接不释放,导致新请求排队等待连接。需要结合数据库慢查询日志定位。
  3. 瞬时流量超过池容量 :业务高峰期并发数超过 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 排名,对排查连接池相关问题提供了比单纯看日志更直观的手段。


相关推荐
Java内核笔记1 小时前
告别第三方库!Spring Boot 4 原生 API 版本控制全解析:4 种策略 + 实战案例
java·后端
神奇小汤圆1 小时前
把Spring Boot 4的Native Image玩明白了,启动3秒变50毫秒的踩坑全记录
后端
东方小月1 小时前
从零开发一个 Coding Agent(五):使用 TypeBox 校验工具参数
前端·人工智能·后端
Scene2161 小时前
Agent Harness、Loop 与 Graph:构建生产级 AI Agent 的三大架构支柱
后端
SomeB1oody2 小时前
【RustyML入门】2.0. 经典机器学习
开发语言·后端·机器学习·rust·教程
就改了2 小时前
SpringBoot 自定义线程池 + 实时监控指标
java·spring boot·后端
暗黑小白2 小时前
脱敏引擎工程化
后端·ai agent
用户8181870627462 小时前
第23章 JPA / Hibernate 异常
后端
用户8181870627462 小时前
第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试
后端