dbaplus社群 2024年12月15日
性能提升2000%!MyBatis-Plus批量插入的终极优化技巧
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了如何通过优化MyBatis-Plus的批量插入操作,显著提升数据库性能。文章首先分析了批量插入的性能瓶颈,如逐条插入效率低、外键关系处理复杂等问题。接着,详细介绍了如何通过配置`rewriteBatchedStatements=true`启用JDBC批处理重写,以及预先生成ID来解决外键关联。此外,还探讨了多线程并发插入的实现方式,以及数据库层面的优化策略。通过综合优化,文章展示了性能提升2000%的显著效果,并提供了最佳实践总结,为Java开发者在处理高并发、大数据量场景下的数据库操作提供了宝贵的参考。

🚀 **JDBC批处理优化**: 通过设置 `rewriteBatchedStatements=true`,MySQL JDBC驱动可以将多条SQL语句合并为一条执行,减少网络交互和数据库解析开销,显著提升批量插入性能。

🆔 **预生成ID策略**: 使用如zzidc等ID生成器预先生成主键ID,避免了插入后再获取ID的延迟,解决了外键关联问题,使得批量插入更加高效。

🧵 **多线程并发插入**: 利用Spring的`@Async`注解实现异步方法调用,每个异步方法拥有独立的事务和SqlSession,确保线程安全,并通过`CompletableFuture`管理异步任务,提高并发插入效率。

🎛️ **数据库层面优化**: 合理配置数据库连接池的大小,并设置MyBatis的执行器类型为`BATCH`,可以进一步提升批量操作的性能,减少资源消耗。

📊 **监控与调优**: 通过日志记录和`CompletableFuture`监控异步任务的执行情况,并根据实际情况调整线程池参数,确保系统性能稳定和高效。

朱洪旭 2024-12-15 08:01 广东

不但在考试系统中适用,在其他需要批量处理大量数据的场景下同样有重要参考价值。



分享概要

一、前言

二、背景:批量插入的性能挑战

三、初探 MyBatis-Plus 的 saveBatch 方法

四、深度解析 rewriteBatchedStatements=true 的作用

五、预先生成 ID:解决外键关系的关键

六、综合优化实践:性能提升 2000%

七、多线程并发插入的实现

八、数据库层面的优化

九、监控与调优

十、最佳实践总结

结语


一、前言


在当今互联网高速发展的时代,高并发、大数据量的处理已成为各大企业应用的常态。作为 Java 开发者,我们常常面临着如何提高数据库操作效率的挑战。MyBatis-Plus 作为一款优秀的 ORM 框架,为我们提供了简洁高效的数据库操作方式。然而,当涉及到大规模数据的批量插入时,即使使用了 saveBatch 方法,性能提升仍然有限。


本文将揭秘如何通过配置 rewriteBatchedStatements=true 和预先生成 ID 等优化策略,将 MyBatis-Plus 批量插入的性能提升 2000%,助力您的应用突破性能瓶颈!


二、背景:批量插入的性能挑战


1、场景描述


在实际开发中,如考试系统、订单处理、日志存储等场景,经常需要批量插入大量数据。例如,在一个在线考试系统中,创建一份试卷需要插入多张表的数据:



在保存试卷时,需要关联保存试卷、题目以及题目选项,此时对于保存的性能就有较高的要求了。


2、性能瓶颈



三、初探 MyBatis-Plus 的 saveBatch 方法


1、saveBatch 方法简介


在 MyBatis-Plus 中,saveBatch 方法是用于批量保存数据的方法。它能够在单次操作中将多条数据同时插入数据库,从而提高插入效率,减少数据库连接次数,提升性能。


    boolean saveBatch(Collection<T> entityList);    boolean saveBatch(Collection<T> entityList, int batchSize);



2、常用场景



3、默认实现的局限性



四、深度解析 rewriteBatchedStatements=true 的作用


1、JDBC 批处理机制


