一、为什么需要批量插入优化

在日常开发中,我们经常会遇到需要向数据库一次性插入大量数据的场景。比如数据迁移、日志记录、批量导入等。如果采用单条插入的方式,性能会非常糟糕。举个例子,插入1万条数据,如果每条需要50ms,总共就需要500秒,这显然是不可接受的。

MySQL的批量插入功能可以显著提升性能,但如果不注意使用方法,反而可能适得其反。我曾经遇到过一个案例,同事在批量插入时没有控制好事务大小,导致产生了长达30分钟的锁等待,直接影响了线上业务。

二、基础批量插入方法

最基础的批量插入语法非常简单,下面是一个标准的Java JDBC示例:

// Java JDBC批量插入示例
String sql = "INSERT INTO users (name, age, email) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(sql)) {
    
    // 批量添加10000条数据
    for (int i = 0; i < 10000; i++) {
        pstmt.setString(1, "user_" + i);
        pstmt.setInt(2, i % 100);
        pstmt.setString(3, "user_" + i + "@example.com");
        pstmt.addBatch();  // 添加到批处理
        
        // 每1000条执行一次
        if (i % 1000 == 0) {
            pstmt.executeBatch();
        }
    }
    // 执行剩余批次
    pstmt.executeBatch();
}

这个例子展示了最基本的批量插入方法,通过addBatch()累积SQL语句,然后定期执行。但这种方式其实还有很大的优化空间。

三、高级优化技巧

1. 使用多值INSERT语法

MySQL支持在一个INSERT语句中插入多行数据,这种方式比批处理更高效:

// Java JDBC多值INSERT示例
StringBuilder sql = new StringBuilder(
    "INSERT INTO users (name, age, email) VALUES ");
    
List<Object[]> params = new ArrayList<>();
// 准备10000条数据
for (int i = 0; i < 10000; i++) {
    sql.append(i == 0 ? "(?,?,?)" : ",(?,?,?)");
    params.add(new Object[]{"user_" + i, i % 100, "user_" + i + "@example.com"});
}

try (Connection conn = dataSource.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(sql.toString())) {
    
    // 设置参数
    int paramIndex = 1;
    for (Object[] param : params) {
        pstmt.setString(paramIndex++, (String) param[0]);
        pstmt.setInt(paramIndex++, (Integer) param[1]);
        pstmt.setString(paramIndex++, (String) param[2]);
    }
    
    pstmt.executeUpdate();
}

这种方式减少了网络往返次数,但要注意SQL语句长度不能超过max_allowed_packet限制。

2. 事务控制优化

批量插入时合理控制事务大小很关键:

// Java JDBC事务优化示例
int batchSize = 1000;
try (Connection conn = dataSource.getConnection()) {
    conn.setAutoCommit(false);  // 关闭自动提交
    
    String sql = "INSERT INTO users (name, age, email) VALUES (?, ?, ?)";
    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
        for (int i = 0; i < 10000; i++) {
            pstmt.setString(1, "user_" + i);
            pstmt.setInt(2, i % 100);
            pstmt.setString(3, "user_" + i + "@example.com");
            pstmt.addBatch();
            
            // 每1000条提交一次
            if (i > 0 && i % batchSize == 0) {
                pstmt.executeBatch();
                conn.commit();  // 提交事务
            }
        }
        // 提交剩余数据
        pstmt.executeBatch();
        conn.commit();
    }
}

3. 使用LOAD DATA INFILE

对于超大数据量,最快速的方法是使用MySQL的LOAD DATA INFILE:

// Java使用LOAD DATA INFILE示例
// 首先将数据写入临时文件
Path tempFile = Files.createTempFile("users", ".csv");
try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) {
    for (int i = 0; i < 10000; i++) {
        writer.write(String.format("user_%d,%d,user_%d@example.com\n", 
            i, i % 100, i));
    }
}

// 执行LOAD DATA INFILE
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement()) {
    
    String sql = String.format(
        "LOAD DATA LOCAL INFILE '%s' INTO TABLE users " +
        "FIELDS TERMINATED BY ',' LINES TERMINATED BY '\\n' " +
        "(name, age, email)", tempFile.toString());
    
    stmt.execute(sql);
}

这种方式比常规INSERT快20-100倍,但需要文件系统访问权限。

四、性能对比与选择策略

为了帮助大家选择合适的方法,我做了个简单的性能测试(插入10万条数据):

  1. 单条INSERT:约300秒
  2. JDBC批处理:约45秒
  3. 多值INSERT:约15秒
  4. LOAD DATA INFILE:约3秒

选择策略:

  • 小批量数据(<1000行):使用多值INSERT
  • 中等批量数据(1000-10万行):使用批处理+事务控制
  • 超大数据量(>10万行):优先考虑LOAD DATA INFILE

五、常见问题与解决方案

1. 主键冲突问题

批量插入时如果遇到主键冲突,整个操作会失败。可以使用INSERT IGNORE或ON DUPLICATE KEY UPDATE:

// 使用ON DUPLICATE KEY UPDATE处理冲突
String sql = "INSERT INTO users (id, name, age, email) VALUES (?,?,?,?) " +
             "ON DUPLICATE KEY UPDATE name=VALUES(name), age=VALUES(age)";

2. 超时问题

大批量操作可能导致超时,需要调整参数:

// 设置超时参数
try (Connection conn = dataSource.getConnection()) {
    conn.setNetworkTimeout(executor, 600000);  // 10分钟超时
    // 其他操作...
}

3. 内存问题

批量数据占用过多内存时,可以采用分页处理:

// 分页处理大数据量
int total = 1000000;
int pageSize = 10000;
for (int page = 0; page < total / pageSize; page++) {
    // 处理每页数据...
}

六、总结与最佳实践

经过多年的实践,我总结了以下最佳实践:

  1. 始终使用PreparedStatement防止SQL注入
  2. 合理设置批处理大小,通常1000-5000为宜
  3. 控制事务大小,避免长事务
  4. 考虑使用连接池配置合适的连接数
  5. 对于超大数据量,优先考虑文件导入方式
  6. 监控和调整MySQL参数,如max_allowed_packet
  7. 考虑在非高峰期执行大批量操作

记住,没有放之四海而皆准的方案,要根据具体场景选择合适的方法。测试是关键,在实施前务必在测试环境验证性能。