Skip to content

Java 连接 PostgreSQL ​

Java 生态通过 JDBC 标准接口访问 PostgreSQL,官方驱动是 pgjdbc(Maven 坐标 org.postgresql:postgresql)。JDBC 的抽象让「连接、预处理、事务」有一套跨数据库的标准姿势,但 PostgreSQL 特有的能力——COPY 批量装载、LISTEN/NOTIFY、JSONB、数组类型——都需要落到 pgjdbc 的扩展 API(CopyManager、PGConnection、PGobject)才能用足。本页的主线就是「标准 JDBC 打底 + pgjdbc 扩展补全」,再以 HikariCP 与 Spring Boot 收尾到生产形态。

Java 服务通常是长驻进程,连接管理与 PHP/脚本语言有本质差异:应用自己持有连接池、连接生命周期横跨数小时,因此池参数、语句缓存、批量重写这类「连接级优化」在 Java 里的杠杆特别大——一个 reWriteBatchedInserts=true 就能把批量插入提速一个数量级,一条错误的池配置也能让数据库在流量高峰瞬间耗尽连接。

阅读本页需要 JDBC 基础(Connection/PreparedStatement/ResultSet 的基本用法);我们把笔墨集中在「PostgreSQL 视角」的差异点与工程实践上。

前置知识

建议先阅读:Python 连接 PostgreSQL

基础概念 ​

概念英文说明
JDBCJava Database ConnectivityJava 的数据库访问标准接口
pgjdbcPostgreSQL JDBC Driver官方驱动,实现 JDBC 4.x
扩展查询协议extended query protocol服务端预处理的底层协议,pgjdbc 默认使用
批量重写batch rewritereWriteBatchedInserts 把 N 条 INSERT 合并成多值 INSERT
CopyManager—pgjdbc 对 COPY 协议的 Java 封装
PGConnection—pgjdbc 的 PostgreSQL 扩展接口(通知、大对象等)
HikariCP—事实标准的 JDBC 连接池

连接链路的完整图景:

text
  应用线程 ──► HikariCP 连接池 ──► PgJDBC Connection ──► PostgreSQL 后端进程
               (池化复用)          (语句缓存/批量重写)      (每连接一进程)

两个决定性能的默认行为需要先知道:

  1. pgjdbc 默认走扩展查询协议(PREPARE/BIND/EXECUTE),参数在服务端绑定,注入安全由协议层保证;同名语句复用服务端执行计划(默认第 5 次起,由 prepareThreshold 控制);
  2. executeBatch() 默认是逐条发送——只是省了 API 样板,网络往返一点没省;配合 URL 参数 reWriteBatchedInserts=true 才会把同构 INSERT 重写成多值语句,这是 Java 批量插入的第一优化。

语法与用法 ​

Maven 依赖(版本以 Maven Central 最新 42.7.x 为准):

xml
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.4</version>
</dependency>

JDBC URL 与常用参数:

text
jdbc:postgresql://db1.prod.internal:5432/appdb
    ?user=app_rw&password=secret
    &sslmode=require
    &connectTimeout=5&socketTimeout=60
    &ApplicationName=svc-orders
    &reWriteBatchedInserts=true
参数说明
sslmodedisable/prefer/require/verify-ca/verify-full(同 libpq 语义)
connectTimeout建连超时,单位秒
socketTimeout读超时秒数(0 为不限),防止查询永久挂死
ApplicationName应用标识,pg_stat_activity 里可溯源
reWriteBatchedInsertstrue 时批量 INSERT 重写为多值语句
prepareThreshold同一语句执行多少次后改用服务端命名预处理(默认 5)
currentSchema等价 search_path,如 currentSchema=tenant_a,public

标准 JDBC 用法(现代驱动经 SPI 自动注册,无需 Class.forName):

java
try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(
         "SELECT id, name FROM users WHERE status = ? AND age > ?")) {
    ps.setString(1, "active");
    ps.setInt(2, 18);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            long id = rs.getLong("id");
            String name = rs.getString("name");
        }
    }
}

