批量插入优化

Posted by Wh0ami-hy on January 24, 2024

1. 背景

超过 10w+ 的数据量

2. JDBC环境

JDBC环境下推荐自己实现批处理方式

导入依赖

!-- MySQL 驱动 -->  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>5.1.9</version>  
</dependency>

JDBC 批量插入的实现方式:批处理插入方式 + 手动事务提交

public class JDBCPlusDemo {  
    public static void main(String[] args) {  
        // url 设置允许重写批量提交 rewriteBatchedStatements=true  
        String url = "jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true";  
        String user = "root";  
        String password = "123456";  
        String driver = "com.mysql.jdbc.Driver";  
        // sql语句(注意url设置为rewriteBatchedStatements=true时,不允许sql语句带有;号,否则会抛出BatchUpdateException异常)  
        String sql = "INSERT INTO fee(`owner`,`fee1`,`fee2`,`fee3`,`fee4`,`fee5`) VALUES (?,?,?,?,?,?)";  
        Connection conn = null;  
        PreparedStatement ps = null;  
        // 开始时间  
        long start = System.currentTimeMillis();  
        try {  
            Class.forName(driver);  
            conn = DriverManager.getConnection(url, user, password);  
            ps = conn.prepareStatement(sql);  
            // 关闭自动提交  
            conn.setAutoCommit(false);  
            for (int i = 1; i <= 100000; i++) {  
                ps.setString(1, "o"+i);  
                ps.setBigDecimal(2, new BigDecimal("11111.111"));  
                ps.setBigDecimal(3, new BigDecimal("11111.111"));  
                ps.setBigDecimal(4, new BigDecimal("11111.111"));  
                ps.setBigDecimal(5, new BigDecimal("11111.111"));  
                ps.setBigDecimal(6, new BigDecimal("11111.111"));  
                // 加入批处理(将当前sql加入缓存)  
                ps.addBatch();  
                // 以 1000 条数据作为分片  
                if (i % 1000 == 0) {  
                    // 执行缓存中的sql语句  
                    ps.executeBatch();  
                    // 清空缓存  
                    ps.clearBatch();  
                }  
            }  
            ps.executeBatch();  
            ps.clearBatch();  
            // 事务提交(实际开发中需要判断有插入失败的需要在 finally 中做好事务回滚操作)  
            conn.commit();  
        } catch (ClassNotFoundException | SQLException e) {  
            e.printStackTrace();  
        } finally {  
            if (conn != null) {  
                try {  
                    conn.close();  
                } catch (SQLException e) {  
                    e.printStackTrace();  
                }  
            }  
            if (ps != null) {  
                try {  
                    ps.close();  
                } catch (SQLException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
        // 结束时间  
        long end = System.currentTimeMillis();  
        System.out.println("十万条数据插入时间(批处理插入):" + (end - start) + " ms");  
    }  
}

需要注意的细节和重点

  • 需要注意批量处理时的分片操作,分片操作可以避免一次性提交的数据量过大从而导致数据库在处理时出现的性能问题和内存占用过高问题,有效的分片可以减轻数据库的负担
  • 使用手动事务提交可以提高插入速度,在批量插入大量数据时,手动事务提交相对于自动提交事务来说可以减少磁盘的写入次数,减少锁竞争,从而提高插入的性能
  • 使用 JDBC 时需要注意插入的 sql 语句结尾不能带 ; 号,否则会抛出 BatchUpdateException 异常
  • 在配置 MySQL 的 url 时需要加上 rewriteBatchedStatements=true 才能达到真正意义上的批处理效果。如果不开启允许重写批量提交,实际上批处理操作和原本的单条语句循环插入的效果一样

3. MyBatis环境

foreach 动态拼接插入,当数据量为 10w 条时,测试结果报错,这是因为默认情况下 MySQL 可执行的最大 SQL 语句大小为 4194304 即 4MB,这里使用动态 SQL 拼接后的大小远大于默认值,故报错

MyBatis环境下推荐在MyBatis中使用JDBC 的批处理插入方式

@Transactional  
public int saveByBatch(List<Fee> feeList) {  
    // 记录结果(影响行数)  
    int res = 0;  
    // 开启批处理模式  
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);  
    FeeMapper feeMapper = sqlSession.getMapper(FeeMapper.class);  
    for (int i = 1; i <= feeList.size(); i++) {  
        // 利用mapper的单条插入方法插入  
        res += feeMapper.insertByOne(feeList.get(i-1));  
        // 进行分片类似 JDBC 的批处理  
        if (i % 100000 == 0) {  
            sqlSession.commit();  
            sqlSession.clearCache();  
        }  
    }  
    sqlSession.commit();  
    sqlSession.clearCache();  
    return res;  
}

需要注意的细节和重点

  • 和 JDBC 一样都需要开启允许重写批量处理提交(即在配置文件的数据库配置 url 中加上rewriteBatchedStatements=true
  • 代码中需要使用批处理模式(利用 SqlSessionFactory 设置批处理模式并获取对应的 Mapper 接口)
  • 代码中同样进行了分片操作,目的是为了减轻数据库的负担避免在处理时内存占用过高
  • 可以在实现方法中加上 @Transactional 注解来起到手动提交事务的效果(好处和 JDBC 一样)

4. MyBatis Plus环境

使用自带的saveBatch(feeList)方法

需要注意的细节和重点

需要开启允许重写批量处理提交(即在配置文件的数据库配置 url 中加上rewriteBatchedStatements=true


本站总访问量