💠

💠 2024-10-08 11:23:38


JDBC

Java DataBase Connectivity

核心思想是定义一套接口规范,让各个数据库厂商实现这套接口,从而让应用方调用数据库的能力时可以不关心底层数据库

  • 设计是美好的,但是现实是丑陋的,或者说无法对多类型的数据库做完全的抽象一致,基础功能确实能一致化,其他功能还是要特殊化处理
    • 举例: 获取某个表的元信息表引擎,表索引,字段名,字段类型 MySQL和PostgreSQL实现完全不一致,pg实现成本很大,有些功能无法实现

码农翻身:JDBC的诞生

基础流程

  • 注册 driver
  • 建立 connection
  • 创建 statement
  • 执行获取 ResultSet
  • 解析返回的 ResultSet

Tips

Statement

主要分为 Statement 和 PrepareStatement, 在使用层面主要的区别为前者直接执行原始SQL,存在SQL注入风险, 后者是编译模板。

PrepareStatement

Oracle: Using Prepared Statements

依赖于具体数据库,常见的 MySQL PostgreSQL都有SQL编译功能

权衡

客户端参数调整

  • Druidpool-prepared-statements 连接池层面的缓存

ResultSet

仅为JDBC接口,具体行为细节来自实际数据库厂商提供的驱动

长连接流式导出数据

常见的分页导出的缺点有 分页越来越慢和不稳定排序导致页之间数据重复或丢失,用长连接流方式可以规避

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    // 阻塞模式 查数据和业务逻辑交替执行
    private void fetchBatchWithDataResource(DataSource ds, String sql, String where, int fetchSize,
                                            Consumer<List<LinkedHashMap<String, Object>>> handle) {
        Connection connection = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            connection = ds.getConnection();
            String query;
            if (StringUtils.isNotBlank(where)) {
                query = sql + " WHERE " + where;
            } else {
                query = sql;
            }

            log.info("stream export: query={}", query);

            stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
            stmt.setQueryTimeout(3600);
            stmt.setFetchSize(fetchSize);

            rs = stmt.executeQuery(query);
            int counter = 0;

            List<LinkedHashMap<String, Object>> data = new ArrayList<>();
            while (rs.next()) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();
                LinkedHashMap<String, Object> row = new LinkedHashMap<>();
                for (int i = 1; i <= columnCount; i++) {
                    row.put(meta.getColumnName(i), rs.getObject(i));
                }
                data.add(row);

                if (data.size() > fetchSize) {
                    handle.accept(data);
                    counter++;
                    data.clear();
                }
            }
            if (!data.isEmpty()) {
                handle.accept(data);
                counter++;
            }

            log.info("stream export: count={} dataSize={} ", counter, (counter - 1) * fetchSize + data.size());
        } catch (Exception e) {
            log.error("", e);
            throw new RuntimeException(e);
        } finally {
            close(connection, stmt, rs);
        }
    }
  • Statement 设置了 fetchSize 或者 TYPE_FORWARD_ONLY 模式后,都会采用游标的方式获取全部的数据

  • 参数 handle 是解析ResultSet 去生成 CSV Excel 等业务逻辑方法引用

  • 优化版本 生产者-消费者模式,降低阻塞时间,从而降低大量任务的整体耗时,但是CPU毛刺会增多且明显

    • 生产者:查询,消费者:业务逻辑,队列:QueueChannel
    • 样例代码
  • Clickhouse可以直接使用, 不需要额外的配置

  • PostgreSQL 调整:

    • executeQuery前 关闭 autoCommit,finally 开启,才会fetch指定的数据量,否则会拉取全部的数据到JVM。pg jdbc doc
  • MySQL 调整:

    • url配置需要添加 useCursorFetch=true 或者 关闭 autoCommit

SQLException

大部分数据库厂商都会由此派生出自定义的异常,CK除外,因此支持JDBC通用数据库的平台需要做特殊处理。


厂商驱动

MySQL

Clickhouse

clickhouse-java


Tips

mysql-connector-java 插入 utf8mb4 字符失败问题处理分析

批量插入性能优化

  • 关闭事务,或者手动管理事务 循环插入前开启,插入完一批再提交
  • 多条 insert values() 改为一条 insert values (),()