事务与批量:

java
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO items (id, name) VALUES (?, ?)")) {
    for (Item it : items) {
        ps.setLong(1, it.id());
        ps.setString(2, it.name());
        ps.addBatch();
    }
    int[] counts = ps.executeBatch();   // 配合 reWriteBatchedInserts 提速
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
    throw e;
} finally {
    conn.setAutoCommit(true);
}

COPY 导入(pgjdbc 扩展):

java
CopyManager cm = conn.unwrap(PgConnection.class).getCopyAPI();
try (InputStream in = new FileInputStream("orders.csv")) {
    long rows = cm.copyIn(
        "COPY stg_orders FROM STDIN WITH (FORMAT csv, HEADER true)", in);
}

详细说明 ​

依赖、驱动版本与兼容性 ​

pgjdbc 版本号与 JDBC 规范、JDK 版本的对应关系需要留意:42.x 系列支持 JDK 8+ 与 JDBC 4.2,能连接各代 PostgreSQL 服务器(驱动向下兼容多年前的服务器版本)。实践要点:

  • 升级驱动是低风险高收益的运维动作:42.x 系列修过连接泄露、批量重写边界、SSL 握手等多类问题,保持跟随最新补丁版;
  • 驱动版本与服务器大版本解耦:不像 libpq 那样强调同源编译,Maven 依赖即用;
  • 不要同时引入多个驱动实现(如残留的旧 net/postgres 驱动),DriverManager 的注册顺序会带来诡异行为。

Gradle 用户对应 implementation 'org.postgresql:postgresql:42.7.4';Jakarta EE 容器部署时把 jar 放入应用即可,现代容器普遍支持 SPI 自动发现。

JDBC URL 与连接参数全集 ​

pgjdbc 的 URL 参数分两族:libpq 同名参数(sslmode、connectTimeout 等,语义与 libpq 一致)与 pgjdbc 专属参数(reWriteBatchedInserts、prepareThreshold、ApplicationName 等)。几个高频组合:

text
# 生产 Web 服务标配
jdbc:postgresql://db1:5432/appdb?sslmode=verify-full&connectTimeout=3&socketTimeout=60&ApplicationName=svc-orders

# 批量任务
jdbc:postgresql://db1:5432/warehouse?reWriteBatchedInserts=true&ApplicationName=etl-daily

# 多 schema 应用
jdbc:postgresql://db1:5432/appdb?currentSchema=tenant_a,public

socketTimeout 值得单独强调:默认 0(无限等待),一条卡死的查询会把调用线程永久占住,最终拖垮线程池。按业务 P99 的数倍设置一个上限,配合服务端 statement_timeout 双保险。verify-full 需要提供 CA 证书(sslrootcert 参数或 JVM 信任库),公网与跨机房链路应作为默认。

DriverManager 与 DataSource ​

DriverManager 适合脚本与原型;生产代码一律走 DataSource——它是连接池、JNDI、Spring 等基础设施的标准接入点。pgjdbc 自带两个轻量 DataSource 实现:PGSimpleDataSource(不池化,每次 getConnection 新建连接,适合需要显式控制生命周期的场景)与 PGConnectionPoolDataSource(应用内简单池)。真实项目里这两者通常被 HikariCP 替代(后述),但理解 DataSource 接口本身很重要:

java
PGSimpleDataSource ds = new PGSimpleDataSource();
ds.setURL("jdbc:postgresql://db1:5432/appdb");
ds.setUser("app_rw");
ds.setPassword("secret");
ds.setSslMode("require");       // 参数也有 setter,IDE 可发现性好
ds.setApplicationName("svc-orders");

try (Connection conn = ds.getConnection()) { /* … */ }

PreparedStatement 与类型映射 ​

PreparedStatement 是 Java 侧防注入与提性能的基石:语句模板一次解析、参数另行绑定。PostgreSQL 类型映射的要点:

