PostgreSQL JDBC 驱动内核:扩展协议、COPY API 与异步通知

4 阅读34分钟

概述

前文《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 的关系:通过架构图厘清标准接口与 PGConnectionCopyManager 等扩展的定位。
  • 核心连接参数prepareThresholddefaultRowFetchSizereWriteBatchedInserts 等的设计意图与生产调优。
  • 扩展查询协议:驱动的自动 PREPARE 机制与 pg_prepared_statements 监控。
  • COPY APICopyManager 的高性能批量导入,与 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 生态,CopyManagerLISTEN/NOTIFYprepareThreshold 的精细控制是其在批量导入、实时通知和预编译优化上超越其他数据库驱动的关键。

0. PG JDBC 驱动与标准 JDBC 的关系

任何 JDBC 驱动都必须实现 java.sql 包中的核心接口:DriverConnectionStatementPreparedStatementResultSet 等,以供应用程序以统一的方式访问数据库。PG JDBC 驱动完成了这些接口的标准实现:

  • org.postgresql.Driver 实现了 java.sql.Driver
  • org.postgresql.jdbc.PgConnection 实现了 java.sql.Connection
  • org.postgresql.jdbc.PgStatementPgPreparedStatement 分别实现了 java.sql.Statementjava.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.ConnectionPGConnection。调用 unwrap(PGConnection.class) 即可安全地获取扩展接口,进而访问 CopyManager 等 PG 独有功能。PGStatement 则提供了 setPrepareThreshold() 等方法,允许细粒度控制预编译行为。

架构意义:这种设计既保持了标准 JDBC 的兼容性(应用程序无需感知 PG 即可运行),又为需要深度整合的应用提供了类型安全的扩展通道。所有非标准操作都被隔离在 PGConnection / PGStatement 接口之后,清晰划定了“标准”与“扩展”的边界。

生产建议:在使用连接池(如 HikariCP)时,获取到的 Connection 往往是代理对象,但仍可通过 unwrap 获取底层 PGConnection。务必在扩展操作完成后尽快归还连接,避免长事务或状态污染。


1. 核心连接参数与设计意图

PG JDBC 驱动通过 PGProperty 枚举暴露了数十个连接参数,它们不是简单的开关,而是与 PostgreSQL 协议栈深度耦合的调优杠杆。这些参数与标准 JDBC 的连接属性(如 userpassword)平级,通过 URL 或 Properties 对象传递。

1.1 核心参数全景

参数默认值协议/行为影响
ApplicationNamePostgreSQL JDBC Driver写入 pg_stat_activity.application_name,用于监控和审计
prepareThreshold5控制第几次执行后自动切换为服务端预编译(扩展查询协议)
preparedStatementCacheQueries256客户端缓存 PreparedStatement 对象数量,仅用于客户端缓存
preparedStatementCacheSizeMiB5客户端缓存大小上限,超出后按 LRU 淘汰
defaultRowFetchSize0 (全量)控制游标行为和流式查询,非零时通过 PORTAL 分批次拉取
reWriteBatchedInsertsfalse将多条 INSERT 重写为单条多值 INSERT,减少网络往返
connectTimeout10 (秒)Socket 连接超时
socketTimeout0 (无限)Socket 读超时,对防止连接僵死至关重要
tcpKeepAlivefalse启用 TCP KeepAlive,防止防火墙静默断开
optionsnull发送给服务端的运行时选项,如 -c default_transaction_isolation=serializable

这些参数在标准 JDBC 中并无对应,是 PG 驱动对连接行为的特有增强。

1.2 prepareThreshold——预编译开关的精妙设计

prepareThreshold 是驱动自动切换到服务端预编译的阈值(默认 5)。这意味着一条相同的 SQL 语句,前 4 次执行使用简单查询协议(Query 消息,一条消息包含 SQL + 参数,一次往返),从第 5 次开始,驱动会向服务端发送 Parse 消息,将语句预编译为命名语句,后续执行仅发送 Bind/Execute 消息。

这种设计来源于经验:对于只执行少数几次的语句,ParseBindExecute 三步的网络代价可能超过简单查询协议。驱动内部为每个 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 开始,但预编译语句可能已被前一连接通过 DEALLOCATEDISCARD 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 查询应设置 defaultRowFetchSize 为 5002000 并配合关闭自动提交;批量导入优先考虑 COPY API,次选 reWriteBatchedInserts=true


2. 扩展查询协议的驱动实现

在第 4 篇中,我们详细了解了 PostgreSQL 扩展查询协议的三步走:ParseBindExecute。本节聚焦驱动如何智能地决定何时启用该协议,以及如何管理生成的服务端预编译语句。

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 后,PGStatementisUseServerPrepare 置为 true,后续执行仅发送 Bind(绑定参数值)和 Execute(指定返回行数)。驱动自动生成的预编译语句名称为 S_1S_2 ... 递增,对用户透明。

