博客
关于我
mybatis+springboot+MySQL批量插入 1w 条数据
阅读量:796 次
发布时间:2023-02-09

本文共 6331 字,大约阅读时间需要 21 分钟。

MyBatis+SpringBoot+MySQL批量插入1万条数据的性能优化方案

前言

传统的单条INSERT语句逐行插入方式,在处理1万条数据时往往需要数秒,这不仅会导致事务锁竞争加剧,更可能引发连接超时等系统性风险。那么如何优化这种批量插入的场景呢?让我们一起探索吧!

传统插入的性能瓶颈

1. 网络延迟

每次插入都独立发往数据库执行,那么每个SQL语句的网络通信都可能造成延迟。

2. 事务控制

每次插入通常会涉及一次事务的开启和提交(如果没有显式地控制事务)。

3. 锁争用和并发性

如果大量插入数据时,逐个插入的方式会增加锁的争用,尤其是当数据表上有多个事务并发访问时,插入的性能会进一步下降。

优化批量插入

扩大事务粒度

在进行批量插入时,将多条INSERT语句放在一个事务中(大事务)比独立事务通常性能更好,原因如下:

  • 事务提交开销

    独立事务需要执行COMMIT操作,这会触发MySQL的日志持久化(如redo log的fsync操作),导致磁盘I/O开销。同时,多次发送开启事务和提交事务的操作,带来了额外的网络开销。大事务只需一次COMMIT,减少了日志刷盘的次数,从而显著降低I/O等待时间。

  • 锁竞争与锁释放

    独立事务每次提交会释放行锁,下一次插入时需重新获取锁,增加了锁竞争的开销。大事务在整个过程中持有锁(如行锁),减少了锁的重复获取和释放,提高并发效率。

  • 日志写入优化

    对于InnoDB引擎,事务日志(redo log)以顺序追加方式写入。大事务的日志批量写入比多次小事务的分散写入更高效,减少了日志缓冲区切换和磁盘寻址开销。

合并多条INSERT

将多条INSERT语句合并成一条INSERT语句(例如INSERT INTO table (col1, col2) VALUES (val1, val2), (val3, val4), ...)也能提高性能,原因包括:

  • 减少网络开销:合并为单条INSERT语句后,客户端与数据库服务器的交互次数减少,降低了网络延迟的影响。
  • 降低SQL解析成本:每条SQL都需要经历语法解析、优化器处理等步骤。合并后只需一次解析,节省CPU资源。
  • 减少锁开销:每条独立的INSERT都需要进行锁定和解锁操作,而合并为一条INSERT操作时,从而减少了锁相关的性能损失。

实现INSERT合并