PostgreSQL 类型JDBC 获取 / 设置说明
int8/bigintgetLong / setLong64 位整数
numericgetBigDecimal / setBigDecimal金额必用,杜绝 double
timestamptzgetOffsetDateTime / setOffsetDateTime带时区时间
boolgetBoolean / setBoolean
uuidgetObject(..., UUID.class) / setObject(i, uuid)原生支持
jsonbgetString 读 / PGobject 或 ?::jsonb 写见下
text[]getArray / createArrayOf("text", …)
byteagetBytes / setBytes二进制列

JSONB 写入的两种姿势:

java
// 方式一:PGobject 显式声明类型
PGobject json = new PGobject();
json.setType("jsonb");
json.setValue("{\"kind\":\"click\",\"n\":3}");
ps.setObject(1, json);

// 方式二:SQL 侧强转 + setString
ps = conn.prepareStatement("INSERT INTO events (payload) VALUES (?::jsonb)");
ps.setString(1, "{\"kind\":\"click\"}");

方式二更简洁且不依赖驱动扩展类,但要注意传入内容必须是合法 JSON;两种方式都让 JSON 解析留在数据库层,客户端不做任何字符串拼接。

事务、隔离级别与 SAVEPOINT ​

java
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
try {
    // …业务 SQL…
    Savepoint sp = conn.setSavepoint("before_risky");
    try {
        // 风险段
    } catch (SQLException e) {
        conn.rollback(sp);          // 局部回滚
    }
    conn.commit();
} catch (SQLException e) {
    conn.rollback();                // 整体回滚
}

Java 长驻进程里事务的三个纪律:事务边界放服务层(DAO 不开事务,只参与外层事务);事务内不调用慢 IO(HTTP、MQ、文件),连接被长时间占用是池耗尽的头号原因;异常路径必然 rollback(try-with-resources + finally 或框架的 @Transactional)。

批量执行与 reWriteBatchedInserts ​

addBatch()/executeBatch() 的默认行为是把 N 条语句逐条发给服务器,省的是代码与解析,费的是网络。reWriteBatchedInserts=true 让驱动把同构的 INSERT INTO t VALUES (?,?) 重写成一条多值语句:

text
INSERT INTO t VALUES (?,?)   × 1000 条批量
  ↓ reWriteBatchedInserts=true(每 ~128 行重写一条)
INSERT INTO t VALUES (?,?),(?,?),(?,?)…   × ~8 条

网络往返从 1000 次降到个位数,插入吞吐通常提升 2~5 倍,配合 COPY 之前这是 Java 批量写入的第一步。使用注意:

  • 只对同构 INSERT(同一表、同一列集)生效,异构批退化为逐条;
  • 重写有单语句长度上限的考量,驱动内部按批次拆分;
  • executeBatch() 的返回值在重写模式下是「每批合计」语义,按行精确统计需要用 RETURNING 或另行核对。

批量更大(百万行级)时,请直接进入下节的 CopyManager。

CopyManager:Java 侧最快的写入通道 ​

CopyManager 封装 COPY 协议,输入/输出可以是流、Reader/Writer 或字节数组。获取它需要拿到 pgjdbc 的原生连接:

java
import org.postgresql.copy.CopyManager;
import org.postgresql.jdbc.PgConnection;

// 池化连接(HikariCP 包装)也能 unwrap
CopyManager cm = conn.unwrap(PgConnection.class).getCopyAPI();

// 导入:InputStream 逐块流入,内存平稳
try (InputStream in = Files.newInputStream(Path.of("events.csv"))) {
    long copied = cm.copyIn(
        "COPY stg_events (event_id, user_id, kind, event_time) "
        + "FROM STDIN WITH (FORMAT csv, HEADER true)", in);
    System.out.println("copied " + copied + " rows");
}

// 导出:流式输出
try (OutputStream out = Files.newOutputStream(Path.of("dump.csv"))) {
    cm.copyOut("COPY orders TO STDOUT WITH (FORMAT csv, HEADER true)", out);
}