除了服务端缓存,驱动还有一个客户端 PreparedStatement 对象缓存,由 preparedStatementCacheQueriespreparedStatementCacheSizeMiB 控制,以 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 命令允许高速地从文件或流中导入/导出表数据。CopyManagerpgjdbc 的非标准扩展,它直接使用 COPY 子协议(消息 CopyInResponseCopyDataCopyDone),数据以原生二进制或文本格式在连接上流动,完全绕过 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 模式,唯一的退出方式就是 CopyDoneCopyFail,若程序崩溃而未发送结束消息,连接将被服务端中断。

3.3 性能对比实验:COPY vs INSERT 批处理

以下 JMH 基准测试模拟 100 万行用户数据导入(3 个字段:id, name, email)。测试环境:PG 16.x 本地,pgjdbc 42.7.2,HikariCP 连接池单连接,synchronous_commit=off

实验组

  1. 原始批处理PreparedStatement.addBatch() + executeBatch(),每批 1000 条,自动提交关闭。
  2. 批处理重写:同上,但 URL 加 reWriteBatchedInserts=true,驱动自动重写为多值 INSERT。
  3. CopyManagercopyIn(...) 一次性导入 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.226,178~450多次网络往返
批处理重写18.354,645~450重写减少往返
CopyManager4.1243,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 对象,并调用所有注册的 NotificationListenernotification() 方法。

// 注册监听示例
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)在回收连接时可能执行 connectionTestQueryinitSQL。若该测试包含 DISCARD ALL(一些监控脚本、或 SET SESSION 清理),则会清空服务端所有预编译语句、临时表等。当连接再次被借出时,PGStatementisUseServerPrepare 依然为 true,但对应的命名语句已不存在,导致第一次执行时收到 prepared statement "S_1" does not exist 错误。此时驱动会清除当前预编译状态并重新发送 Parse,虽不影响正确性,却增加了额外的网络往返和错误处理开销。

规避方案

  1. HikariCP 的 connectionTestQuery 使用无害的 SELECT 1
  2. 若必须重置连接状态,使用 DISCARD TEMPRESET ALL 而非 DISCARD ALL
  3. 在应用层面,连接被借出后通过 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 规范,同时提供 PGConnectionPGStatement 等扩展接口,通过 unwrap() 方法可获取这些接口以使用 COPY、大对象、异步通知等 PG 独有功能。
  • 详细解释:标准 JDBC 接口如 java.sql.ConnectionStatement 只定义了通用数据库操作。PG 驱动中 PgConnection 类同时实现了 ConnectionPGConnection,后者声明了 getCopyAPI()getLargeObjectAPI()addNotificationListener() 等方法。应用程序通过 conn.unwrap(PGConnection.class) 即可从标准连接安全地获取扩展接口,从而调用非标准功能。这种设计既保证了通用性,又为深度集成留下了类型安全的窗口。
  • 多角度追问
    1. unwrap() 和强制转型有什么区别?——unwrap() 是 JDBC 4.1 引入的官方方法,用于获取特定驱动的实现接口,若传入的接口不是底层连接的实现时会抛出 SQLException,比强制转型更安全。
    2. 连接池代理的 Connection 能 unwrap 成功吗?——可以,HikariCP 等连接池的代理类会将 unwrap() 委托给底层真实连接。
    3. 除了 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() 方法(内部构造 BindExecute 消息)。未启用时,每次执行递增 executionCount,当其 >= prepareThreshold 时,调用 setUseServerPrepare(true),生成类似 S_1 的语句名,并调用 prepare() 方法发送 Parse 消息。服务端返回 ParseComplete 后,当前及后续执行都将走扩展协议。
  • 多角度追问
    1. 如果设 prepareThreshold=0 会怎样?——驱动将永不启用服务端预编译,始终使用简单查询协议。
    2. 连接中断后再重连,已完成的预编译语句还存在吗?——不存在,服务端预编译语句绑定到连接生命周期,重连后驱动会重新计数并重新 Parse
    3. 可以针对某个 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 记录,且整个过程中不需要进出事务上下文,开销极低。
  • 多角度追问
    1. 在未记录日志表 (UNLOGGED) 上使用 COPY 还会更快吗?——会,因为完全跳过 WAL,此时速度几乎只受磁盘带宽和网络带宽限制。
    2. COPY 可以并行吗?——无法对同一个 COPY 命令并行,但可以启动多个连接同时执行不同的 COPY 命令来导入不同表或分区。
    3. COPY 支持冲突处理吗?——不支持 ON CONFLICT,这是 SQL 层的特性,因此 COPY 更适合纯追加场景。
  • 加分回答CopyManagercopyIn 支持 COPY ... FROM STDIN WITH (FORMAT binary) 二进制模式,进一步减少类型转换开销,适合数据类型固定的高速数据交换。