实现合并INSERT有两种常见方式:

  • 手动拼接SQL:比如通过MyBatis的<foreach>标签。
  • JDBC BatchInsert:在Connection中,添加参数rewriteBatchedStatements=true,如jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true。JDBC会帮我们完成SQL的合并。
  • 预生成ID

    InnoDB为保证自增ID的全局唯一性,在分配自增值时会持有自增锁(AUTO-INC Lock)。在高并发情况下,多个插入操作会频繁争抢数据库的自增ID,这可能导致锁的竞争和性能瓶颈。

    使用预生成ID会有更好的性能表现,比如预先生成雪花ID。避免在数据库层面加锁解锁影响性能。

    批量插入方案性能测试

    环境准备

    • application.yml配置(关键在于rewriteBatchedStatements=true):
      spring:
      datasource:
      url: jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    • 数据库表user
      CREATE TABLE `user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `name` varchar(100) DEFAULT NULL,
      `age` int(11) DEFAULT NULL,
      `email` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    • Java User实体类
      @Data
      @TableName("user")
      public class User {
      private Long id;
      private String name;
      private Integer age;
      private String email;
      }
    • 生成测试数据
      private List
      prepareTestData(int count) {
      List
      users = new ArrayList<>(count);
      for (int i = 0; i < count; i++) {
      User user = new User();
      user.setId(null); // 自增ID
      user.setName("test" + i);
      user.setAge(20 + i % 50);
      user.setEmail("test" + i + "@test.com");
      users.add(user);
      }
      return users;
      }
    • 清空数据
      TRUNCATE TABLE user;
      ALTER TABLE user AUTO_INCREMENT = 1;

    测试方法

    独立事务循环插入

    @Test
    public void testSingleTransactionInsert() {
    List
    users = prepareTestData(10000);
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for (User user : users) {
    userMapper.insert(user);
    }
    stopWatch.stop();
    System.out.println("独立事务循环插入耗时: " + stopWatch.getTotalTimeMillis() + "ms");
    }

    测试结果:独立事务循环插入耗时: 12485ms

    大事务循环插入

    @Test
    @Transactional // 开始事务
    public void testBigTransactionInsert() {
    List
    users = prepareTestData(10000);
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for (User user : users) {
    userMapper.insert(user);
    }
    stopWatch.stop();
    System.out.println("大事务循环插入耗时: " + stopWatch.getTotalTimeMillis() + "ms");
    }

    测试结果:大事务循环插入耗时: 9565ms

    MyBatis foreach批量插入

    @Mapper
    public interface UserMapper extends BaseMapper
    {
    // 方便测试,直接将SQL写到注解
    @Insert(
    "
    "
    )
    void batchInsert(@Param("users") List
    users);
    }
    @Test
    public void testMybatisForeachInsert() {
    List
    users = prepareTestData(10000);
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    userMapper.batchInsert(users);
    stopWatch.stop();
    System.out.println("MyBatis foreach批量插入耗时: " + stopWatch.getTotalTimeMillis() + "ms");
    }

    测试结果:MyBatis foreach批量插入耗时: 891ms

    JDBC BatchInsert

    @Test
    public void testJdbcBatchInsert() {
    List
    users = prepareTestData(10000);
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    jdbcTemplate.batchUpdate(
    "INSERT INTO user (name, age, email) VALUES (?, ?, ?)",
    new BatchPreparedStatementSetter() {
    @Override
    public void setValues(PreparedStatement ps, int i) throws SQLException {
    User user = users.get(i);
    ps.setString(1, user.getName());
    ps.setInt(2, user.getAge());
    ps.setString(3, user.getEmail());
    }
    @Override
    public int getBatchSize() {
    return users.size();
    }
    }
    );
    stopWatch.stop();
    System.out.println("JDBC batch插入耗时: " + stopWatch.getTotalTimeMillis() + "ms");
    }

    测试结果:JDBC batch插入耗时: 587ms

    MyBatis SqlSession批量插入

    @Test
    public void testMybatisBatchInsert() {
    List
    users = prepareTestData(10000);
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    for (User user : users) {
    mapper.insert(user);
    }
    sqlSession.commit();
    }
    stopWatch.stop();
    System.out.println("MyBatis SqlSession批量插入耗时: " + stopWatch.getTotalTimeMillis() + "ms");
    }

    测试结果:MyBatis SqlSession批量插入耗时: 825ms

    MyBatis-Plus saveBatch

    @Test
    public void testMybatisPlusBatchInsert() {
    List
    users = prepareTestData(10000);
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    userService.saveBatch(users, 1000);
    stopWatch.stop();
    System.out.println("MyBatis-Plus批量插入耗时: " + stopWatch.getTotalTimeMillis() + "ms");
    }

    测试结果:MyBatis-Plus批量插入耗时: 860ms

    性能排名

    基于测试结果,性能排名如下:

  • JDBC BatchInsert:587ms
  • MyBatis SqlSession批量插入:825ms
  • MyBatis-Plus saveBatch:860ms
  • MyBatis foreach批量插入:891ms
  • 大事务循环插入:9565ms
  • 独立事务循环插入:12485ms
  • 如何选择?

    • 极致性能:JDBC BatchInsert。虽然需要手写SQL,导致硬编码,但性能优越。
    • 通用方案:优先选择MyBatis-Plus saveBatch,可以方便地分片处理和分批提交事务。其次选择MyBatis SqlSession批量插入。

    测试的局限性

  • 需要记录每种方案在不同数据量、不同字段数和不同字段大小对批量插入的影响。
  • 考虑并发场景下的性能,是否会涉及大量锁竞争。
  • 测试是在本地运行,没有考虑网络带宽的影响。
  • 如何将批量插入应用到业务中?

    问题的核心在于:如何“攒一波”数据来实现批量插入? 推荐批量插入与MQ配合使用。将需要插入的数据的消息发送给MQ,生产者需要保证MQ的消息发送和本地事务的原子性。对于消费者,一次性拉取多个消息进行批量插入。当消息消费失败时,可以让MQ重新投递消息并重新消费。

    不过,MQ的引入和积攒数据,带来的数据插入的延迟是不可避免的,同时只能保证最终一致性而不是强一致。

    转载地址:http://cfffk.baihongyu.com/

    你可能感兴趣的文章
    Mac os如何安装绿盾客户端
    查看>>
    mac xmind 激活
    查看>>
    MAC 上的 Selenium,消息:“chromedriver“可执行文件可能具有错误的权限
    查看>>
    Mac 下 Python+Selenium 自动上传西瓜视频
    查看>>
    mac 下 react Native ios环境搭建
    查看>>
    Mac 下使用sourcetree操作git教程
    查看>>
    mac 下如何建立vue-cli项目
    查看>>
    Mac 也需要杀毒?我亲测三款顶级杀毒软件,保护你的数据安全!从零基础到精通,收藏这篇就够了!
    查看>>
    Mac 在命令行快速切换目录 mark
    查看>>
    mac 安装PIL
    查看>>
    Mac 开发PhoneGap 应用,怎样加入插件 barcodescaner
    查看>>
    mac 搭建APK反编译环境[转]
    查看>>
    MAC 显示隐藏文件
    查看>>
    Mac 的“任务管理器” —— 活动监视器
    查看>>
    mac 虚拟机安装oracle 11g,MAC+Vmware Fusion安装Oracle11g RAC
    查看>>
    mac 配置环境变量,讲的太仔细了,非常棒
    查看>>
    mac-gradle的安装和配置
    查看>>
    mac/ip/TCP/udp报文格式与理论大小
    查看>>
    Mac:Permission denied XXX
    查看>>
    macaca 测试web(2)
    查看>>