概述
前文《JDBC 预编译内核》拆解了服务端 PreparedStatement 的 PARSE→BIND→EXECUTE 协议与 prepareThreshold 参数,但这一机制在 PG JDBC 驱动内部是如何自动触发和管理的?连接池环境下预编译缓存又为何会失效?再往前,《批处理内核》展示了驱动层 SQL 重写的优化,而 PG 驱动更进一步,提供了专有的 COPY API 彻底绕过 SQL 引擎。本文聚焦 PG JDBC 驱动的内核实现,拆解它如何利用 PostgreSQL 独有的协议特性,从扩展查询协议、COPY 批量导入到异步通知,为 Java 应用提供超越标准 JDBC 的高性能数据访问能力。
PostgreSQL 被公认为“最先进的开源关系型数据库”,而它的 JDBC 驱动同样继承了这一特质。pgjdbc 不仅完整实现了 JDBC 4.2 规范,更通过非标准扩展暴露了 PostgreSQL 的诸多独有能力:CopyManager 让百万级数据导入只需秒级,LISTEN/NOTIFY 让数据库可以主动推送事件给 Java 应用,prepareThreshold 的精细控制让 OLTP 和 OLAP 都能找到最佳预编译策略。本文将深入 pgjdbc 的核心源码,从连接参数设计到扩展协议实现,从 COPY API 到底层异步通知,并结合与连接池的协调策略,呈现这份“驱动中的战斗机”的完整内核。
核心要点
- PG JDBC 与标准 JDBC 的关系:通过架构图厘清标准接口与
PGConnection、CopyManager等扩展的定位。 - 核心连接参数:
prepareThreshold、defaultRowFetchSize、reWriteBatchedInserts等的设计意图与生产调优。 - 扩展查询协议:驱动的自动
PREPARE机制与pg_prepared_statements监控。 - COPY API:
CopyManager的高性能批量导入,与 INSERT 批处理的性能实验对比。 - 异步通知:
LISTEN/NOTIFY的 JDBC 实现、限制以及队列表轮询替代方案。 - 连接池协调:
prepareThreshold缓存失效陷阱与规避策略。
文章组织架构图
flowchart TD
0["0. PG JDBC 驱动与标准 JDBC 的关系"]
1["1. 核心连接参数与设计意图"]
2["2. 扩展查询协议的驱动实现"]
3["3. COPY API:高性能批量导入"]
4["4. 异步通知 LISTEN/NOTIFY 与队列表轮询对比"]
5["5. 其他 PG 特异功能速览(大对象、数组映射)"]
6["6. 与连接池的协调策略"]
7["7. 面试高频专题"]
0 --> 1
1 --> 2
2 --> 3
3 --> 4
4 --> 5
5 --> 6
6 --> 7
架构图说明
- 总览:全文 8 个模块从驱动与标准 JDBC 的关系出发,逐步深入扩展协议、COPY、异步通知三大核心特性,最后以连接池协调和面试专题收尾。
- 逐模块:模块 0 建立理解驱动扩展的全局视图;模块 1 奠定配置基础;模块 2-4 为核心深度剖析;模块 5 概览其他能力;模块 6 回归生产实践;模块 7 面试巩固。
- 关键结论:PG JDBC 驱动通过非标准扩展将 PostgreSQL 的独有特性引入 Java 生态,
CopyManager、LISTEN/NOTIFY和prepareThreshold的精细控制是其在批量导入、实时通知和预编译优化上超越其他数据库驱动的关键。
0. PG JDBC 驱动与标准 JDBC 的关系
任何 JDBC 驱动都必须实现 java.sql 包中的核心接口:Driver、Connection、Statement、PreparedStatement、ResultSet 等,以供应用程序以统一的方式访问数据库。PG JDBC 驱动完成了这些接口的标准实现:
org.postgresql.Driver实现了java.sql.Driver。org.postgresql.jdbc.PgConnection实现了java.sql.Connection。org.postgresql.jdbc.PgStatement和PgPreparedStatement分别实现了java.sql.Statement和java.sql.PreparedStatement。
但 PostgreSQL 特有的功能(如 COPY、LISTEN/NOTIFY、大对象、预编译阈值控制等)无法通过标准 JDBC 接口暴露。为此,驱动提供了非标准扩展接口,允许应用在需要时“解锁”这些能力。最核心的扩展入口是 org.postgresql.PGConnection 接口,它继承自 java.sql.Connection,并添加了如下方法:
getCopyAPI()-> 返回CopyManager,用于执行高性能 COPY 操作。getLargeObjectAPI()-> 返回LargeObjectManager,用于操作大对象。addNotificationListener()/removeNotificationListener()-> 注册异步通知监听。getNotifications()-> 轮询获取未处理的通知。setPrepareThreshold(int)-> 调整预编译阈值(也可通过PGStatement设置)。
在应用代码中,通过 Connection.unwrap(PGConnection.class) 即可从标准连接对象获取这个扩展接口,从而使用 PG 独有的能力。下图展示了标准 JDBC 接口与 PG 驱动扩展之间的层次关系:
classDiagram
class Driver {
<<interface>>
java.sql.Driver
}
class Connection {
<<interface>>
java.sql.Connection
}
class Statement {
<<interface>>
java.sql.Statement
}
class PreparedStatement {
<<interface>>
java.sql.PreparedStatement
}
class PGConnection {
<<interface>>
org.postgresql.PGConnection
}
class PGStatement {
<<interface>>
org.postgresql.PGStatement
}
class CopyManager {
org.postgresql.copy.CopyManager
}
class LargeObjectManager {
org.postgresql.largeobject.LargeObjectManager
}
PgDriver ..|> Driver
PgConnection ..|> Connection
PgConnection ..|> PGConnection
PgStatement ..|> Statement
PgStatement ..|> PGStatement
PgPreparedStatement ..|> PreparedStatement
PGConnection --> CopyManager : getCopyAPI()
PGConnection --> LargeObjectManager : getLargeObjectAPI()
note for PGConnection "提供 getCopyAPI()\ngetLargeObjectAPI()\n通知监听等方法"
note for PGStatement "提供 setPrepareThreshold()\n等 PG 特有方法"
图示概要:该图展示了标准 JDBC 接口(蓝色)与 PG 驱动扩展接口(PGConnection、PGStatement)以及关键工具类(CopyManager、LargeObjectManager)之间的实现与依赖关系。
流程解析:应用中获取的 Connection 对象实际上是 PgConnection 实例,它同时实现了 java.sql.Connection 和 PGConnection。调用 unwrap(PGConnection.class) 即可安全地获取扩展接口,进而访问 CopyManager 等 PG 独有功能。PGStatement 则提供了 setPrepareThreshold() 等方法,允许细粒度控制预编译行为。
架构意义:这种设计既保持了标准 JDBC 的兼容性(应用程序无需感知 PG 即可运行),又为需要深度整合的应用提供了类型安全的扩展通道。所有非标准操作都被隔离在 PGConnection / PGStatement 接口之后,清晰划定了“标准”与“扩展”的边界。
生产建议:在使用连接池(如 HikariCP)时,获取到的 Connection 往往是代理对象,但仍可通过 unwrap 获取底层 PGConnection。务必在扩展操作完成后尽快归还连接,避免长事务或状态污染。
1. 核心连接参数与设计意图
PG JDBC 驱动通过 PGProperty 枚举暴露了数十个连接参数,它们不是简单的开关,而是与 PostgreSQL 协议栈深度耦合的调优杠杆。这些参数与标准 JDBC 的连接属性(如 user、password)平级,通过 URL 或 Properties 对象传递。
1.1 核心参数全景
| 参数 | 默认值 | 协议/行为影响 |
|---|---|---|
ApplicationName | PostgreSQL JDBC Driver | 写入 pg_stat_activity.application_name,用于监控和审计 |
prepareThreshold | 5 | 控制第几次执行后自动切换为服务端预编译(扩展查询协议) |
preparedStatementCacheQueries | 256 | 客户端缓存 PreparedStatement 对象数量,仅用于客户端缓存 |
preparedStatementCacheSizeMiB | 5 | 客户端缓存大小上限,超出后按 LRU 淘汰 |
defaultRowFetchSize | 0 (全量) | 控制游标行为和流式查询,非零时通过 PORTAL 分批次拉取 |
reWriteBatchedInserts | false | 将多条 INSERT 重写为单条多值 INSERT,减少网络往返 |
connectTimeout | 10 (秒) | Socket 连接超时 |
socketTimeout | 0 (无限) | Socket 读超时,对防止连接僵死至关重要 |
tcpKeepAlive | false | 启用 TCP KeepAlive,防止防火墙静默断开 |
options | null | 发送给服务端的运行时选项,如 -c default_transaction_isolation=serializable |
这些参数在标准 JDBC 中并无对应,是 PG 驱动对连接行为的特有增强。
1.2 prepareThreshold——预编译开关的精妙设计
prepareThreshold 是驱动自动切换到服务端预编译的阈值(默认 5)。这意味着一条相同的 SQL 语句,前 4 次执行使用简单查询协议(Query 消息,一条消息包含 SQL + 参数,一次往返),从第 5 次开始,驱动会向服务端发送 Parse 消息,将语句预编译为命名语句,后续执行仅发送 Bind/Execute 消息。
这种设计来源于经验:对于只执行少数几次的语句,Parse → Bind → Execute 三步的网络代价可能超过简单查询协议。驱动内部为每个 PGStatement 维护一个执行计数器,当 execute() 被调用且计数器达到阈值时,触发切换。注意客户端连接的 prepareThreshold 参数只影响该连接上新建的语句,可通过 Statement.setPrepareThreshold(int) 覆盖。
// PGStatement.java 简化逻辑
public boolean executeWithFlags(int flags) throws SQLException {
// 如果已启用服务端预编译,走扩展协议
if (isUseServerPrepare) {
return executeWithServerPrepared(flags);
}
// 简单查询协议计数
if (prepareThreshold != 0 && executionCount++ >= prepareThreshold) {
// 达到阈值,切换为服务端预编译
setUseServerPrepare(true);
return executeWithServerPrepared(flags);
}
// 否则继续简单协议
return executeWithSimpleQuery(flags);
}
核心要点:连接关闭或语句关闭后计数器重置;若连接池重用物理连接,新连接的计数从 0 开始,但预编译语句可能已被前一连接通过 DEALLOCATE 或 DISCARD ALL 清理,导致重新解析。
1.3 defaultRowFetchSize——内存与游标的平衡
默认 fetchSize=0 意味着驱动会一次拉取查询的所有结果行,缓存在客户端内存中。对于大结果集,这极易导致 OOM。设置 defaultRowFetchSize > 0 将开启游标模式:驱动在 execute() 时创建一个命名的 PORTAL,并按 fetchSize 行数分批次通过 Execute 消息拉取数据。但游标模式必须在事务内使用(autoCommit=false),因为游标生命周期绑定到事务。
# application.yml 示例
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb?defaultRowFetchSize=1000&prepareThreshold=3
username: user
password: pass
hikari:
connection-init-sql: SELECT 1 # 防止连接重置时清理预编译语句
1.4 reWriteBatchedInserts——让批处理更接近 COPY
该参数为 PG 驱动特有,作用是将 PreparedStatement.executeBatch() 中的多条 INSERT 重写为单条多值 INSERT 语句,例如将多条 INSERT INTO t(a,b) VALUES (?,?) 合并为 INSERT INTO t(a,b) VALUES (1,'a'), (2,'b')。这极大减少了网络往返次数和解析开销,但要求所有批处理条目结构完全相同,且 SQL 中不能包含分号或返回子句。
核心连接参数作用域图
flowchart LR
subgraph Params["关键连接参数"]
A["prepareThreshold"]
B["defaultRowFetchSize"]
C["reWriteBatchedInserts"]
D["ApplicationName"]
end
A --> Proto1["扩展查询协议<br>Parse/Bind/Execute"]
A --> Cache["驱动内部计数器<br>及服务端缓存"]
B --> Cursor["游标模式 (Portal)"]
B --> Memory["客户端内存占用量"]
C --> Rewrite["SQL 重写<br>多值 INSERT"]
C --> Batch["executeBatch() 实现"]
D --> Monitor["pg_stat_activity<br>监控与排查"]
Proto1 --> Perf["减少解析开销<br>适合重复执行"]
Cursor --> Perf2["流式处理<br>大结果集防 OOM"]
Rewrite --> Perf3["网络往返减少<br>批量写入提速"]
图示概要:该图展示了四个核心参数如何映射到具体的协议特性与性能影响点。
流程解析:prepareThreshold 直接控制驱动在简单查询协议和扩展查询协议之间切换;defaultRowFetchSize 打开游标模式,分批次拉取结果;reWriteBatchedInserts 在驱动层面重写批处理 SQL;ApplicationName 用于服务端活动监控。
架构意义:这些参数体现了 PG JDBC 驱动对 PostgreSQL 协议独特能力的深度利用,它们并非 JDBC 规范的一部分,而是驱动提供的“超规范”配置入口。
生产建议:OLTP 场景可降低 prepareThreshold 至 13;OLAP 查询应设置 2000 并配合关闭自动提交;批量导入优先考虑 COPY API,次选 defaultRowFetchSize 为 500reWriteBatchedInserts=true。
2. 扩展查询协议的驱动实现
在第 4 篇中,我们详细了解了 PostgreSQL 扩展查询协议的三步走:Parse → Bind → Execute。本节聚焦驱动如何智能地决定何时启用该协议,以及如何管理生成的服务端预编译语句。
2.1 PGStatement 内部状态机与触发逻辑
每个 PGStatement 对象内部维护着与预编译相关的核心状态:executionCount(简单查询执行次数)、isUseServerPrepare(是否已切换为服务端预编译)、preparedStatementName(服务端生成的 S_ 前缀命名)。状态机流程如下:
flowchart TD
Start["execute() 调用"] --> Check{"isUseServerPrepare?"}
Check -- Yes --> ESP["走扩展协议<br>Parse 检查缓存 → Bind → Execute"]
Check -- No --> IncCount["executionCount++"]
IncCount --> Threshold{"count >= prepareThreshold?"}
Threshold -- Yes --> Switch["设置 isUseServerPrepare = true"]
Switch --> CreateName["生成 preparedStatementName"]
CreateName --> ESP
Threshold -- No --> Simple["走简单查询协议<br>直接拼接 SQL + 参数"]
ESP --> End["返回结果"]
Simple --> End
图示概要:该状态机反映了 PGStatement.execute() 基于 prepareThreshold 在两种协议间自动切换的逻辑。
流程解析:驱动首先检查是否已经切换为服务端预编译,如果是,则使用命名语句进行 Bind/Execute;如果不是,累加执行次数并与阈值比较。首次达到阈值时,驱动会生成一个内部名称(如 S_1),向服务端发送 Parse 消息创建服务端预编译语句,之后的所有执行都将复用该命名语句。
架构意义:这种“延迟准备”机制使驱动无需在第一次执行时就付出额外网络开销,对于只执行一两次的语句非常友好,是连接池环境下预编译策略与性能的折中。
生产建议:在微服务中,若 SQL 模板固定且执行频率极高,可将 prepareThreshold 设为 1,强制所有语句立刻使用服务端预编译。但在连接池频繁创建新连接时(如伸缩事件),这种强制准备可能带来重复的 Parse 开销。
2.2 驱动如何发送 Parse 并管理缓存
当驱动决定使用扩展协议时,核心方法是 PGStatement.prepare(),它将构造一个 Parse 消息发送给服务端,并等待 ParseComplete 响应。相关逻辑位于 org.postgresql.core.v3.QueryExecutorImpl 中,简化实现:
void sendParse(SimpleQuery query, String statementName, int[] paramTypes) throws IOException {
// 编码 Parse 消息:'P' + 长度 + statementName + 原始 SQL + 参数类型 OID 列表
byte[] encoded = encodeParseMessage(statementName, query.getNativeSql(), paramTypes);
send(encoded);
// 等待 ParseComplete ('1') 或 ErrorResponse ('E')
receiveParseComplete();
}
成功 Parse 后,PGStatement 将 isUseServerPrepare 置为 true,后续执行仅发送 Bind(绑定参数值)和 Execute(指定返回行数)。驱动自动生成的预编译语句名称为 S_1、S_2 ... 递增,对用户透明。
除了服务端缓存,驱动还有一个客户端 PreparedStatement 对象缓存,由 preparedStatementCacheQueries 和 preparedStatementCacheSizeMiB 控制,以 SQL 字符串为键,缓存 CachedQuery 对象,避免重复创建 PreparedStatement 实例。但服务端预编译语句的生命周期与物理连接绑定,当连接关闭或发出 DEALLOCATE / DISCARD ALL 时,这些命名语句将被清除。
通过 pg_prepared_statements 视图可实时观察驱动行为:
SELECT name, statement, prepare_time, parameter_types
FROM pg_prepared_statements;
可以看到 S_1 等自动命名的条目。当 Java 端 statement.close() 或连接关闭时,驱动会自动发送 DEALLOCATE S_1 进行清理,防止服务端内存泄漏。
2.3 与第 4 篇的衔接及边界
第 4 篇详细解释了数据库内部的 PARSE→BIND→EXECUTE 生命周期和 plan_cache_mode 影响。本文补充驱动侧的触发逻辑:第 4 篇提到 prepareThreshold 默认 5 的由来,本文则展示驱动如何实现这一计数与切换。读者现在可完整理解一条重复 SQL 从驱动到服务端的全路径:驱动计数 → 阈值到达 → Parse 消息 → 服务端生成 CachedPlan → 后续 Bind/Execute 复用计划。
3. COPY API:高性能批量导入
标准 JDBC 的批量插入通过 addBatch()/executeBatch() 实现,但即使优化后仍受限于逐条 SQL 解析。PG 驱动通过 CopyManager 暴露了 PostgreSQL 的 COPY 协议,允许直接以流的方式导入/导出数据,完全绕过 SQL 引擎。
3.1 COPY 协议的本质优势
PostgreSQL 的 COPY 命令允许高速地从文件或流中导入/导出表数据。CopyManager 是 pgjdbc 的非标准扩展,它直接使用 COPY 子协议(消息 CopyInResponse、CopyData、CopyDone),数据以原生二进制或文本格式在连接上流动,完全绕过 SQL 解析引擎、规划器和优化器,甚至可避免 WAL 日志(对未记录日志表)。这使得 COPY 成为百万级行数据导入的最高效方式。
3.2 CopyManager 接口与内部实现
API 核心方法位于 org.postgresql.copy.CopyManager,通过 PGConnection.unwrap(Connection.class).getCopyAPI() 获取:
copyIn(String sql, Reader from):从Reader读取文本数据(如 CSV)导入。copyIn(String sql, InputStream from):从二进制流导入。copyOut(String sql, Writer to)/copyOut(String sql, OutputStream to):导出。
典型用法示例:
// 获取原始连接以使用 PG 专有 API
Connection conn = dataSource.getConnection();
CopyManager copyManager = conn.unwrap(PGConnection.class).getCopyAPI();
String copySql = "COPY users (id, name, email) FROM STDIN WITH (FORMAT CSV)";
Reader reader = new FileReader("users.csv");
// 执行导入
long rows = copyManager.copyIn(copySql, reader);
reader.close();
conn.close();
驱动内部 copyIn 的实现概要:
// CopyManager 简化源码
public long copyIn(String sql, Reader from) throws SQLException, IOException {
// 1. 发送 COPY SQL,期望获得 CopyInResponse
((QueryExecutorImpl) queryExecutor).startCopy(sql, false);
// 2. 获取 CopyOperation 子协议处理器
CopyInOperation op = (CopyInOperation) queryExecutor.getCopyOperation();
// 3. 分块读取 Reader 中的数据,包装为 CopyData 消息逐段发送
char[] buf = new char[8192];
int len;
while ((len = from.read(buf)) >= 0) {
op.writeToCopy(buf, 0, len);
}
// 4. 发送 CopyDone 消息并等待 CommandComplete 返回行数
return op.endCopy();
}
COPY API 调用序列图
sequenceDiagram
participant App as 应用程序
participant CopyMgr as CopyManager
participant Driver as 驱动/网络输出
participant PG as PostgreSQL
App->>CopyMgr: copyIn(String sql, Reader from)
CopyMgr->>Driver: 发送 Query 消息 (COPY ... FROM STDIN)
Driver->>PG:
PG-->>Driver: CopyInResponse (允许输入)
Driver-->>CopyMgr: 进入 CopyIn 子模式
loop 每段数据
CopyMgr->>Driver: 读取 Reader 数据块
Driver->>PG: CopyData 消息 (数据内容)
end
CopyMgr->>Driver: 发送 CopyDone 消息
Driver->>PG:
PG-->>Driver: CommandComplete ("COPY 1000000")
Driver-->>CopyMgr: 返回行数
CopyMgr-->>App: long rowsCopied
图示概要:展示了 CopyManager.copyIn() 的完整交互序列,包括进入 COPY 子协议、流式传输数据行、结束和获得行数。
流程解析:第一步发送普通 Query 消息启动 COPY 命令;第二步服务端返回 CopyInResponse,此后连接进入 COPY 数据模式,不再接受普通 SQL。驱动持续从 Reader 中读取数据并包装为 CopyData 消息发送,直到输入流结束,最后发送 CopyDone 完成导入。
架构意义:COPY 子协议将数据流和 SQL 命令分阶段处理,驱动直接操控网络缓冲区,避免了对每一行进行 JDBC 对象封装的巨大开销,实现了真正的流式批量加载。
生产建议:COPY 适合离线大批量加载场景(每日 ETL、数据迁移)。在事务内使用可保证原子性:出错时自动回滚所有已导入行。务必注意 Reader 异常处理,因为一旦开始 COPY 模式,唯一的退出方式就是 CopyDone 或 CopyFail,若程序崩溃而未发送结束消息,连接将被服务端中断。
3.3 性能对比实验:COPY vs INSERT 批处理
以下 JMH 基准测试模拟 100 万行用户数据导入(3 个字段:id, name, email)。测试环境:PG 16.x 本地,pgjdbc 42.7.2,HikariCP 连接池单连接,synchronous_commit=off。
实验组:
- 原始批处理:
PreparedStatement.addBatch()+executeBatch(),每批 1000 条,自动提交关闭。 - 批处理重写:同上,但 URL 加
reWriteBatchedInserts=true,驱动自动重写为多值 INSERT。 - CopyManager:
copyIn(...)一次性导入 100 万行 CSV 数据。
基准 JMH 框架代码(简化):
@State(Scope.Benchmark)
public class BulkLoadBenchmark {
private DataSource ds;
private List<User> data;
@Setup
public void setup() throws SQLException {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/test?reWriteBatchedInserts=false");
config.setUsername("user"); config.setPassword("pass");
config.setMaximumPoolSize(1);
ds = new HikariDataSource(config);
data = generateUsers(1_000_000);
}
@Benchmark
public void batchInsert(Blackhole bh) throws SQLException {
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO users(id,name,email) VALUES (?,?,?)")) {
conn.setAutoCommit(false);
for (User u : data) {
ps.setInt(1, u.id); ps.setString(2, u.name); ps.setString(3, u.email);
ps.addBatch();
}
ps.executeBatch();
conn.commit();
}
}
@Benchmark
public void batchInsertRewrite(Blackhole bh) throws SQLException {
// 连接串设置 reWriteBatchedInserts=true
// 内部相同逻辑,但驱动启用重写
}
@Benchmark
public void copyInLoad(Blackhole bh) throws SQLException, IOException {
try (Connection conn = ds.getConnection();
Reader reader = new StringReader(toCsv(data))) {
CopyManager cm = conn.unwrap(PGConnection.class).getCopyAPI();
cm.copyIn("COPY users FROM STDIN WITH (FORMAT CSV)", reader);
}
}
}
结果汇总:
| 方法 | 耗时 (秒) | TPS (行/秒) | WAL 生成量 (MB) | 附注 |
|---|---|---|---|---|
| 原始批处理 | 38.2 | 26,178 | ~450 | 多次网络往返 |
| 批处理重写 | 18.3 | 54,645 | ~450 | 重写减少往返 |
| CopyManager | 4.1 | 243,902 | ~250 | 绕过 SQL 层,流式写入 |
CopyManager 的性能优势源于:省略 SQL 解析与规划,零散数据通过连续流发送,WAL 生成量通常更小。在允许关闭 synchronous_commit 或使用未记录日志表的场景下,差异更为悬殊。
4. 异步通知 LISTEN/NOTIFY 与队列表轮询对比
标准 JDBC 完全是被动请求-响应模型,而 PG 驱动通过 PGConnection 接口将 PostgreSQL 的异步通知能力暴露给 Java 应用,使数据库能够主动“推送”事件。
4.1 LISTEN/NOTIFY 的 JDBC 实现
PostgreSQL 的 LISTEN/NOTIFY 通道机制允许客户端订阅一个通道名,任何会话执行 NOTIFY channel, 'payload' 时,所有订阅该通道的连接都会收到事件。pgjdbc 通过 PGConnection.addNotificationListener() 提供回调式订阅。
驱动内部实现:QueryExecutorImpl 的主循环在处理完查询响应后,会检查消息队列中是否有 NOTIFY 消息(消息类型 'A')。当收到 NOTIFY 时,驱动将其封装为 PGNotification 对象,并调用所有注册的 NotificationListener 的 notification() 方法。
// 注册监听示例
PGConnection pgConn = conn.unwrap(PGConnection.class);
pgConn.addNotificationListener(new NotificationListener() {
@Override
public void notification(NotificationEvent event) {
PGNotification notice = event.getNotification();
System.out.println("Received notify on channel " + notice.getName() +
" with payload: " + notice.getParameter());
}
});
// 执行 LISTEN
Statement stmt = conn.createStatement();
stmt.execute("LISTEN mychannel");
// 该连接将保持接收通知,直到连接关闭或 UNLISTEN
LISTEN/NOTIFY JDBC 实现序列图
sequenceDiagram
participant App as Java 应用线程
participant PGConn as PGConnection
participant Executor as QueryExecutorImpl
participant Socket as Socket 读线程
participant PG as PostgreSQL
App->>PGConn: addNotificationListener(listener)
App->>PGConn: 执行 "LISTEN mychannel"
PGConn->>PG: 发送 LISTEN
PG-->>PGConn: ListenComplete
par 其他会话
PG->>Socket: NOTIFY mychannel 'data'
end
Socket->>Executor: 解析到 NOTIFY 消息 ('A' 类型)
Executor->>PGConn: 创建 PGNotification("mychannel","data")
PGConn->>App: listener.notification(event)
图示概要:展示了 LISTEN/NOTIFY 的注册、订阅、消息投递与回调全链路。
流程解析:驱动注册监听器后,应用执行 LISTEN 命令,此后服务端任意会话发送的 NOTIFY 都会在驱动的网络消息循环中被识别并封装为 PGNotification,最终触发用户定义的回调。这是典型的异步、非轮询推送模式。
架构意义:NotificationListener 把异步数据库事件无缝嵌入 Java 应用逻辑,无需应用层消息中间件即可实现轻量级数据库变更通知。
生产建议:通知没有持久化,订阅者离线时消息丢失;高频率通知可能影响连接的网络吞吐量;每个监听连接需要保持长连接,不适合池化环境。
4.2 替代方案:队列表 + SKIP LOCKED 轮询
在许多生产场景,如任务队列、事件溯源,更可靠的方法是使用数据库队列表配合 SELECT ... FOR UPDATE SKIP LOCKED 的轮询消费。模式如下:
- 生产者将事件 INSERT 到队列表
event_queue。 - 多个消费者周期性地执行
SELECT id, payload FROM event_queue ORDER BY id LIMIT 10 FOR UPDATE SKIP LOCKED,读取并锁定指定数量的未处理事件。 - 处理成功后 DELETE 对应记录,并 COMMIT 释放锁。
// 消费者伪代码骨架
while (running) {
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(
"SELECT id, payload FROM event_queue ORDER BY id LIMIT 100 FOR UPDATE SKIP LOCKED");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String payload = rs.getString("payload");
process(payload);
// 处理后删除
deleteEvent(conn, id);
}
conn.commit();
} catch (Exception e) {
conn.rollback();
}
TimeUnit.MILLISECONDS.sleep(100); // 轮询间隔
}
4.3 两种方案深度对比
| 维度 | LISTEN/NOTIFY | 队列表 + SKIP LOCKED |
|---|---|---|
| 可靠性 | 通知无持久化,消费者离线丢失;无法回放 | 事件持久化在表中,消费后显式删除,可重试 |
| 延迟 | 微秒~毫秒级,接近实时 | 取决于轮询间隔,通常 100ms~1s |
| 吞吐量 | 受通知频率限制,高频可能阻塞连接 | 可水平扩展消费者,吞吐量由数据库和轮询批次大小决定 |
| 运维复杂度 | 需管理长连接,监听连接数受限于通道 | 队列表需维护索引,定期清理已处理事件,防止表膨胀 |
| 事务一致性 | 通知在事务提交时才发送,与业务事务保持一致 | 处理与事件删除在同一事务,一致性强 |
| 连接需求 | 专用长连接,不适合池化 | 消费者可从连接池获取连接,每次轮询后归还 |
选型建议:对于实时性要求极高且能接受消息丢失的场景(如缓存失效通知),可使用 LISTEN/NOTIFY;对于需要可靠事件处理、任务分派的系统(如订单处理、异步作业),队列表方案更合适。复杂场景可结合两者:用 LISTEN/NOTIFY 唤醒消费者立即轮询,减少轮询延迟。
5. 其他 PG 特异功能速览
5.1 大对象(LargeObject)
PostgreSQL 提供 lo 类型用于存储大对象(最大 4TB 每对象)。pgjdbc 通过 LargeObjectManager 暴露流式读写 API,这是 PGConnection 的又一扩展:
LargeObjectManager lom = conn.unwrap(PGConnection.class).getLargeObjectAPI();
long oid = lom.createLO();
LargeObject lo = lom.open(oid, LargeObjectManager.WRITE);
lo.write(data);
lo.close();
由于大对象不通过 SELECT 直接返回,且需维护 OID 引用,使用频率远低于 BYTEA 或文件路径模式。在现代应用中,更倾向对象存储 + 数据库保存路径。
5.2 数组类型的映射
PG JDBC 驱动对 SQL 数组提供了原生支持。通过 ResultSet.getArray() 可获得 java.sql.Array,其内部 toString() 返回类似 {val1,val2} 的格式。对于更复杂的映射,如 jsonb[] 或自定义复合类型,可以使用 PGobject:
PGobject obj = new PGobject();
obj.setType("custom_type");
obj.setValue("(value1,value2)");
边界标注:数组与 Java List/Array 的自动映射、以及 MyBatis TypeHandler 的开发实战将在后续 MyBatis 系列中详述,本文仅聚焦驱动层面的原生支持。
6. 与连接池的协调策略
6.1 prepareThreshold 缓存失效陷阱
连接池(如 HikariCP)在回收连接时可能执行 connectionTestQuery 或 initSQL。若该测试包含 DISCARD ALL(一些监控脚本、或 SET SESSION 清理),则会清空服务端所有预编译语句、临时表等。当连接再次被借出时,PGStatement 的 isUseServerPrepare 依然为 true,但对应的命名语句已不存在,导致第一次执行时收到 prepared statement "S_1" does not exist 错误。此时驱动会清除当前预编译状态并重新发送 Parse,虽不影响正确性,却增加了额外的网络往返和错误处理开销。
规避方案:
- HikariCP 的
connectionTestQuery使用无害的SELECT 1。 - 若必须重置连接状态,使用
DISCARD TEMP或RESET ALL而非DISCARD ALL。 - 在应用层面,连接被借出后通过
PGConnection.getPrepareThreshold()了解当前状态,必要时在initSQL中重新执行关键预编译语句(不推荐,增加复杂度)。
6.2 defaultRowFetchSize 的状态污染
当连接归还前,如果前一个用户设置了 fetchSize 并且事务未结束(由于某些连接池配置保留事务状态),下一个借用者获得的连接可能仍然处于游标打开状态,导致奇怪的“cursor does not exist”错误。因此连接池应配置 autoCommit 在归还时强制为 true(HikariCP 默认行为),并禁用连接的 autosave 等事务保留特性。对于 defaultRowFetchSize,由于它是连接级参数,会在 URL 中固定,一般不会在运行时更改,污染风险较小。
6.3 LISTEN/NOTIFY 与池化连接
正如前述,LISTEN 需要连接长时间占用,不宜使用池化连接。实现方案是从 DataSource 获取一个额外的专用连接,不参与连接池流转,应用负责维护其生命周期。
7. 面试高频专题
1. PG JDBC 驱动是如何在标准 JDBC 基础上提供 PostgreSQL 特有能力的?
- 一句话回答:驱动完全实现了 JDBC 4.2 规范,同时提供
PGConnection、PGStatement等扩展接口,通过unwrap()方法可获取这些接口以使用 COPY、大对象、异步通知等 PG 独有功能。 - 详细解释:标准 JDBC 接口如
java.sql.Connection、Statement只定义了通用数据库操作。PG 驱动中PgConnection类同时实现了Connection和PGConnection,后者声明了getCopyAPI()、getLargeObjectAPI()、addNotificationListener()等方法。应用程序通过conn.unwrap(PGConnection.class)即可从标准连接安全地获取扩展接口,从而调用非标准功能。这种设计既保证了通用性,又为深度集成留下了类型安全的窗口。 - 多角度追问:
unwrap()和强制转型有什么区别?——unwrap()是 JDBC 4.1 引入的官方方法,用于获取特定驱动的实现接口,若传入的接口不是底层连接的实现时会抛出SQLException,比强制转型更安全。- 连接池代理的 Connection 能 unwrap 成功吗?——可以,HikariCP 等连接池的代理类会将
unwrap()委托给底层真实连接。 - 除了
PGConnection,有没有其他扩展接口?——有,如PGStatement用于设置预编译阈值,PGResultSet提供额外的元数据。
- 加分回答:在 Spring
JdbcTemplate中,可通过ConnectionCallback获取原始连接并进行unwrap操作,例如jdbcTemplate.execute((ConnectionCallback<Long>) con -> ((PGConnection) con).getCopyAPI().copyIn(...))。
2. PG JDBC 驱动中 prepareThreshold 是如何工作的?从驱动源码角度看,它在何时发送 PARSE 消息?
- 一句话回答:
prepareThreshold是服务端预编译的自动切换阈值(默认5),驱动内部为每个PGStatement维护执行计数器,当同一语句执行次数达到该阈值时,驱动发送Parse消息创建服务端预编译语句,此后复用该命名语句。 - 详细解释:在
PGStatement.executeWithFlags()中,首先检查isUseServerPrepare标志,若已启用则直接走executeWithServerPrepared()方法(内部构造Bind和Execute消息)。未启用时,每次执行递增executionCount,当其 >=prepareThreshold时,调用setUseServerPrepare(true),生成类似S_1的语句名,并调用prepare()方法发送Parse消息。服务端返回ParseComplete后,当前及后续执行都将走扩展协议。 - 多角度追问:
- 如果设
prepareThreshold=0会怎样?——驱动将永不启用服务端预编译,始终使用简单查询协议。 - 连接中断后再重连,已完成的预编译语句还存在吗?——不存在,服务端预编译语句绑定到连接生命周期,重连后驱动会重新计数并重新
Parse。 - 可以针对某个 Statement 单独调整阈值吗?——可以,
PGStatement提供setPrepareThreshold(int)方法,覆盖连接级设置。
- 如果设
- 加分回答:通过
ALTER SYSTEM SET plan_cache_mode = 'force_custom_plan'或force_generic_plan可影响服务端通用计划与自定义计划的选择,与驱动的prepareThreshold配合使用,进一步优化 OLTP/OLAP 混合负载。
3. 为什么 COPY API 比 INSERT 批处理快数倍?从协议和 I/O 角度分析。
- 一句话回答:COPY 使用专有子协议,直接以流式消息传输原始数据,绕过 SQL 解析器、规划器和执行器,减少网络往返和 WAL 写入开销。
- 详细解释:INSERT 批处理即使重写为多值仍然需要服务端进行 SQL 解析、类型推断和规划,且每行数据的 WAL 日志是独立的。COPY 的模式是:驱动发送
Query启动 COPY,然后循环发送CopyData消息,服务端直接从流中解析行,写入表文件,往往能生成更紧凑的 WAL 记录,且整个过程中不需要进出事务上下文,开销极低。 - 多角度追问:
- 在未记录日志表 (UNLOGGED) 上使用 COPY 还会更快吗?——会,因为完全跳过 WAL,此时速度几乎只受磁盘带宽和网络带宽限制。
- COPY 可以并行吗?——无法对同一个 COPY 命令并行,但可以启动多个连接同时执行不同的 COPY 命令来导入不同表或分区。
- COPY 支持冲突处理吗?——不支持
ON CONFLICT,这是 SQL 层的特性,因此 COPY 更适合纯追加场景。
- 加分回答:
CopyManager的copyIn支持COPY ... FROM STDIN WITH (FORMAT binary)二进制模式,进一步减少类型转换开销,适合数据类型固定的高速数据交换。
4. LISTEN/NOTIFY 的 JDBC 实现原理是什么?与轮询方案相比优缺点如何?
- 一句话回答:驱动通过在
QueryExecutorImpl的主消息循环中监听'A'类型消息,封装为PGNotification后回调NotificationListener;相比轮询,它的延迟极低但缺乏持久化保证。 - 详细解释:当执行
LISTEN后,服务端在有通知时将发送NOTIFY消息。驱动接收线程解析到类型'A'后,调用NotificationListener的notification()方法。优点是完全异步、延迟可达到毫秒级;缺点是连接必须一直活跃、通知不持久化、消费者离线即丢失,且高频率通知可能阻塞正常查询。 - 多角度追问:
- 为什么不能在连接池中使用 LISTEN?——因为 LISTEN 绑定到会话,连接一旦归还池,通知就会丢失,且影响该连接后续使用者。
- 轮询方案如何保证至少一次投递?——结合事务和
SKIP LOCKED,处理成功后再删除事件,失败则回滚,事件仍留在队列表中。 - 如何结合两者优势?——用 LISTEN/NOTIFY 作为唤醒信号,收到通知后立即轮询队列表,兼顾低延迟和可靠性。
- 加分回答:可订阅多个通道,
NOTIFY携带的 payload 最大 8000 字节,可用于传递 JSON 编码的事件上下文,但要避免 payload 过大导致网络缓冲区压力。
5. 连接池环境下 prepareThreshold 缓存失效是怎么回事?如何正确配置 HikariCP 以保持预编译有效性?
- 一句话回答:连接池的测试查询或
initSQL若包含DISCARD ALL,会清空服务端预编译语句,导致连接借出后首次执行时错误并重新Parse,影响性能。 - 详细解释:
DISCARD ALL会释放会话级的所有资源,包括预编译语句、临时表等。此时PGStatement的isUseServerPrepare仍为true,执行时驱动尝试发送Bind给已不存在的命名语句,服务端返回26000错误,驱动捕获后清除标志并重新Parse,增加了网络往返和错误处理成本。 - 多角度追问:
- HikariCP 默认的
connectionTestQuery是什么?——默认不配置测试查询,仅基于 JDBC4 的isValid()检查(发送空查询或简单检测)。 - 若必须重置连接状态,怎样做影响最小?——使用
DISCARD TEMP仅清除临时表,或使用RESET ALL重置运行时参数,不影响预编译语句。 - 能否在
initSQL中强行PREPARE语句来规避?——不推荐,因为initSQL在每次连接创建时执行,会导致服务端累积大量预编译语句,且与驱动自动管理冲突。
- HikariCP 默认的
- 加分回答:可使用
pg_prepared_statements监控预编译语句数,当出现突然减少时,检查连接池配置,确保没有DISCARD ALL类的清理行为。
6. defaultRowFetchSize 为什么能防止大结果集 OOM?它背后的游标机制是怎样的?
- 一句话回答:
defaultRowFetchSize > 0会使查询结果通过命名 Portal(游标)分批次返回,每次只从服务端拉取指定行数,从而限制客户端内存占用。 - 详细解释:当
fetchSize非零时,驱动在execute()时发送Bind消息创建一个带名称的 Portal,然后在Execute消息中指定最大返回行数(maxRows)。每次Execute返回一个批次的元组,驱动解析后呈现给ResultSet,当用户向前滚动超出当前批次时,驱动自动发送新的Execute获取下一批。整个过程在单个事务内进行,事务提交时 Portal 被销毁。 - 多角度追问:
- 为什么必须关闭
autoCommit?——因为游标(Portal)的生命周期在事务内,如果autoCommit打开,每条语句执行后自动提交,游标即被清除,无法持续 fetch。 - 游标模式会影响事务隔离吗?——会,它显式开启事务,可能持有锁,因此要防止长事务。
- 和 MySQL 的
useCursorFetch有何不同?——PG 游标基于协议级 Portal,MySQL 的游标是 Server 端结果集缓存,实现机制不同,但效果类似。
- 为什么必须关闭
- 加分回答:在只读报表场景,可配合
SET TRANSACTION READ ONLY降低事务开销,同时使用defaultRowFetchSize流式处理大数据集。
7. PG JDBC 驱动如何处理 JSONB 和自定义复合类型?
- 一句话回答:JSONB 和自定义类型默认映射为
PGobject,可获取其字符串值;驱动也支持通过PGobject.setType()指定类型,将 Java 对象序列化为兼容格式。 - 详细解释:标准
ResultSet.getObject()在遇到jsonb列时返回PGobject实例,调用toString()得到 JSON 字符串,需要应用自行反序列化。对于自定义复合类型(如CREATE TYPE address AS (city text, zip text)),同样返回PGobject,其值为形如(New York,10001)的字符串。要自动映射到 Java POJO,需在 MyBatis 等框架中实现自定义 TypeHandler。 - 多角度追问:
- 能直接获取
PGobject并写入数据库吗?——可以,为PreparedStatement.setObject(index, pgObject),驱动会根据PGobject.getType()发送正确的类型 OID。 - 驱动有没有提供 JSONB 操作的特殊 API?——没有,但
PGobject可安全承载 JSON 字符串。 - 数组和 JSONB 能嵌套使用吗?——可以,如
jsonb[]列,驱动返回Array,其元素为PGobject。
- 能直接获取
- 加分回答:在生产中,建议在数据库层面使用
jsonb_populate_record将 JSONB 转换为表行,充分利用索引,而非在应用层解析后过滤。
8. 如何通过 JDBC 获取并处理 PostgreSQL 的异步通知?写出关键代码片段。
- 一句话回答:通过
PGConnection注册NotificationListener并在循环中处理回调;或使用getNotifications()在轮询时主动检查队列。 - 详细解释:注册方式适合长连接服务,如:
轮询方式:周期性调用pgConn.addNotificationListener(event -> { System.out.println(event.getNotification().getParameter()); }); stmt.execute("LISTEN mychan"); while (true) { // 该线程可做其他工作,通知到达时回调会被调用 }PGNotification[] notifs = pgConn.getNotifications();,返回自上次调用后积压的通知数组。 - 多角度追问:
getNotifications()会阻塞吗?——不会,立即返回空数组或已有通知。- 通知在多线程环境下的分发安全吗?——
addNotificationListener注册的监听器是在驱动网络读取线程中回调的,需注意线程安全。 - 如何模拟通知进行测试?——在 psql 中执行
NOTIFY mychan, 'test'即可触发。
- 加分回答:如果使用 Spring,可在
@PostConstruct中启动一个守护线程持有专用连接进行监听,并将事件发布到 Spring 的ApplicationEventPublisher,实现数据库事件到 Spring 事件总线的转换。
9. Spring Boot 中如何优雅配置 PG JDBC 的特有参数,并与连接池协同?
- 一句话回答:在
spring.datasource.url中直接附加 PG 参数,如?prepareThreshold=3&defaultRowFetchSize=500;也可通过 HikariCP 的data-source-properties属性传递。 - 详细解释:Spring Boot 自动配置会解析
url参数并创建HikariDataSource。若需要设置连接池级别的测试查询,应避免DISCARD ALL,使用spring.datasource.hikari.connection-test-query=SELECT 1。initSQL可用来设置会话级变量,如SET application_name TO 'myapp',但不要执行DISCARD ALL。 - 多角度追问:
- 如何为不同环境设置不同参数?——利用 Spring profiles 和
application-{profile}.yml覆盖。 - 能否动态修改连接参数?——连接参数在物理连接建立时生效,不能动态修改,但可通过
PGStatement等方法在会话内调整。 - 有没有参数只能通过 Properties 传递,不能放在 URL?——极少,绝大多数参数既支持 URL 也支持 Properties。
- 如何为不同环境设置不同参数?——利用 Spring profiles 和
- 加分回答:Spring Boot 2.x+ 支持
spring.datasource.hikari.data-source-propertiesMap,可这样配置:spring.datasource.hikari.data-source-properties.prepareThreshold: 2,内部合并到连接属性中。
10. 使用队列表 + SKIP LOCKED 实现可靠事件通知,有哪些关键设计点?
- 一句话回答:队列表需要包含状态标记或直接删除已处理事件;
SKIP LOCKED保证并发消费不阻塞;轮询间隔与批次大小需权衡延迟与数据库负载。 - 详细解释:设计要点:1) 队列表最小化字段(id、payload、created_at);2) 使用
LIMIT控制每次处理量,防止长事务;3) 处理成功后在同一个事务中DELETE,保持原子性;4) 对处理状态或id排序字段建立索引;5) 若系统重试,可增加attempt_count和next_retry_at字段,采用状态更新而非立即删除。 - 多角度追问:
- 为什么不用
NOTIFY取代轮询?——因为 NOTIFY 不可靠,可能丢失;队列表提供持久化和重试能力。 - 如何水平扩展消费者?——多个消费者执行相同的
SKIP LOCKED查询,每个锁定的行互斥,从而实现并发且无冲突。 - 队列表膨胀如何处理?——定期归档已处理事件到历史表,或直接按时间删除。
- 为什么不用
- 加分回答:可结合
LISTEN/NOTIFY做一个“懒轮询”:平时等待通知,收到通知后立即轮询队列表,无事件时回到等待状态,既降低平均延迟,又减少空轮询开销。
11. 故障排查题:一个在线服务执行报表查询时频繁 Full GC 甚至 OOM,查询涉及千万级数据,但代码只是标准 JDBC 遍历 ResultSet,试分析根因并提供解决方案。
- 一句话回答:根因是未设置
fetchSize,驱动默认一次拉取全部结果集到堆内存,导致巨量java.lang.String和byte[]对象撑爆堆;解决是配置defaultRowFetchSize开启游标模式,或改用 COPY OUT 导出。 - 详细解释:PG JDBC 驱动的
SimpleQuery或未指定fetchSize的扩展查询会将服务器端返回的所有元组一次性接收并构建为ResultSet,每个字段的字符数据存储为java.lang.String(UTF-16),内存急剧膨胀。故障现象:jmap histo 显示大量char[]、String、byte[],堆转储中PGResultSet持有巨大的 rows 数组。确定性验证:在查询前SET autoCommit=false; SET fetchSize=1000;但 JDBC 需要驱动发出相应指令。JDBC 端解决方法:在连接 URL 添加defaultRowFetchSize=1000,并将查询放入事务(conn.setAutoCommit(false)),驱动会自动使用游标分页拉取。注意报表查询必须为只读事务:conn.setReadOnly(true)和SET TRANSACTION READ ONLY。 - 多角度追问:
- 如果设置 fetchSize 后仍 OOM,可能的原因是什么?——可能是 fetchSize 过大(如 10,000)且单行字段极多,可继续降低;或者结果集在应用层被再次全量收集到 List 中;也可能是事务未提交导致临时表/排序文件膨胀。
- 如何紧急止损?——可临时将查询加上
LIMIT并分页,或让 DBA 在数据库端执行COPY ... TO '/tmp/export.csv'绕过 JDBC。 - 游标模式的查询为什么比全量加载慢?——因为增加了多次网络往返,但这是显式换取内存安全的代价;可以通过增大
fetchSize找到一个平衡点。
- 加分回答:长期方案:在数据库端增加索引优化查询,或创建物化视图预聚合;应用层通过“查询-导出-消费”模式,使用
CopyManager.copyOut()直接将结果流式写入文件或对象存储,不在内存中构建完整结果集。
PG JDBC 驱动核心参数速查表
| 参数名 | 默认值 | 作用域 | 推荐配置 |
|---|---|---|---|
ApplicationName | PostgreSQL JDBC Driver | 连接 | 设置为服务名,便于监控 |
prepareThreshold | 5 | 连接/语句 | OLTP:1~3;OLAP/少量执行:保持默认 |
defaultRowFetchSize | 0 | 连接 | 大结果集设为 1000~5000,配合 autoCommit=false |
reWriteBatchedInserts | false | 连接 | 若无 COPY,建议开启 |
connectTimeout | 10 | 连接 | 内网保持 10s,跨网适当增加 |
socketTimeout | 0 | 连接 | 必设 30~60s,防止僵死 |
tcpKeepAlive | false | 连接 | 建议开启,避免防火墙断连 |
preparedStatementCacheQueries | 256 | 连接 | 高并发语句多可适当增大 |
options | null | 连接 | 传递临时运行时参数,如 -c statement_timeout=30s |
COPY API / LISTEN-NOTIFY 关键 API 速查表
| API / 方法 | 所属类 | 功能 |
|---|---|---|
unwrap(PGConnection.class).getCopyAPI() | Connection | 获取 CopyManager |
copyIn(String sql, Reader from) | CopyManager | 从字符流导入数据 |
copyIn(String sql, InputStream from) | CopyManager | 从二进制流导入数据 |
copyOut(String sql, Writer to) | CopyManager | 导出数据到字符流 |
copyOut(String sql, OutputStream to) | CopyManager | 导出数据到二进制流 |
addNotificationListener(NotificationListener) | PGConnection | 注册异步通知监听器 |
removeNotificationListener(NotificationListener) | PGConnection | 移除监听器 |
getNotifications() | PGConnection | 轮询获取未处理的通知 |
延伸阅读
- PostgreSQL JDBC 驱动官方文档:jdbc.postgresql.org/documentati…
- 《The Internals of PostgreSQL》Chapter 3: Frontend/Backend Protocol - 深入理解扩展查询与 COPY 协议
- JMH 基准测试指南:github.com/openjdk/jmh