it编程 > 编程语言 > Java

MyBatis中批量插入的三个关键优化技巧与避坑指南

7人参与 2026-01-31 Java

上周接了个数据迁移的活,要把10万条数据从老系统导入新系统。

写了个简单的批量插入,跑起来一看——5分钟。

领导说太慢了,能不能快点?

折腾了一下午,最后优化到3秒,记录一下过程。

最初的代码(5分钟)

最开始写的很简单,foreach循环插入:

// 方式1:循环单条插入(最慢)
for (user user : userlist) {
    usermapper.insert(user);
}

10万条数据,每条都要走一次网络请求、一次sql解析、一次事务提交。

算一下:假设每条插入需要3ms,10万条就是300秒 = 5分钟。

这是最蠢的写法,但我见过很多项目都这么写。

第一次优化:批量sql(30秒)

把循环插入改成批量sql:

<!-- mapper.xml -->
<insert id="batchinsert">
    insert into user (name, age, email) values
    <foreach collection="list" item="item" separator=",">
        (#{item.name}, #{item.age}, #{item.email})
    </foreach>
</insert>
// 分批插入,每批1000条
int batchsize = 1000;
for (int i = 0; i < userlist.size(); i += batchsize) {
    int end = math.min(i + batchsize, userlist.size());
    list<user> batch = userlist.sublist(i, end);
    usermapper.batchinsert(batch);
}

从5分钟降到30秒,提升10倍。

原理:一条sql插入多条数据,减少网络往返次数。

但还有问题:30秒还是太慢。

第二次优化:jdbc批处理(8秒)

mysql有个参数叫rewritebatchedstatements,开启后可以把多条insert合并成一条。

第一步:修改数据库连接url

jdbc:mysql://localhost:3306/test?rewritebatchedstatements=true

第二步:使用mybatis的批处理模式

@autowired
private sqlsessionfactory sqlsessionfactory;

public void batchinsertwithexecutor(list<user> userlist) {
    try (sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch)) {
        usermapper mapper = sqlsession.getmapper(usermapper.class);
        
        int batchsize = 1000;
        for (int i = 0; i < userlist.size(); i++) {
            mapper.insert(userlist.get(i));
            
            if ((i + 1) % batchsize == 0) {
                sqlsession.flushstatements();
                sqlsession.clearcache();
            }
        }
        sqlsession.flushstatements();
        sqlsession.commit();
    }
}

从30秒降到8秒。

原理:executortype.batch模式下,mybatis会缓存sql,最后一次性发送给数据库执行。配合rewritebatchedstatements=true,mysql驱动会把多条insert合并。

第三次优化:多线程并行(3秒)

8秒还是不够快,上多线程:

public void parallelbatchinsert(list<user> userlist) {
    int threadcount = 4;  // 根据数据库连接池大小调整
    int batchsize = userlist.size() / threadcount;
    
    executorservice executor = executors.newfixedthreadpool(threadcount);
    list<future<?>> futures = new arraylist<>();
    
    for (int i = 0; i < threadcount; i++) {
        int start = i * batchsize;
        int end = (i == threadcount - 1) ? userlist.size() : (i + 1) * batchsize;
        list<user> sublist = userlist.sublist(start, end);
        
        futures.add(executor.submit(() -> {
            batchinsertwithexecutor(sublist);
        }));
    }
    
    // 等待所有任务完成
    for (future<?> future : futures) {
        try {
            future.get();
        } catch (exception e) {
            thrownew runtimeexception(e);
        }
    }
    
    executor.shutdown();
}

从8秒降到3秒。

注意事项:

优化效果对比

方案耗时提升倍数
循环单条插入300秒基准
批量sql30秒10倍
jdbc批处理8秒37倍
多线程并行3秒100倍

踩过的坑

坑1:foreach拼接sql过长

<foreach collection="list" item="item" separator=",">

如果一次插入太多条,sql会非常长,可能超过max_allowed_packet限制。

解决:  分批插入,每批500-1000条。

坑2:rewritebatchedstatements不生效

检查几个点:

坑3:自增主键返回问题

批量插入时想获取自增主键:

<insert id="batchinsert" usegeneratedkeys="true" keyproperty="id">

注意:rewritebatchedstatements=true时,自增主键返回可能有问题,需要升级mysql驱动到8.0.17+。

坑4:内存溢出

10万条数据一次性加载到内存,可能oom。

解决:分页读取 + 分批插入。

int pagesize = 10000;
int total = counttotal();
for (int i = 0; i < total; i += pagesize) {
    list<user> page = selectbypage(i, pagesize);
    batchinsertwithexecutor(page);
}

最终方案代码

@service
publicclass batchinsertservice {
    
    @autowired
    private sqlsessionfactory sqlsessionfactory;
    
    /**
     * 高性能批量插入
     * 10万条数据约3秒
     */
    public void highperformancebatchinsert(list<user> userlist) {
        if (userlist == null || userlist.isempty()) {
            return;
        }
        
        int threadcount = math.min(4, runtime.getruntime().availableprocessors());
        int batchsize = (int) math.ceil((double) userlist.size() / threadcount);
        
        executorservice executor = executors.newfixedthreadpool(threadcount);
        countdownlatch latch = new countdownlatch(threadcount);
        
        for (int i = 0; i < threadcount; i++) {
            int start = i * batchsize;
            int end = math.min((i + 1) * batchsize, userlist.size());
            
            if (start >= userlist.size()) {
                latch.countdown();
                continue;
            }
            
            list<user> sublist = new arraylist<>(userlist.sublist(start, end));
            
            executor.submit(() -> {
                try {
                    dobatchinsert(sublist);
                } finally {
                    latch.countdown();
                }
            });
        }
        
        try {
            latch.await();
        } catch (interruptedexception e) {
            thread.currentthread().interrupt();
        }
        
        executor.shutdown();
    }
    
    private void dobatchinsert(list<user> userlist) {
        try (sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch, false)) {
            usermapper mapper = sqlsession.getmapper(usermapper.class);
            
            for (int i = 0; i < userlist.size(); i++) {
                mapper.insert(userlist.get(i));
                
                if ((i + 1) % 1000 == 0) {
                    sqlsession.flushstatements();
                    sqlsession.clearcache();
                }
            }
            
            sqlsession.flushstatements();
            sqlsession.commit();
        }
    }
}

总结

优化点关键配置
批量sqlforeach拼接,分批1000条
jdbc批处理rewritebatchedstatements=true+executortype.batch
多线程线程数 ≤ 连接池大小

核心原则:  减少网络往返 + 减少事务次数 + 并行处理。

到此这篇关于mybatis中批量插入的三个关键优化技巧与避坑指南的文章就介绍到这了,更多相关mybatis批量插入优化内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

您想发表意见!!点此发布评论

推荐阅读

SpringBoot设置欢迎页的三种方式详解

01-31

java的多重注解(重复注解)详解

01-31

JAVA中Spring Boot的AOP切面编程是什么,如何使用?(实例代码)

01-31

Spring Boot全局异常处理机制中DispatcherServlet的处理流程和作用

01-31

Java多重数组使用及说明

01-31

Spring Boot异常处理try-catch应该怎么使用?

01-31

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论