JDBC、DataSource 与连接池:连接获取、复用和故障判断
一条参数化查询会依次取得连接、创建语句、绑定参数、读取结果,最后释放资源。JDBC 把这些操作分给了四类对象:
DataSource 取得连接的入口
└─ Connection 一次连接使用权;承载事务和会话设置
└─ PreparedStatement SQL 结构、参数、执行设置
└─ ResultSet 查询结果与当前读取位置
创建:Connection → PreparedStatement → ResultSet
关闭:ResultSet → PreparedStatement → Connection池化后,最外层的 Connection.close() 多了一项重要含义:结束本次借用,让底层连接有机会交给另一个调用者。理解这次交接,才能解释“SQL 很快,拿连接却很慢”“上一个请求改的设置出现在下一个请求中”等问题。
JDBC 对象怎样对应数据库会话
接口、驱动和 DataSource
JDBC 是 Java 访问关系数据库的标准接口。应用通常使用 java.sql.Connection、PreparedStatement 等类型;PostgreSQL 驱动 pgJDBC 把方法调用转换为数据库协议消息。驱动 JAR 必须在运行时类路径中。现代 JDBC 驱动通过服务提供者机制注册,通常无需手写 Class.forName("org.postgresql.Driver");缺少驱动时,补一行类加载代码也无法补齐依赖。pgJDBC 连接说明
DataSource 仍属于 javax.sql,是 Java SE 的组成部分,不能随 Jakarta Servlet 的包名迁移改成 jakarta.sql.DataSource。这个接口只规定获取连接等能力,具体实现可以每次新建连接,也可以委托连接池或参与分布式事务。DataSource API
两种实现很容易混淆:
| 实现 | 调用 getConnection() 时的主要工作 | 调用连接的 close() |
|---|---|---|
PGSimpleDataSource | 通过 pgJDBC 建立一个新连接 | 关闭驱动物理连接 |
HikariDataSource | 从池中借用可用连接,必要时建连或等待 | 结束逻辑借用;健康连接经清理后归还 |
连接池通常随应用启动创建,在应用停止时关闭。每次查询都新建一个池,会反复认证、建连和销毁后台线程,失去复用的意义。共享的是池;业务方法借到的连接、语句和结果集应限制在明确的调用范围内,不作为共享字段交给多个请求并发操作。
JDBC URL 中各部分的职责
实验使用:
jdbc:postgresql://db:5432/pool_lab
├─ jdbc Java JDBC URL 前缀
├─ postgresql 驱动子协议
├─ db Compose 网络中的服务名
├─ 5432 PostgreSQL 服务端口
└─ pool_lab 数据库名称这里的 db 只在实验容器加入的 Docker 网络中解析。直接在宿主机执行同一个 URL,通常找不到该主机;容器中的 localhost 则指向容器自身。
数据库和 schema 是不同层级。连接先选择 pool_lab 数据库;SQL 中的 public.pool_marker 才是 schema 与表名。省略 schema 时,PostgreSQL 按 search_path 查找对象。业务代码更换数据库账户、默认 schema 或搜索路径后,即使 SQL 文本相同,也可能访问不同对象。PostgreSQL schema 与搜索路径
用户名和密码通过独立属性传给驱动,避免混入 URL 后被异常、指标或日志直接打印。跨主机访问还应配置服务器身份校验:pgJDBC 的 sslmode=verify-full 同时校验证书链和主机名,需要正确的根证书及服务端配置。实验仅使用无宿主端口映射的本机 Docker 网络,不配置 TLS;这个设置不能直接搬到生产网络。pgJDBC SSL 说明
从参数到结果
PreparedStatement 保留 SQL 结构,setInt(1, id) 为第一个问号绑定值,参数序号从 1 开始。表名、列名和 ASC/DESC 等语法位置不能用问号替换;需要动态排序时,把外部选项映射到代码中预先允许的 SQL 片段。PreparedStatement API
参数化首先解决值与 SQL 语法的分离。驱动是否已在服务器创建命名预备语句,还受执行次数、prepareThreshold 等设置影响;不要用“调用了 prepareStatement”推断服务器端缓存一定命中。pgJDBC 的这些参数列在连接属性说明中。
读取结果时,游标最初位于第一行之前,要先调用 next()。常见类型可以按下面的方式处理:
| SQL 值 | 常用 Java 读取方式 | 需要保留的含义 |
|---|---|---|
| 非空整数 | getInt、getLong | 根据取值范围选择宽度 |
| 可空整数 | getObject("count", Integer.class) | SQL NULL 保留为 Java null |
| 精确金额 | getBigDecimal | 避免用二进制浮点替代十进制金额 |
| 日期 | getObject("day", LocalDate.class) | 日期本身没有时区 |
PostgreSQL timestamp | LocalDateTime | 不携带时区 |
PostgreSQL timestamptz | OffsetDateTime | 表达时间点;不保留最初输入的地区时区名称 |
使用 getInt 读取 SQL NULL 会得到 0,要紧接着用 wasNull() 区分“真实的 0”和“空值”。驱动支持的类型转换应结合 ResultSet API 与 pgJDBC 查询说明核对。
连接 PostgreSQL 并读出一行
环境、文件和运行身份
下载完整实验工程。单独查看源码时,可从 pom.xml、Connections.java、FirstQuery.java 和 PoolLab.java 进入。
运行环境为 Linux Bash,已安装 Docker Engine、Compose 插件、unzip 和 openssl。使用有 Docker 操作权限的普通宿主用户;构建及 Java 容器沿用该用户的 UID/GID。Docker 权限本身具有很高的主机控制能力,不能把“容器内非 root”理解为给任意用户开放 Docker 的安全依据。
版本固定如下:
| 组件 | 实验版本与用途 |
|---|---|
| Java | JDK 25 运行,源码编译目标为 17;也可在 JDK 17 运行 |
| Maven | 3.9.12-eclipse-temurin-25 镜像 |
| HikariCP | 7.0.2,连接池 |
| pgJDBC | 42.7.13,PostgreSQL 驱动 |
| PostgreSQL | 18.6,独立临时数据库 |
| SLF4J Simple | 2.0.17,输出连接失效等日志 |
HikariCP 与驱动版本和 Spring Boot 4.1.1 的依赖管理保持一致;实验是普通 Java 程序,不要求启动 Spring 容器。迁入 Boot 项目时优先由同一份 BOM 管理版本,避免手工覆盖某一项后引入不兼容组合。Spring Boot 依赖版本表
在下载目录执行:
test "$(id -u)" -ne 0 || exit 1
docker version
docker compose version
unzip jdbc-datasource-pool-lab.zip
cd jdbc-datasource-pool
export LAB_DIR="$PWD"
export LAB_CACHE="$LAB_DIR/.m2-cache"
mkdir -p "$LAB_CACHE"
export LAB_DB_PASSWORD="$(openssl rand -hex 24)"
export LAB_APP_PASSWORD="$(openssl rand -hex 24)"
export LAB_DB_URL='jdbc:postgresql://db:5432/pool_lab'前两次版本查询都应成功;docker version 若只有 Client 而 Server 报错,先处理 Docker 服务或访问权限。两个密码仅供这次隔离实验使用,不输出到终端,不复用生产凭据。后续命令在同一个 Bash 会话执行。
工程结构如下:
jdbc-datasource-pool/
├─ pom.xml
├─ compose.yaml
├─ init.sh
├─ run.sh
├─ README.md
└─ src/main/java/example/
├─ Connections.java 连接配置和查询辅助方法
├─ FirstQuery.java 第一条参数化查询
└─ PoolLab.java 复位、污染、耗尽、失效和取消实验启动数据库并分开管理身份
compose.yaml 的数据库进程使用镜像中的 999:999,数据和运行目录放在可写 tmpfs,根文件系统只读。没有 ports,宿主机和外网不能通过端口映射访问它。PostgreSQL 18 官方镜像的数据目录布局及初始化脚本规则见镜像文档。
services:
db:
image: postgres:18.6
user: "999:999"
read_only: true
environment:
POSTGRES_USER: lab_owner
POSTGRES_DB: pool_lab
POSTGRES_PASSWORD: ${LAB_DB_PASSWORD:?Set LAB_DB_PASSWORD}
LAB_APP_PASSWORD: ${LAB_APP_PASSWORD:?Set LAB_APP_PASSWORD}
tmpfs:
- /var/lib/postgresql:rw,uid=999,gid=999,mode=700
- /var/run/postgresql:rw,uid=999,gid=999,mode=775
- /tmp:rw,mode=1777
volumes:
- ./init.sh:/docker-entrypoint-initdb.d/10-app.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U lab_owner -d pool_lab"]
interval: 1s
timeout: 3s
retries: 30
stop_grace_period: 10slab_owner 是初始化与检查使用的数据库管理账户。Java 使用另建的普通角色 lab_app,只取得实验库连接、schema 使用和实验表的增删改查权限。init.sh 中的核心 SQL 是:
CREATE ROLE lab_app LOGIN PASSWORD :'app_password';
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
CREATE TABLE pool_marker (id integer PRIMARY KEY, label text NOT NULL);
INSERT INTO pool_marker VALUES (1, 'ready');
GRANT CONNECT ON DATABASE pool_lab TO lab_app;
GRANT USAGE ON SCHEMA public TO lab_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON pool_marker TO lab_app;这里的 :'app_password' 是 psql 的变量引用,由初始化脚本通过 --set 传入并按 SQL 字符串引用。运行实验无需手工替换这段 SQL。
docker compose -p da10-pool up -d --wait
docker compose -p da10-pool exec -T db id
docker compose -p da10-pool exec -T db \
psql -X -U lab_owner -d pool_lab -v ON_ERROR_STOP=1 \
-c "SELECT id, label FROM pool_marker"应看到数据库容器进入 healthy,进程身份包含 uid=999(postgres),表中有 1 | ready。如果容器启动失败,先看:
docker compose -p da10-pool logs --tail=80 db重点检查初始化脚本能否读取、tmpfs 权限、密码变量是否为空。该数据库放在内存文件系统中,停止容器会丢失实验数据;修改初始化脚本后,应按后面的清理步骤删除本实验容器,再重新创建。
建池与执行查询
pom.xml 明确声明实际使用的三个依赖:
<dependencies>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>7.0.2</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
</dependency>
</dependencies>完整 POM 还固定编译插件和依赖复制插件。构建目录放在容器的 /tmp,不会把 target 写回源码目录。
连接配置集中在 Connections.pool():
HikariConfig config = new HikariConfig();
config.setJdbcUrl(required("LAB_DB_URL"));
config.setUsername("lab_app");
config.setPassword(required("LAB_APP_PASSWORD"));
config.setMaximumPoolSize(1);
config.setMinimumIdle(1);
config.setConnectionTimeout(700);
config.setValidationTimeout(300);
config.setPoolName("pool-lab");
config.addDataSourceProperty("connectTimeout", "3");
config.addDataSourceProperty("socketTimeout", "5");
config.addDataSourceProperty("ApplicationName", "pool-lab");
return new HikariDataSource(config);池大小设为 1 是为了让同一物理会话的复用和等待现象稳定可见。这里的 700、300 是毫秒,驱动的 3、5 是秒;参数单位和所在层次不同,下一节会分别解释。
FirstQuery.query() 接收已经创建的池,完成查询及资源关闭:
String sql = """
select current_database() as database_name, current_user as user_name,
label from pool_marker where id = ?
""";
try (Connection connection = source.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, 1);
statement.setQueryTimeout(2);
try (ResultSet rows = statement.executeQuery()) {
Connections.require(rows.next(), "Marker row is missing");
String database = rows.getString("database_name");
String user = rows.getString("user_name");
String label = rows.getString("label");
Connections.require("pool_lab".equals(database), "Unexpected database");
Connections.require("lab_app".equals(user), "Unexpected role");
Connections.require("ready".equals(label), "Unexpected marker");
Connections.require(!rows.next(), "Expected exactly one row");
System.out.println("database=" + database + " user=" + user
+ " label=" + label + " autoCommit=" + connection.getAutoCommit());
}
}结果集在内层关闭;语句和连接按 try-with-resources 声明的相反顺序关闭。代码还检查数据库名称、运行角色和行数,避免“连上了另一套库但碰巧返回一行”被误判为成功。
正常返回和抛出异常都会触发资源关闭。如果业务处理与关闭同时失败,Java 会保留主异常,并把关闭异常放入 suppressed exceptions;日志应记录异常对象及其关联异常,不能只打印 getMessage()。事务已开启时,业务代码仍应显式提交或回滚,再结束连接使用。Connection API
首次运行与预期输出
定义一个只负责启动实验容器的 Bash 函数,后续各实验复用它:
lab_java() {
docker run --rm \
--user "$(id -u):$(id -g)" --read-only \
--network da10-pool_default \
--tmpfs /tmp:rw,exec,mode=1777 \
--mount "type=bind,src=$LAB_DIR,dst=/src,readonly" \
--mount "type=bind,src=$LAB_CACHE,dst=/cache" \
-e LAB_DB_URL -e LAB_APP_PASSWORD -e MAVEN_CONFIG=/tmp/maven \
maven:3.9.12-eclipse-temurin-25 bash /src/run.sh "$@"
}
lab_java firstrun.sh 编译三份 Java 源码,复制运行依赖到独立临时目录,然后启动指定实验。缓存目录属于当前宿主用户;/tmp 开放执行权限是为了允许 Maven 使用的本地库装载,不需要把根文件系统改为可写。
Maven 构建成功后,业务输出为:
database=pool_lab user=lab_app label=ready autoCommit=true
activeAfterQuery=0第一行来自真实查询和连接属性;第二行在查询方法返回后读取 Hikari 的活动连接数并断言为 0。若看到 Missing LAB_...,先恢复当前 Bash 会话中的变量;依赖下载失败时检查构建容器的网络和 Maven 仓库,不要修改数据库参数。
连接归还后,哪些状态会留下来
逻辑借用与物理会话
Hikari 为每次借用返回连接代理。代理关联池中的条目,条目持有 pgJDBC 物理连接;pg_backend_pid() 可以识别这条 PostgreSQL 会话。
第一次借用:代理 A ─┐
├─ 同一个池条目 ─ pgJDBC 连接 ─ PostgreSQL backend
第二次借用:代理 B ─┘代理 A 关闭后不能继续使用。代理 B 可以复用同一 backend,所以数据库连接上的残留事务和设置必须在两次借用之间处理。
HikariCP 7.0.2 的 ProxyConnection.close() 会先处理未关闭语句;连接仍可用时,根据事务脏标记与 auto-commit 状态回滚遗留事务,复位被追踪的 JDBC 属性,清理 warning,再使逻辑代理失效。健康条目重新进入可借用集合;已经标为失效的条目走淘汰流程。HikariCP 7.0.2 ProxyConnection 源码
图中概括的是交接步骤,不能把它当作所有连接池的统一算法。JDBC 只定义接口;自动回滚、复位哪些属性以及如何识别失效,都需要核对具体实现。
JDBC setter 的复位实验
运行:
lab_java reset实验依次修改 readOnly、事务隔离级别和 autoCommit,执行查询后关闭连接,再次借出时检查:
sameBackend=true autoCommitReset=true readOnlyReset=true isolationReset=true
leftoverTransactionRolledBack=truesameBackend=true 排除了“新建了另一条连接,所以设置碰巧恢复”的解释。后一个断言来自另一轮借用:前一轮关闭前插入了 id=9001 但没有提交,下一轮查询该行数量为 0。
这些自动处理可以兜住部分遗漏,但业务事务仍应明确结束。连接池无法替业务判断某个操作应提交还是回滚;换成非池化 DataSource 后,关闭活动事务的行为也不能靠这次实验外推。
setReadOnly(true) 在 JDBC 中属于驱动提示,数据库如何实施取决于驱动与事务状态。pgJDBC 还提供 readOnlyMode 控制相关行为。真正需要禁止写入的账号,应由数据库权限限制;不能把一个 Java 属性当成权限系统。Connection 只读契约
直接执行 SET 为什么会串到下一次借用
再运行:
lab_java pollution关键操作是:
SET application_name = 'changed-by-sql';这个设置由 SQL 修改服务器会话。Hikari 的 JDBC 属性追踪不会解析每条 SQL,并为其中的任意会话设置生成复位逻辑。实验中第二轮借用仍读到 changed-by-sql,随后显式恢复原值:
sqlSessionSettingSurvived=true explicitlyRestored=true
transactionLocalSettingRestored=true最后一行来自对照:开启事务后执行 SET LOCAL application_name = 'transaction-only',提交后该设置恢复。PostgreSQL 的 SET LOCAL 将设置限制在当前事务,适合确实需要事务级调整的参数;执行前要已有事务,且要按正常异常路径结束事务。PostgreSQL SET
application_name 是安全、易观察的演示变量。真实应用更应留意 search_path、时区、角色切换、临时对象和会话级锁。跨请求改变这些状态前,要确定谁恢复、何时恢复、恢复失败后连接是否还允许复用。多租户选择 schema 时,优先采用明确、受控的租户映射,不能把外部输入直接拼到 SET search_path 中。
connectionInitSql 只在新建物理连接后运行,无法在每次归还时修复污染;maxLifetime 也不会马上替换正在借出的连接。相关参数职责见 HikariCP 配置说明。
等待预算和连接数量怎样配合
超时发生在不同位置
一次数据访问可能经历这些时间段:
等待池内连接 → 新建物理连接/认证 → 执行 SQL → 读取结果 → 提交或回滚 → 归还已有空闲连接时,会跳过新建连接。各层超时约束的是不同工作:
| 设置 | 常用单位 | 约束对象 | 不能据此推断的结果 |
|---|---|---|---|
Hikari connectionTimeout | 毫秒 | 等待从池中取得连接 | SQL 执行时长已经受控 |
Hikari validationTimeout | 毫秒 | 检测连接有效性的等待 | 下一条 SQL 一定成功 |
pgJDBC connectTimeout | 秒 | socket 建连 | 完整认证、全部地址尝试和整个请求都落在同一个预算内 |
JDBC Statement.setQueryTimeout | 秒 | 语句执行的取消请求 | 归还连接、提交及总请求时间已经被统一限制 |
pgJDBC socketTimeout | 秒 | socket 读取等待,超时会关闭连接 | 服务端写入一定没有完成 |
PostgreSQL statement_timeout | 毫秒或带单位值 | 服务端语句执行 | 客户端等池、结果处理时间也被限制 |
Hikari 要求 connectionTimeout 至少为 250 毫秒,validationTimeout 至少为 250 毫秒且小于连接获取超时。实验设置 700/300 毫秒,是为了观察池等待,不是生产推荐值。HikariCP 配置说明
服务端的 statement_timeout、lock_timeout、idle_in_transaction_session_timeout 分别作用于语句、锁等待和事务内空闲会话,可按账号或事务进行配置。PostgreSQL 客户端连接默认值
总请求期限需要应用统一计算剩余时间,再为借连接、执行、结果处理和收尾预留空间。只把几个独立超时都设为 3 秒,一个请求仍可能顺序等待多个 3 秒。发生写入超时时,要根据所处阶段、事务状态和业务幂等键判断后续动作;连接失效或提交响应丢失时,可能需要查询业务结果,而不是立即再次执行写入。
查询取消后先处理事务
运行:
lab_java timeout实验先关闭自动提交,对 SELECT pg_sleep(3) 设置 1 秒查询超时。pgJDBC 发起取消后,PostgreSQL 返回语句取消错误;同一事务继续查询会报事务已失败:
queryCanceled=true sqlState=57014
transactionNeedsRollback=true sqlState=25P02
queryAfterRollback=true处理顺序是保留第一条错误、回滚事务、再决定是否继续使用连接。若只捕获异常然后继续查询,就会被一串 25P02 淹没,真正的取消或约束错误反而难以找到。跨多条 SQL 的提交、保存点和批量失败恢复,接着阅读 JDBC 本地事务与批处理。
池耗尽可以发生在数据库空闲时
运行:
lab_java exhaustion池只有一个连接。实验先借走它但不归还,再尝试第二次借用,预期得到 SQLTransientConnectionException。释放第一条连接后,下一次借用恢复:
borrowTimedOut=true activeWhileHeld=1
borrowAfterRelease=true activeAfterClose=0等待期间,第一条连接完全可以没有执行 SQL。比如事务方法中插入了远程 HTTP 调用、读取大文件或等待线程锁,数据库看到的可能是空闲会话,应用看到的却是全部连接已借出。
这里的“活动”含义不同:
| 观测位置 | active 指什么 |
|---|---|
| Hikari 活动连接数 | 已借给应用、尚未归还的连接 |
PostgreSQL pg_stat_activity.state='active' | 当前正在执行命令的会话 |
PostgreSQL idle in transaction | 事务尚未结束,当前正在等客户端下一条命令 |
缩短持有连接的时间,往往比扩大池更直接。没有数据库操作的工作可以放到借连接之前;涉及一致性要求时,再判断哪些数据库操作必须保留在同一个事务中。不能为了缩短占用把原本需要原子提交的两条更新随意拆开。
容量按整个部署计算
一个应用实例配置 20 个连接,10 个实例就可能占用 200 个连接;滚动发布时,新旧实例还可能同时存在。数据库总连接额度中也要保留管理、迁移、后台任务和故障处理空间。
粗略估计平均占用量可以使用:
平均占用连接数 ≈ 每秒取得连接次数 × 平均持有时长(秒)例如每秒 200 次借用,平均持有 0.04 秒,平均占用约 8。这个值没有包含突发和长尾,也没有说明数据库能同时高效执行多少查询;它适合检查数量级,不能直接生成 maximumPoolSize。
Hikari 的 maximumPoolSize 限制物理连接总量,minimumIdle 影响空闲连接保有量;省略 minimumIdle 时默认跟随最大池大小。扩大池前,联合查看获取等待、持有时间分布、数据库 CPU、锁等待和查询延迟。数据库已过载时,增加并发连接会让更多请求一起等待。
连接生命周期参数也各有用途:idleTimeout 用于收缩多余空闲连接,keepaliveTime 针对空闲连接做保活,maxLifetime 控制物理连接寿命,leakDetectionThreshold 记录超长借用的疑似泄漏位置。泄漏检测打印警告,不会强行收回仍被业务使用的连接。HikariCP 生命周期参数
拿不到、用不动、还不回来的连接
先定位获取阶段还是使用阶段
故障信息至少应保留异常类型、SQLSTATE、驱动关联异常和调用栈;为 SQL 记录稳定的操作名称或模板标识。绑定参数、密码、完整连接 URL 中的凭据不能直接进入日志。
| 现象或错误 | 优先检查 | 下一步 |
|---|---|---|
缺少驱动或 No suitable driver | 运行时依赖和 JDBC URL 前缀 | 确认驱动 JAR 随部署产物交付 |
28P01 | 实际数据库角色、密码来源 | 修正凭据,不靠重试刷连接 |
08001 或 socket 建连失败 | 容器网络、DNS、端口和数据库监听 | 在应用所在网络检查目标地址 |
| Hikari 获取超时 | active/idle/pending,以及持有连接的调用栈 | 区分池已满与新建连接持续失败 |
57014 | 第一条被取消的语句和设置的超时 | 结束失败事务,再判断是否重试 |
25P02 | 同一事务更早的异常 | 回滚;保留最初错误 |
57P01、08xxx | 会话终止、网络或数据库重启 | 淘汰失效连接;写入结果另行核实 |
SQLSTATE 五位码比本地化错误文本稳定。完整分类可查 PostgreSQL 错误码表。
用真实会话看“借出了但没执行”
在当前终端后台启动观察实验,保留同一组环境变量:
lab_java hold &
LAB_HOLD_JOB=$!程序取得连接并开启事务,执行一次查询后等待 45 秒。出现下面形态的输出后再检查:
holdingSeconds=45 backendPid=<本次后端进程号> state=idle-in-transaction看到上述输出后,在同一终端执行只读查询:
docker compose -p da10-pool exec -T db \
psql -X -U lab_owner -d pool_lab -v ON_ERROR_STOP=1 -c "
SELECT pid, usename, application_name, state,
wait_event_type, wait_event,
xact_start IS NOT NULL AS has_transaction,
left(query, 100) AS last_query
FROM pg_stat_activity
WHERE datname = 'pool_lab' AND application_name = 'pool-lab'
ORDER BY pid"可以看到 lab_app、pool-lab、idle in transaction 和 has_transaction=t;等待事件通常是等待客户端输入。这里的 query 在非 active 状态下是最近一条语句,不能当作仍在执行的 SQL。观察权限也有范围:普通用户对其他账号会话的可见信息受限。pg_stat_activity 字段说明
观察完后执行 wait "$LAB_HOLD_JOB",等待这次后台任务结束。45 秒结束时,实验回滚并归还连接,输出 holdFinished=true。再次检查时,应用池已经关闭,对应会话消失。若真实系统长期出现此状态,应沿业务线程栈查找事务中途的外部调用、结果集处理或锁等待,再把数据库会话和该调用关联起来。
生产诊断中可用同一操作系统身份执行:
jcmd -l
read -r -p '输入上一步确认的应用进程号:' APP_PID
[[ "$APP_PID" =~ ^[0-9]+$ ]] || exit 1
jcmd "$APP_PID" Thread.print -l在容器内执行时要进入应用的 PID 命名空间,并使用包含 JDK 工具的镜像;精简 JRE 镜像可能没有 jcmd。线程转储可能包含业务数据,应限制文件权限及传播范围。jcmd 工具说明
失效连接如何退出池
最后运行:
lab_java invalid实验先取得连接并记录 backend PID,再使用同一个普通数据库角色新建独立连接,对前一条实验会话执行 pg_terminate_backend。PostgreSQL 允许普通角色终止自己角色拥有的会话;跨角色和超级用户会话有额外权限约束。代码只针对刚取得的实验 PID,不扫描或批量终止会话。PostgreSQL 会话管理函数
随后,失效连接执行查询失败,Hikari 输出将连接标为 broken 的警告。断言结果为:
terminatedBackendRejected=true connectionFailureClassAccepted=true
replacementBackend=true queryAfterReplacement=truereplacementBackend=true 来自新旧 PID 对比,后一条断言来自替代连接上的真实查询。测试接受预期的断连 SQLSTATE 分类,不要求不同网络时序都产生一模一样的异常文本。
这类检测减少坏连接继续流转的机会,但连接校验与下一条业务 SQL 之间仍可能断线。对写操作,需要保留业务键和事务结果的查询路径;自动换到一条健康连接,无法替已经失败的事务补做业务决策。
接入 Spring Boot 时保留同一套所有权
Spring Boot 引入 spring-boot-starter-jdbc 后通常优先自动配置 HikariCP。JDBC URL、账号和池参数分别放入 spring.datasource 与 spring.datasource.hikari;自定义 DataSource 时还要检查自动配置是否退出。Spring Boot SQL 数据库配置
spring:
datasource:
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: ${DB_POOL_MAX}
connection-timeout: ${DB_ACQUIRE_TIMEOUT_MS}这段表示配置绑定位置,数值由部署容量和请求预算决定。数据库密码应从对应环境的秘密配置提供,不随示例工程提交。
使用 Spring 管理的事务时,通过 JdbcTemplate 或正确配对的 DataSourceUtils.getConnection/releaseConnection 参与线程绑定连接的管理。直接调用原始 DataSource 新借一条连接,可能在同一个方法里形成第二条独立事务连接。连接的取得和释放必须走配套的管理方式。Spring JDBC 连接管理
结束实验
确认工程目录和项目名后,仅清理这组资源:
docker compose -p da10-pool ps
docker compose -p da10-pool down
unset LAB_DB_PASSWORD LAB_APP_PASSWORD LAB_DB_URL
unset -f lab_java这会删除本实验的容器与网络;tmpfs 中的表数据不可恢复。下载的源码和 .m2-cache 仍保留,便于下次重新运行。不要用全局 docker system prune 代替这一步。
权威资料与规范地址
接口契约按 Java SE 25 查阅;连接复位的实现分析固定到 HikariCP 7.0.2;PostgreSQL 文档使用 18 系列。框架升级后,优先复跑对应实验,再检查实现及默认参数的变化。