JDBC 批处理机制是一种优化数据库操作性能的技术,允许将多条 SQL 语句作为一个批次发送到数据库服务器执行,从而减少客户端与数据库之间的交互次数,显著提高性能。通常用于 批量插入、批量更新 和 批量删除 等场景。具体的流程如下:


    //创建 PreparedStatement 对象,用于定义批处理的 SQL 模板。    PreparedStatement pstmt = conn.prepareStatement(sql);    for (Data data : dataList) {      // 多次调用 addBatch() 方法,每次调用都会将一条 SQL 加入批处理队列。      pstmt.addBatch();    }      //执行批处理,调用 executeBatch() 方法,批量发送 SQL 并执行。      pstmt.executeBatch();


2、MySQL JDBC 驱动的默认行为对批处理的影响



3、rewriteBatchedStatements=true 的魔力



未开启参数时的批处理 SQL:


INSERT INTO question (exam_id, content) VALUES (?, ?);INSERT INTO question (exam_id, content) VALUES (?, ?);INSERT INTO question (exam_id, content) VALUES (?, ?);


开启参数后的批处理 SQL:


INSERT INTO question (exam_id, content) VALUES (?, ?), (?, ?), (?, ?);


五、预先生成 ID:解决外键关系的关键


1、问题分析


在插入题目和选项时,选项需要引用对应题目的主键 ID。如果等待题目插入后再获取 ID,会导致无法进行批量操作,影响性能。所以,预先生成ID就成了我们解决问题的关键。


2、预先生成 ID 的解决方案


使用 zzidc(自研的 ID 生成器):



3、应用实践


1)引入 zzidc


       <!--id生成器-->        <dependency>            <groupId>com.bj58.zhuanzhuan.idc</groupId>            <artifactId>contract</artifactId>            <version>${com.bj58.zhuanzhuan.idc.version}</version>        </dependency>

2)具体的代码业务执行逻辑


在构建题目和选项数据时,预先生成 ID,并在选项中引用对应的题目 ID:


      public Boolean createExamPaper(HeroExamRequest<ExamPaperRequest> request) throws BusinessException{          // 构建题目数据          Question question = new Question();          question.setId(questionId);          question.setExamId(examId);          // ...          // 构建选项数据          Option option = new Option();          option.setQuestionId(questionId);          // ...    }

六、综合优化实践:性能提升 2000%


1、配置 rewriteBatchedStatements=true


1)修改数据库连接配置


实现这个配置的方式很简单,只需要在我们现有的数据库连接后面直接加上就好。例如:jdbc:mysql://localhost:3306/db_name?rewriteBatchedStatements=true


2)注意事项



2、完整代码示例


@Servicepublic class ExamServiceImpl implements ExamService {
@Autowired private ExamMapper examMapper; @Autowired private QuestionService questionService; @Autowired private OptionService optionService;
private static final int BATCH_SIZE = 2000;
@Override @Transactional(rollbackFor = Exception.class) public void createExam(Exam exam, int questionCount, int optionCountPerQuestion) { // 预先生成试卷 ID long examId = zzidc.nextId(); exam.setId(examId); examMapper.insert(exam);
List<Question> questionList = new ArrayList<>(); List<Option> allOptionList = new ArrayList<>();
for (int i = 0; i < questionCount; i++) { // 预先生成题目 ID long questionId = zzidc.nextId(); Question question = new Question(); question.setId(questionId); question.setExamId(examId); question.setContent("题目内容" + i); questionList.add(question); // 构建选项数据 for (int j = 0; j < optionCountPerQuestion; j++) { Option option = new Option(); option.setQuestionId(questionId); option.setContent("选项内容" + j); allOptionList.add(option); } }
// 批量插入题目和选项 questionService.saveBatch(questionList, BATCH_SIZE); optionService.saveBatch(allOptionList, BATCH_SIZE); }}


注意:以上代码为示例,需根据实际项目进行调整。


3、性能测试


1)测试数据



2)测试方案



3)测试结果



