两表 Hash 比较 — JDBC 真流式改造说明

1. 背景与问题

原实现中 HashRowCursor 已使用 setFetchSize(compareFetchSize),并在应用层通过 Iterator + StreamingHashComparer.merge() 做双指针归并,业务上不会把全表 hash 装入 List

但在 PostgreSQL / openGauss JDBC 默认 autoCommit=true 时:

  • fetchSize 通常无法启用服务端游标(portal);
  • 驱动可能在客户端一次性或大块缓冲整个 ResultSet
  • 表现为:executeQuery() 或 merge 前期耗时很长、堆内存随时间窗行数上涨。

这属于 「应用层真流式、JDBC 层假流式」


2. 改造目标

目标 说明
启用 JDBC 服务端游标 autoCommit=false + fetchSize > 0
保持只读语义 setReadOnly(true),比较结束 rollback
不破坏后续差异查询 归并后先关闭 hash 游标,再在同一连接上执行步骤 6、7
改动范围小 仅在 TableHashCompareExecutor(做法 A),不改 ExternalJdbcExecutor 全局行为

3. 设计思路

3.1 为何在 Executor 配置连接,而非 ExternalJdbcExecutor

  • ExternalJdbcExecutor 可能被其他场景复用,全局 setAutoCommit(false) 会影响调用方事务语义。
  • hash 比较的流式需求仅存在于打开 HashRowCursor 到归并结束这一段;在编排器内显式配置,职责清晰。

3.2 为何 setReadOnly(true)

  • 比较任务只做 SELECT,只读事务可提示数据库优化,且避免误写。
  • 结束时用 rollback() 而非 commit():无持久化变更,释放事务状态即可。

3.3 为何 merge 后必须立刻关闭游标

PostgreSQL 协议下,同一连接autoCommit=false + 服务端游标时,通常只允许一个未关闭的 ResultSet

原流程在游标仍打开时,于同一 pgConnection / ogConnection 上执行 fetchRowsByIdsfetchNormalizedRows,可能触发:

  • SQLException: Another ResultSet is already open
  • 或未定义行为

因此:步骤 5 归并结束 → 关闭双游标 → 步骤 6、7 差异明细查询

finally 中仍保留 closeQuietly(pgCursor/ogCursor),用于异常提前退出时的兜底。

3.4 资源关闭顺序

关闭 HashRowCursor(ResultSet + PreparedStatement)
    → rollback(若 autoCommit=false)
    → close Connection

与 JDBC 规范及 PG 驱动建议一致。

3.5 与本改造无关的优化

以下问题不能仅靠 JDBC 真流式解决,需另做(索引、时间分片、SQL 减负等):

  • 时间窗内行数极大时的 ORDER BY id Sort(库端 work_mem、临时文件);
  • (timeColumn, id) 索引时的全表/大范围扫描;
  • 差异 id 极多时 CompareResult 三个 List 及步骤 6、7 的内存。

4. 执行流程(改造后)

openConnection (PG / OG)
    → configureStreamingConnection(autoCommit=false, readOnly=true)
    → loadColumns(元数据,短查询)
    → HashRowCursor.open × 2(fetchSize 不变)
    → StreamingHashComparer.merge
    → close 两个 HashRowCursor          ← 新增:释放 ResultSet,供后续查询
    → fetchRowsInBatches / fetchNormalizedInBatches(差异明细)
    → logSummary
finally:
    → close cursors(若尚未关闭)
    → endReadOnlyTransaction(rollback)
    → close connections

5. 代码变更摘要

涉及文件src/main/java/com/liang/learn/service/tablehash/TableHashCompareExecutor.java

未改文件(行为配合,无需修改):

  • HashRowCursor.java — 继续使用 setFetchSize(fetchSize)
  • TableHashProperties.compareFetchSize — 默认 1000
  • ExternalJdbcExecutor.openConnection — 仍为 DriverManager 默认连接

5.1 连接打开后配置流式

pgConnection = jdbcExecutor.openConnection(pgDataSource);
ogConnection = jdbcExecutor.openConnection(ogDataSource);
configureStreamingConnection(pgConnection);
configureStreamingConnection(ogConnection);
private static void configureStreamingConnection(Connection connection) {
    try {
        connection.setAutoCommit(false);
        connection.setReadOnly(true);
    } catch (SQLException e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "配置流式连接失败: " + e.getMessage());
    }
}

5.2 归并后关闭游标

CompareResult result = streamingHashComparer.merge(pgCursor, ogCursor, idUdtName);

// 真流式下同一连接仅能有一个 ResultSet,归并后须先关闭游标再查差异明细
closeQuietly(pgCursor);
closeQuietly(ogCursor);
pgCursor = null;
ogCursor = null;

// 6. 对 PG独有 / OG独有 id 批量查全行并输出日志
// ...

5.3 finally 中结束只读事务

} finally {
    closeQuietly(pgCursor);
    closeQuietly(ogCursor);
    endReadOnlyTransaction(pgConnection);
    endReadOnlyTransaction(ogConnection);
    closeQuietly(pgConnection);
    closeQuietly(ogConnection);
}
private static void endReadOnlyTransaction(Connection connection) {
    if (connection == null) {
        return;
    }
    try {
        if (!connection.isClosed() && !connection.getAutoCommit()) {
            connection.rollback();
        }
    } catch (SQLException ignored) {
        // ignore rollback errors during cleanup
    }
}

5.4 新增 import

import java.sql.SQLException;

6. 配置与前置条件

要求
app.table-hash.compare-fetch-size 必须 > 0(默认 1000)
PG / OG 各一条连接 每连接同时仅一个 hash ResultSet,当前架构满足
驱动 PostgreSQL JDBC、openGauss JDBC(兼容 PG 游标语义)

可选(不能替代 setAutoCommit(false)):

jdbc:postgresql://...?defaultRowFetchSize=1000

7. 改造前后对比

维度 改造前 改造后
JDBC 结果集 驱动常全量/大块缓冲 服务端按 fetchSize 分批 FETCH
应用 merge 逐行 next(),一致行不进 List 不变
差异查询时机 游标可能仍打开 游标已关闭,可安全复用连接
事务 隐式 autocommit 只读事务 + rollback 清理

8. 验证建议

  1. 堆内存:大表、0 差异场景,对比改造前后 merge 阶段堆峰值。
  2. 阶段耗时HashRowCursor.open 返回时刻 vs merge 结束时刻;真流式下 open 不应占绝大部分墙钟时间。
  3. 功能回归:有差异表跑通步骤 6、7 日志;确认无「ResultSet already open」类错误。

9. 后续可选工作(本次未做)

  • 时间分片:解决库端 Sort / 超时;无时间索引的普通堆表上分片通常不能降总 I/O。
  • 索引(timeColumn, id),使分片与单片查询都能 Index Scan。
posted @ 2026-05-26 19:44  景之1231  阅读(2)  评论(0)    收藏  举报