4. LISTEN/NOTIFY 的 JDBC 实现原理是什么?与轮询方案相比优缺点如何?

  • 一句话回答:驱动通过在 QueryExecutorImpl 的主消息循环中监听 'A' 类型消息,封装为 PGNotification 后回调 NotificationListener;相比轮询,它的延迟极低但缺乏持久化保证。
  • 详细解释:当执行 LISTEN 后,服务端在有通知时将发送 NOTIFY 消息。驱动接收线程解析到类型 'A' 后,调用 NotificationListenernotification() 方法。优点是完全异步、延迟可达到毫秒级;缺点是连接必须一直活跃、通知不持久化、消费者离线即丢失,且高频率通知可能阻塞正常查询。
  • 多角度追问
    1. 为什么不能在连接池中使用 LISTEN?——因为 LISTEN 绑定到会话,连接一旦归还池,通知就会丢失,且影响该连接后续使用者。
    2. 轮询方案如何保证至少一次投递?——结合事务和 SKIP LOCKED,处理成功后再删除事件,失败则回滚,事件仍留在队列表中。
    3. 如何结合两者优势?——用 LISTEN/NOTIFY 作为唤醒信号,收到通知后立即轮询队列表,兼顾低延迟和可靠性。
  • 加分回答:可订阅多个通道,NOTIFY 携带的 payload 最大 8000 字节,可用于传递 JSON 编码的事件上下文,但要避免 payload 过大导致网络缓冲区压力。

5. 连接池环境下 prepareThreshold 缓存失效是怎么回事?如何正确配置 HikariCP 以保持预编译有效性?

  • 一句话回答:连接池的测试查询或 initSQL 若包含 DISCARD ALL,会清空服务端预编译语句,导致连接借出后首次执行时错误并重新 Parse,影响性能。
  • 详细解释DISCARD ALL 会释放会话级的所有资源,包括预编译语句、临时表等。此时 PGStatementisUseServerPrepare 仍为 true,执行时驱动尝试发送 Bind 给已不存在的命名语句,服务端返回 26000 错误,驱动捕获后清除标志并重新 Parse,增加了网络往返和错误处理成本。
  • 多角度追问
    1. HikariCP 默认的 connectionTestQuery 是什么?——默认不配置测试查询,仅基于 JDBC4 的 isValid() 检查(发送空查询或简单检测)。
    2. 若必须重置连接状态,怎样做影响最小?——使用 DISCARD TEMP 仅清除临时表,或使用 RESET ALL 重置运行时参数,不影响预编译语句。
    3. 能否在 initSQL 中强行 PREPARE 语句来规避?——不推荐,因为 initSQL 在每次连接创建时执行,会导致服务端累积大量预编译语句,且与驱动自动管理冲突。
  • 加分回答:可使用 pg_prepared_statements 监控预编译语句数,当出现突然减少时,检查连接池配置,确保没有 DISCARD ALL 类的清理行为。