方案 耗时(毫秒)性能提升
未优化方案4023-
仅使用 saveBatch2744↑ 30%
综合优化方案149↑ 2700%


4)数据分析



七、多线程并发插入的实现


1、问题分析


直接在多线程中调用 saveBatch 方法,可能导致以下问题:



2、正确的多线程实现方式


1)使用 @Async 异步方法


利用 Spring 的 @Async 注解,实现异步方法调用,每个异步方法都有自己的事务和 SqlSession。


配置异步支持:


@Configuration@EnableAsyncpublic class AsyncConfig {@Bean(name = "taskExecutor")public Executor taskExecutor() {    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();    executor.setCorePoolSize(4); // 核心线程数    executor.setMaxPoolSize(8); // 最大线程数    executor.setQueueCapacity(100); // 队列容量    executor.setThreadNamePrefix("AsyncExecutor-");    executor.initialize();    return executor;  }}


修改批量插入方法:


@Servicepublic class QuestionServiceImpl implements QuestionService {
@Autowired private QuestionMapper questionMapper;
@Override @Async("taskExecutor") @Transactional(rollbackFor = Exception.class) public CompletableFuture<Void> saveBatchAsync(List<Question> questionList) { saveBatch(questionList, BATCH_SIZE); return CompletableFuture.completedFuture(null); }}


2)调用异步方法


public void createExam(Exam exam, int questionCount, int optionCountPerQuestion) {  // ... 数据准备部分略 ...
// 将题目列表拆分成多个批次 List<List<Question>> questionBatches = Lists.partition(questionList, BATCH_SIZE); List<List<Option>> optionBatches = Lists.partition(allOptionList, BATCH_SIZE);
List<CompletableFuture<Void>> futures = new ArrayList<>(); // 异步批量插入题目 for (List<Question> batch : questionBatches) { CompletableFuture<Void> future = questionService.saveBatchAsync(batch); futures.add(future); }
// 异步批量插入选项 for (List<Option> batch : optionBatches) { CompletableFuture<Void> future = optionService.saveBatchAsync(batch); futures.add(future); }
// 等待所有异步任务完成 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();}


3)注意事项



八、数据库层面的优化


1、调整数据库连接池



2、配置 MyBatis 的执行器类型


修改执行器类型为 BATCH:在 MyBatis 配置中,设置执行器类型,可以提高批量操作的性能。


<configuration>  <settings>    <setting name="defaultExecutorType" value="BATCH"/>  </settings></configuration>


注意:使用 BATCH 执行器时,需要手动调用 sqlSession.flushStatements(),并处理返回的 BatchResult,复杂度较高,建议谨慎使用。


九、监控与调优


1、监控异步任务的执行情况



@Async("taskExecutor")@Transactional(rollbackFor = Exception.class)public CompletableFuture<Void> saveBatchAsync(List<Question> questionList) {  long startTime = System.currentTimeMillis();  saveBatch(questionList, BATCH_SIZE);  long endTime = System.currentTimeMillis();  logger.info("Inserted batch of {} questions in {} ms", questionList.size(), (endTime - startTime));  return CompletableFuture.completedFuture(null);}

2、调整线程池参数



十、最佳实践总结


1、综合优化策略



2、注意事项



结语


深入理解 rewriteBatchedStatements=true 参数的效用,再结合预先生成 ID、恰当的多线程实现方式以及数据库参数调整等优化策略,我们成功地将 MyBatis-Plus 批量插入的性能大幅提升了 2000%。这些优化技巧不但在考试系统中适用,在其他需要批量处理大量数据的场景下同样具有重要的参考价值。


性能优化乃是一项系统性工程,需从应用层、数据库层、硬件层等多个层面着手。期望本文的分享能够在实际项目中为您提供切实可行的指导,助力您的应用成功突破性能瓶颈。


作者丨张守法 侠客汇Java开发工程师

来源丨公众号:转转技术(ID:zhuanzhuantech)

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

MyBatis-Plus 批量插入 性能优化 JDBC 多线程
相关文章