copyIn(String, InputStream) 返回拷贝行数,失败抛 SQLException 且事务中止(整个 COPY 是一个事务)。与 \copy 一页的性能清单完全相通:摘索引、关外键、synchronous_commit=off、PG17 的 ON_ERROR ignore 选项都可以写进 copyIn 的 SQL 字符串。

LISTEN/NOTIFY:Java 侧的发布订阅 ​

PostgreSQL 的进程内消息通道在 pgjdbc 里是 LISTEN + getNotifications() 轮询组合:

java
import org.postgresql.PGConnection;
import org.postgresql.PGNotification;

try (Connection conn = ds.getConnection()) {
    PGConnection pg = conn.unwrap(PGConnection.class);
    try (Statement st = conn.createStatement()) {
        st.execute("LISTEN order_events");
    }
    while (!closed) {
        PGNotification[] ns = pg.getNotifications();
        if (ns != null) {
            for (PGNotification n : ns) {
                handle(n.getName(), n.getParameter());
            }
        }
        Thread.sleep(500);        // 轮询间隔
    }
}

工程要点:必须独占连接(池借出后专用于监听);通知只在事务提交后可见(NOTIFY 所在事务提交时才投递);轮询间隔决定延迟下限,实时性要求高时配合 socketTimeout 阻塞读或改用专业消息队列。适用边界是「轻量、进程内、小负载」的协作信号,不适合做业务消息总线。

ServerErrorMessage:错误上下文的细粒度取用 ​

SQLException 之外,pgjdbc 把服务端错误的完整结构暴露在 ServerErrorMessage:

java
catch (SQLException e) {
    org.postgresql.util.ServerErrorMessage sem = e.getServerErrorMessage();
    if (sem != null) {
        log.warn("sqlstate={} constraint={} table={} detail={}",
                 sem.getSQLState(),
                 sem.getConstraint(),   // 约束名,如 users_email_key
                 sem.getTable(),
                 sem.getDetail());      // 如 Key (email)=(a@b.com) already exists.
    }
    throw e;
}

getConstraint() 的约束名可以做成「约束名 → 业务错误码」的映射表——同一张表有多个唯一约束时能区分「邮箱冲突」还是「手机号冲突」,比只看 SQLSTATE 精确;getDetail() 常包含冲突行的键值,写日志时注意脱敏。与 SQLSTATE 分支模板组合:先用 getSQLState() 判类别,再用约束名判具体业务语义。

HikariCP:生产连接池的标准配置 ​

java
HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl("jdbc:postgresql://db1:5432/appdb?sslmode=require&reWriteBatchedInserts=true");
cfg.setUsername("app_rw");
cfg.setPassword(System.getenv("DB_PASS"));
cfg.setMaximumPoolSize(20);
cfg.setMinimumIdle(5);
cfg.setConnectionTimeout(3_000);        // 借连接最多等 3s
cfg.setMaxLifetime(1_800_000);          // 连接最长存活 30min,短于服务端 idle 超时
cfg.setKeepaliveTime(300_000);          // 5min 保活探测
cfg.addDataSourceProperty("ApplicationName", "svc-orders");
cfg.addDataSourceProperty("prepareThreshold", "5");

HikariDataSource ds = new HikariDataSource(cfg);

池大小不是「越大越好」:PostgreSQL 每连接一个进程,池过大反而加剧上下文切换与锁竞争。经验起点是 CPU 核数的 2~4 倍,再按压测调整;池参数与 PostgreSQL 侧配置的关系详见《连接池最佳实践》与《PgBouncer》。当应用实例多、连接数不可控时,Hikari 之外再叠加 PgBouncer 做集中收敛。

大对象(Large Object)的读写 ​

大对象 API 在 pgjdbc 的 LargeObjectManager,必须配合事务:

java
import org.postgresql.largeobject.LargeObject;
import org.postgresql.largeobject.LargeObjectManager;

