跳转至

批处理

本文你会学到

  • 批处理为什么能显著提升大批量写入性能
  • StatementPreparedStatement 两种批处理写法
  • 批处理与事务结合的最佳实践

为什么要用批处理?

每次执行单条 SQL 都会经历:Java → 网络 → 数据库解析 → 执行 → 网络 → Java 的完整往返。批处理将多条 SQL 打包一次性发送,显著减少网络往返次数,大幅提升大批量写入性能。

Statement 批处理

Statement.addBatch() + executeBatch() 批量执行
    /**
     * 演示 Statement 批处理:addBatch 添加多条 SQL,executeBatch 批量执行
     */
    @Test
    void testStatementBatch() throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // 使用 addBatch 添加多条 INSERT SQL
            stmt.addBatch("INSERT INTO logs (message, created_at) VALUES ('系统启动', CURRENT_TIMESTAMP)");
            stmt.addBatch("INSERT INTO logs (message, created_at) VALUES ('用户登录', CURRENT_TIMESTAMP)");
            stmt.addBatch("INSERT INTO logs (message, created_at) VALUES ('数据查询', CURRENT_TIMESTAMP)");
            stmt.addBatch("INSERT INTO logs (message, created_at) VALUES ('用户登出', CURRENT_TIMESTAMP)");

            // executeBatch 一次性执行所有批量 SQL,返回每条的影响行数
            int[] results = stmt.executeBatch();
            assertEquals(4, results.length, "批处理应包含 4 条 SQL");

            for (int i = 0; i < results.length; i++) {
                System.out.printf("批处理第 %d 条: 影响 %d 行%n", i + 1, results[i]);
                assertEquals(1, results[i], "每条 INSERT 应影响 1 行");
            }

            // 验证总记录数
            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM logs");
            rs.next();
            assertEquals(4, rs.getInt(1), "批处理后应有 4 条记录");
        }
    }

PreparedStatement 批处理

PreparedStatement 批处理:SQL 预编译一次,参数绑定多次
    /**
     * 演示 PreparedStatement 批处理:比 Statement 批处理更安全高效
     */
    @Test
    void testPreparedStatementBatch() throws SQLException {
        // PreparedStatement 批处理:SQL 预编译一次,参数绑定多次
        String sql = "INSERT INTO logs (message, created_at) VALUES (?, ?)";
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            // 批量插入 10 条记录
            for (int i = 1; i <= 10; i++) {
                pstmt.setString(1, "批量日志消息 #" + i);
                pstmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
                // 将当前参数组添加到批次中
                pstmt.addBatch();
            }

            // 一次性执行所有批量操作
            int[] results = pstmt.executeBatch();
            assertEquals(10, results.length, "批处理应包含 10 条操作");
            System.out.println("PreparedStatement 批处理执行了 " + results.length + " 条");

            // 验证总记录数
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM logs")) {
                rs.next();
                assertEquals(10, rs.getInt(1), "批处理后应有 10 条记录");
                System.out.println("表中总记录数: " + rs.getInt(1));
            }
            // 优势:SQL 只编译一次,参数绑定防止注入,性能优于 Statement 批处理
        }
    }

批处理 + 事务(最佳实践)

将批处理放在事务中执行,是大批量写入的最佳实践:既减少网络往返次数,又保证原子性;出错时可完整回滚。

批处理 + 事务结合,每批执行后统一提交
    /**
     * 演示批处理 + 事务:性能最佳实践
     * 在事务中执行批处理,批处理完成后统一提交
     */
    @Test
    void testBatchWithTransaction() throws SQLException {
        // 关闭自动提交,在事务中执行批处理(性能最佳实践)
        conn.setAutoCommit(false);

        try {
            String sql = "INSERT INTO logs (message, created_at) VALUES (?, ?)";
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                for (int i = 1; i <= 20; i++) {
                    pstmt.setString(1, "事务批量日志 #" + i);
                    pstmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
                    pstmt.addBatch();

                    // 每 10 条执行一次批处理(大数据量时控制内存)
                    if (i % 10 == 0) {
                        int[] results = pstmt.executeBatch();
                        System.out.println("执行了一批 " + results.length + " 条");
                    }
                }
            }

            // 所有批次执行完毕后统一提交事务
            conn.commit();
            System.out.println("事务已提交");

            // 验证总记录数
            conn.setAutoCommit(true);
            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM logs")) {
                rs.next();
                assertEquals(20, rs.getInt(1), "事务批处理后应有 20 条记录");
                System.out.println("事务批处理总记录数: " + rs.getInt(1));
            }
            // 优势:减少数据库提交次数,提升大批量插入性能
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        }
    }