6. defaultRowFetchSize 为什么能防止大结果集 OOM?它背后的游标机制是怎样的?

  • 一句话回答defaultRowFetchSize > 0 会使查询结果通过命名 Portal(游标)分批次返回,每次只从服务端拉取指定行数,从而限制客户端内存占用。
  • 详细解释:当 fetchSize 非零时,驱动在 execute() 时发送 Bind 消息创建一个带名称的 Portal,然后在 Execute 消息中指定最大返回行数(maxRows)。每次 Execute 返回一个批次的元组,驱动解析后呈现给 ResultSet,当用户向前滚动超出当前批次时,驱动自动发送新的 Execute 获取下一批。整个过程在单个事务内进行,事务提交时 Portal 被销毁。
  • 多角度追问
    1. 为什么必须关闭 autoCommit?——因为游标(Portal)的生命周期在事务内,如果 autoCommit 打开,每条语句执行后自动提交,游标即被清除,无法持续 fetch。
    2. 游标模式会影响事务隔离吗?——会,它显式开启事务,可能持有锁,因此要防止长事务。
    3. 和 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。
  • 多角度追问
    1. 能直接获取 PGobject 并写入数据库吗?——可以,为 PreparedStatement.setObject(index, pgObject),驱动会根据 PGobject.getType() 发送正确的类型 OID。
    2. 驱动有没有提供 JSONB 操作的特殊 API?——没有,但 PGobject 可安全承载 JSON 字符串。
    3. 数组和 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();,返回自上次调用后积压的通知数组。
  • 多角度追问
    1. getNotifications() 会阻塞吗?——不会,立即返回空数组或已有通知。
    2. 通知在多线程环境下的分发安全吗?——addNotificationListener 注册的监听器是在驱动网络读取线程中回调的,需注意线程安全。
    3. 如何模拟通知进行测试?——在 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 1initSQL 可用来设置会话级变量,如 SET application_name TO 'myapp',但不要执行 DISCARD ALL
  • 多角度追问
    1. 如何为不同环境设置不同参数?——利用 Spring profiles 和 application-{profile}.yml 覆盖。
    2. 能否动态修改连接参数?——连接参数在物理连接建立时生效,不能动态修改,但可通过 PGStatement 等方法在会话内调整。
    3. 有没有参数只能通过 Properties 传递,不能放在 URL?——极少,绝大多数参数既支持 URL 也支持 Properties。
  • 加分回答:Spring Boot 2.x+ 支持 spring.datasource.hikari.data-source-properties Map,可这样配置: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_countnext_retry_at 字段,采用状态更新而非立即删除。
  • 多角度追问
    1. 为什么不用 NOTIFY 取代轮询?——因为 NOTIFY 不可靠,可能丢失;队列表提供持久化和重试能力。
    2. 如何水平扩展消费者?——多个消费者执行相同的 SKIP LOCKED 查询,每个锁定的行互斥,从而实现并发且无冲突。
    3. 队列表膨胀如何处理?——定期归档已处理事件到历史表,或直接按时间删除。
  • 加分回答:可结合 LISTEN/NOTIFY 做一个“懒轮询”:平时等待通知,收到通知后立即轮询队列表,无事件时回到等待状态,既降低平均延迟,又减少空轮询开销。

11. 故障排查题:一个在线服务执行报表查询时频繁 Full GC 甚至 OOM,查询涉及千万级数据,但代码只是标准 JDBC 遍历 ResultSet,试分析根因并提供解决方案。

  • 一句话回答:根因是未设置 fetchSize,驱动默认一次拉取全部结果集到堆内存,导致巨量 java.lang.Stringbyte[] 对象撑爆堆;解决是配置 defaultRowFetchSize 开启游标模式,或改用 COPY OUT 导出。
  • 详细解释:PG JDBC 驱动的 SimpleQuery 或未指定 fetchSize 的扩展查询会将服务器端返回的所有元组一次性接收并构建为 ResultSet,每个字段的字符数据存储为 java.lang.String(UTF-16),内存急剧膨胀。故障现象:jmap histo 显示大量 char[]Stringbyte[],堆转储中 PGResultSet 持有巨大的 rows 数组。确定性验证:在查询前 SET autoCommit=false; SET fetchSize=1000; 但 JDBC 需要驱动发出相应指令。JDBC 端解决方法:在连接 URL 添加 defaultRowFetchSize=1000,并将查询放入事务(conn.setAutoCommit(false)),驱动会自动使用游标分页拉取。注意报表查询必须为只读事务:conn.setReadOnly(true)SET TRANSACTION READ ONLY
  • 多角度追问
    1. 如果设置 fetchSize 后仍 OOM,可能的原因是什么?——可能是 fetchSize 过大(如 10,000)且单行字段极多,可继续降低;或者结果集在应用层被再次全量收集到 List 中;也可能是事务未提交导致临时表/排序文件膨胀。
    2. 如何紧急止损?——可临时将查询加上 LIMIT 并分页,或让 DBA 在数据库端执行 COPY ... TO '/tmp/export.csv' 绕过 JDBC。
    3. 游标模式的查询为什么比全量加载慢?——因为增加了多次网络往返,但这是显式换取内存安全的代价;可以通过增大 fetchSize 找到一个平衡点。
  • 加分回答:长期方案:在数据库端增加索引优化查询,或创建物化视图预聚合;应用层通过“查询-导出-消费”模式,使用 CopyManager.copyOut() 直接将结果流式写入文件或对象存储,不在内存中构建完整结果集。

PG JDBC 驱动核心参数速查表

参数名默认值作用域推荐配置
ApplicationNamePostgreSQL JDBC Driver连接设置为服务名,便于监控
prepareThreshold5连接/语句OLTP:1~3;OLAP/少量执行:保持默认
defaultRowFetchSize0连接大结果集设为 1000~5000,配合 autoCommit=false
reWriteBatchedInsertsfalse连接若无 COPY,建议开启
connectTimeout10连接内网保持 10s,跨网适当增加
socketTimeout0连接必设 30~60s,防止僵死
tcpKeepAlivefalse连接建议开启,避免防火墙断连
preparedStatementCacheQueries256连接高并发语句多可适当增大
optionsnull连接传递临时运行时参数,如 -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轮询获取未处理的通知

延伸阅读