conn.setAutoCommit(false);
LargeObjectManager lom = conn.unwrap(PGConnection.class)
                              .getLargeObjectAPI();

// 上传:创建 OID,流式写入
long oid = lom.createLO();
try (LargeObject lo = lom.open(oid, LargeObjectManager.WRITE);
     InputStream in = Files.newInputStream(Path.of("report.pdf"))) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = in.read(buf)) > 0) {
        lo.write(buf, 0, n);
    }
}
try (PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO documents (name, lo_oid) VALUES (?, ?)")) {
    ps.setString(1, "report.pdf");
    ps.setLong(2, oid);
    ps.executeUpdate();
}
conn.commit();

// 下载:按 OID 流式读取
try (PreparedStatement ps = conn.prepareStatement(
        "SELECT lo_oid FROM documents WHERE id = ?")) {
    ps.setLong(1, docId);
    try (ResultSet rs = ps.executeQuery()) {
        rs.next();
        try (LargeObject lo = lom.open(rs.getLong(1), LargeObjectManager.READ);
             OutputStream out = Files.newOutputStream(Path.of("copy.pdf"))) {
            byte[] buf = new byte[8192];
            int n;
            while ((n = lo.read(buf)) > 0) {
                out.write(buf, 0, n);
            }
        }
    }
}
conn.commit();

工程要点:读写都必须在事务内(无事务直接报错);LargeObject 支持 seek() 随机访问,是 bytea(整块读写)做不到的;删除行不会自动清理大对象,需要 lo_unlink 或 lo_manage 触发器配合——大对象与 bytea 的完整取舍见大对象总览。

Spring Boot 集成 ​

Spring Boot 自动装配 HikariCP + pgjdbc,配置集中在 application.yml:

yaml
spring:
  datasource:
    url: jdbc:postgresql://db1:5432/appdb?sslmode=require&reWriteBatchedInserts=true&ApplicationName=svc-orders
    username: app_rw
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 3000
      max-lifetime: 1800000
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 100          # JPA 批量(配合 reWriteBatchedInserts)

JPA/Hibernate 用户的补充:batch_size 让 Hibernate 走 JDBC 批量,与 URL 的 reWriteBatchedInserts 是互补关系(前者攒批、后者重写);IDENTITY 主键策略会禁用批量插入,用 SEQUENCE 配合 allocationSize 才能吃满批量通道。数据校验、Flyway 迁移、@Transactional 的传播语义都是常规 Spring 内容,此处不展开。

实战示例 ​

示例一:标准业务事务模板 ​

java
public void transfer(Connection conn, long from, long to, BigDecimal amount)
        throws SQLException {
    boolean oldAuto = conn.getAutoCommit();
    conn.setAutoCommit(false);
    try {
        try (PreparedStatement ps = conn.prepareStatement(
                "UPDATE accounts SET balance = balance - ? "
                + "WHERE id = ? AND balance >= ?")) {
            ps.setBigDecimal(1, amount);
            ps.setLong(2, from);
            ps.setBigDecimal(3, amount);
            if (ps.executeUpdate() != 1) {
                throw new IllegalStateException("insufficient balance");
            }
        }
        try (PreparedStatement ps = conn.prepareStatement(
                "UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
            ps.setBigDecimal(1, amount);
            ps.setLong(2, to);
            ps.executeUpdate();
        }
        conn.commit();
    } catch (SQLException | IllegalStateException e) {
        conn.rollback();
        throw e;
    } finally {
        conn.setAutoCommit(oldAuto);
    }
}

示例二:批量插入的两级优化 ​

java
public void bulkInsert(Connection conn, List<Item> items) throws SQLException {
    String sql = "INSERT INTO items (id, name, price) VALUES (?, ?, ?)";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        int i = 0;
        for (Item it : items) {
            ps.setLong(1, it.id());
            ps.setString(2, it.name());
            ps.setBigDecimal(3, it.price());
            ps.addBatch();
            if (++i % 5_000 == 0) {
                ps.executeBatch();          // 分批提交,控制单事务体积
            }
        }
        ps.executeBatch();
    }
    // 前提:URL 含 reWriteBatchedInserts=true
}

对照测试(100 万行、同机):不开重写约 60s,开启后约 15s,改用 CopyManager 约 4s——三级通道的量级差距一目了然。

示例三:CopyManager 导入 CSV 并统计 ​

java
public long importCsv(Connection conn, String table, Path csv) throws Exception {
    CopyManager cm = conn.unwrap(PgConnection.class).getCopyAPI();
    String sql = "COPY " + table + " FROM STDIN WITH (FORMAT csv, HEADER true)";
    try (InputStream in = new BufferedInputStream(Files.newInputStream(csv), 1 << 16)) {
        return cm.copyIn(sql, in);
    } catch (SQLException e) {
        // COPY 失败 → 当前事务已被中止,调用方决定 rollback 与重试
        throw e;
    }
}

示例四:HikariCP 工厂类(非 Spring 环境) ​

java
public final class PoolFactory {
    public static HikariDataSource build() {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:postgresql://db1:5432/appdb"
                + "?sslmode=require&connectTimeout=3&socketTimeout=60"
                + "&reWriteBatchedInserts=true&ApplicationName=svc-orders");
        cfg.setUsername("app_rw");
        cfg.setPassword(System.getenv("DB_PASSWORD"));
        cfg.setMaximumPoolSize(20);
        cfg.setConnectionTimeout(3_000);
        cfg.setMaxLifetime(1_800_000);
        cfg.setKeepaliveTime(300_000);
        return new HikariDataSource(cfg);
    }
}

示例五:LISTEN/NOTIFY 的轻量事件消费 ​

java
public void listenOrders(DataSource ds) {
    Thread t = new Thread(() -> {
        try (Connection conn = ds.getConnection()) {
            PGConnection pg = conn.unwrap(PGConnection.class);
            conn.createStatement().execute("LISTEN order_events");
            while (!Thread.currentThread().isInterrupted()) {
                PGNotification[] ns = pg.getNotifications();
                if (ns != null) {
                    for (PGNotification n : ns) {
                        System.out.println("notify: " + n.getParameter());
                    }
                }
                Thread.sleep(500);
            }
        } catch (Exception e) {
            Thread.currentThread().interrupt();
        }
    }, "pg-notify-listener");
    t.setDaemon(true);
    t.start();
}

示例六:大结果集的流式读取(setFetchSize) ​

JDBC 默认把整个结果集拉进内存;设置 fetch size 后驱动按块取回(需要事务内 + 关闭 autoCommit):

java
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(
        "SELECT id, payload FROM big_events WHERE created_at >= ?")) {
    ps.setTimestamp(1, Timestamp.from(Instant.parse("2026-10-01T00:00:00Z")));
    ps.setFetchSize(10_000);            // 每 1 万行一批
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            process(rs.getLong(1), rs.getString(2));   // 逐行处理,内存平稳
        }
    }
} finally {
    conn.commit();
    conn.setAutoCommit(true);
}

要点:autoCommit=false 是前提——自动提交模式下 pgjdbc 无法使用服务端游标(游标必须在事务内),fetch size 会被忽略,行为退化为全量加载。对照《PHP 连接 PostgreSQL》的可滚动游标与《Python 连接 PostgreSQL》的 itersize,这是同一机制在三种语言里的同一要求。

注意事项 ​

executeBatch 默认不省网络往返。 没有 reWriteBatchedInserts=true 的批量只是 API 层攒批,性能与逐条执行接近;这是 Java 批量写入最常见的「以为在优化」误区。

事务内做慢 IO 是池耗尽的头号原因。 @Transactional 方法里调 HTTP/发 MQ,连接占用时间被外部延迟放大,高峰期 connection is not available 大都源于此。

socketTimeout 与 statement_timeout 是两道独立的闸。 客户端 socketTimeout 防线程挂死,服务端 statement_timeout 防语句失控;只配一个都存在绕过路径。

PostgreSQL 连接是进程级资源。 Hikari 池大小 × 应用实例数要对着 max_connections 规划;多实例扩容前先算连接总账,必要时上 PgBouncer。

numeric 金额用 BigDecimal。 double 的精度损失在金额、计量场景必然出事;getBigDecimal/setBigDecimal 是唯一正确通道。

CopyManager 失败会中止事务。 COPY 是单事务原子操作,失败后当前事务必须 rollback;超大导入按行数切片分批,避免「一行坏数据全量重来」。

LISTEN/NOTIFY 不是消息队列。 无持久化、无重放、连接断开即丢(期间通知不缓存);它适合协作信号与缓存失效,业务事件流请用专业中间件。

驱动升级要跟。 42.x 系列的补丁包含安全与稳定性修复;固化旧版本会让已知问题(连接泄露、SSL 握手边界)长期潜伏。

unwrap 是池化连接访问扩展 API 的正道。 对 Hikari 包装的连接直接强转会抛 ClassCastException,conn.unwrap(PgConnection.class) 才是兼容池的实现。

大结果集的 fetch size 需要 autoCommit=false。 自动提交模式下驱动无法开启服务端游标,setFetchSize 被忽略、结果全量加载;「关自动提交 + 设 fetch size + 逐行读」三个动作必须成对出现。

时间类型的时区语义要显式声明。 读 timestamptz 优先用 OffsetDateTime(JDBC 4.2+),遗留代码的 getTimestamp 依赖会话/容器时区——容器时区漂移引发的隐性错位在生产上屡见不鲜。

批量 + RETURNING 的返回语义要在评审里说清。 批量通道返回批级计数,需要行级精确回流时改用「单语句 + RETURNING」的循环;避免测试环境没撞到、生产数据对不上。

最佳实践 ​

  1. 生产连接一律走池 + DataSource,URL 参数显式声明 sslmode/connectTimeout/socketTimeout/ApplicationName。 四件套分别对应加密、快速失败、挂死防护、连接溯源。
  2. 批量插入三级递进:addBatch + reWriteBatchedInserts → 分批事务 → CopyManager。 十万行是 COPY 的分水岭,万行以下重写批就够。
  3. 金额 numeric + BigDecimal,时间 timestamptz + OffsetDateTime。 让类型系统承担精度与时区责任。
  4. 事务边界放服务层,事务内禁慢 IO。 DAO 只参与事务;@Transactional 的粒度评审纳入 code review 清单。
  5. JSONB 用 PGobject 或 ?::jsonb,解析留在数据库。 客户端只传字符串/结构体,不做 SQL 拼接。
  6. 动态标识符(表名/列名/排序)走白名单校验。 PreparedStatement 只保护值,不保护语句结构。
  7. Hikari 池大小从 CPU 核数 2~4 倍起步,按压测收敛。 并配合 maxLifetime 短于服务端 idle 超时,避免拿到「已被服务器断开」的连接。
  8. 大导入先摘索引外键,导完重建并 ANALYZE。 与 \copy 一页的性能清单同源,maintenance_work_mem 调优留给重建阶段。
  9. 错误处理按 SQLSTATE 分支。 e.getSQLState() 的 23505/23503/40001/40P01 分别对应唯一冲突、外键冲突、序列化失败、死锁,重试逻辑只对可重试码生效。
  10. Spring Boot 批量链路三件套齐备:batch_size + reWriteBatchedInserts + SEQUENCE 主键。 缺一环 Hibernate 批量就退化成逐条。
  11. 多实例部署先做连接总账再扩容。 应用池 × 实例数 ≤ max_connections 的安全余量,超出部分用 PgBouncer 收敛。
  12. 把 ApplicationName 规范化到每个服务。 pg_stat_activity 里连接可溯源,是排查连接泄漏与慢查询归属的第一线索。

下一节

继续学习:Go 连接 PostgreSQL

参考链接 ​