MySQL 插入10万条数据性能分析

2024-02-17 14:36

本文主要是介绍MySQL 插入10万条数据性能分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

MySQL 插入10万条数据性能分析

一、背景

笔者想复现一个索引失效的场景,故需要一定规模的数据作支撑,所以需要向数据库中插入大约一百万条数据。那问题就来了,我们应该怎样插入才能使插入的速度最快呢?

为了更加贴合实际,下面的演示只考虑使用 Mybaits 作为 ORM 框架的情况,不使用原生的 JDBC。下面,我们只向数据库中插入十万条数据作为演示。

二、实现

1. 使用 Mybaits 直接插入

Java 代码为:

public void insertByMybatis() {for (int i = 0; i < 100000; i++) {InvoiceOrder invoiceOrder = new InvoiceOrder();invoiceOrder.setOrderId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceName("test" + i);invoiceOrder.setInvoiceDate(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));invoiceOrder.setOrderTime(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));orderMapper.insertSelective(invoiceOrder);}
}

插入结果:

同步插入10万条数数据

同步插入10万条数据的耗时为 242s

2. 使用 Mybatis 直接插入数据,取消事务自动提交

@Autowired
private DataSourceTransactionManager transactionManager;public void insertByMybatisWithNoTransaction() {DefaultTransactionDefinition def = new DefaultTransactionDefinition();def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);TransactionStatus status = transactionManager.getTransaction(def);for (int i = 0; i < 100000; i++) {InvoiceOrder invoiceOrder = new InvoiceOrder();invoiceOrder.setOrderId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceName("test" + i);invoiceOrder.setInvoiceDate(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));invoiceOrder.setOrderTime(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));orderMapper.insertSelective(invoiceOrder);}transactionManager.commit(status);
}

插入结果:

同步插入数据,取消事务自动提交

直接插入数据,并取消事务自动提交,耗时为 28s

3. 使用 Mybatis 批量插入数据

Java 代码为:

/*** Mybatis批量插入*/
public void batchInsertByMybatis() {List<InvoiceOrder> invoiceOrders = new ArrayList<>();for (int i = 0; i < 100000; i++) {InvoiceOrder invoiceOrder = new InvoiceOrder();invoiceOrder.setOrderId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceName("test" + i);invoiceOrder.setInvoiceDate(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));invoiceOrder.setOrderTime(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));invoiceOrders.add(invoiceOrder);if (i % 1500 == 0) {orderMapper.insertBatch(invoiceOrders);invoiceOrders = new ArrayList<>();}}// 最后插入剩下的数据orderMapper.insertBatch(invoiceOrders);
}

结果:

批量插入10万数据

批量插入10条数据耗时 4s

4. 使用 Mybatis 批量插入数据,取消事务自动提交

@Autowired
private DataSourceTransactionManager transactionManager;public void batchInsertByMybatisWithNoTransaction() {DefaultTransactionDefinition def = new DefaultTransactionDefinition();def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);TransactionStatus status = transactionManager.getTransaction(def);List<InvoiceOrder> invoiceOrders = new ArrayList<>();for (int i = 0; i < 100000; i++) {InvoiceOrder invoiceOrder = new InvoiceOrder();invoiceOrder.setOrderId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceName("test" + i);invoiceOrder.setInvoiceDate(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));invoiceOrder.setOrderTime(DateUtil.date().offset(DateField.HOUR_OF_DAY, -i));invoiceOrders.add(invoiceOrder);if ( i % 10000 == 0) {orderMapper.insertBatch(invoiceOrders);invoiceOrders = new ArrayList<>();}}// 最后插入剩下的数据orderMapper.insertBatch(invoiceOrders);transactionManager.commit(status);
}

结果为:

批量插入数据并取消事务自动提交

耗时4s,与批量插入自动提交事务方式的耗时相差不大

5. 使用多线程批量插入数据

public void asyncInsertTest() throws InterruptedException {for (int i = 0; i < THREAD_COUNT; i++) {int finalI = i;threadPoolExecutor.submit(new Runnable() {@Overridepublic void run() {List<InvoiceOrder> invoiceOrders = new ArrayList<>();int begin = finalI * 20000;int end = 20000 * (finalI + 1);for (int id = begin; id < end; id++) {InvoiceOrder invoiceOrder = new InvoiceOrder();invoiceOrder.setId((long) id);invoiceOrder.setOrderId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceId(UUID.randomUUID().toString().replace("-", ""));invoiceOrder.setInvoiceName("test" + id);invoiceOrder.setInvoiceDate(DateUtil.date().offset(DateField.HOUR_OF_DAY, -id));invoiceOrder.setOrderTime(DateUtil.date().offset(DateField.HOUR_OF_DAY, -id));invoiceOrders.add(invoiceOrder);}orderMapper.insertBatch(invoiceOrders);}});}threadPoolExecutor.shutdown();while (!threadPoolExecutor.isTerminated()) {Thread.sleep(100); // 等待线程池中的任务执行完毕}}

结果为:

多线程插入数据

耗时 3s,与同步插入相比,有很大的性能提升,这里的每条线程都是使用批量插入的模式,一次事务提交。

6. 使用 第三方数据库连接池插入数据

SpringBoot 中默认使用的数据库连接池是 Hikari,这里我们换用 Druid 和 c3p0 连接池,同步插入10万条数据。

6.1 使用 Druid 连接池插入数据

Druid 依赖

<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version>
</dependency>

配置 Druid 文件信息

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/test_db?characterEncoding=utf-8&serverTimezone=Hongkong&useSSL=falseusername: rootpassword: ezrealtype: com.alibaba.druid.pool.DruidDataSourceserver:port: 8080

结果如下:

使用Druid连接池插入10条数据

  • 与使用 Hikari 连接池相比,Hikari 耗时 242s,Druid 耗时 246s 两者相差不大;
6.2 使用 c3p0 连接池插入数据

c3p0 依赖

<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version>
</dependency>

配置 c3p0 文件信息

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/test_db?characterEncoding=utf-8&serverTimezone=Hongkong&useSSL=falseusername: rootpassword: ezrealtype: com.alibaba.druid.pool.DruidDataSourceserver:port: 8080

结果为:

c3p0插入10条数据

  • 与使用 Hikari 连接池相比,Hikari 耗时 242s,c3p0 耗时 242s 两者相差不大;

三、结果比较分析

3.1 单次循环插入和批量插入

对比方式 1 和 方式 3,前者是单次循环插入,后者是批量插入,分别耗时 242s 和 4s,显然批量插入的效率高。

这个问题可以分三个方面网络数据库交互事务索引分析:

  1. 网络:客户端每次与数据库交互,都要进行一次 TCP 三次握手和四次挥手,频繁的建立和释放数据库连接增加了数据库负担;
  2. 数据库交互:在建立网络连接后,数据库要进行一系列的准备工作:查询缓存语法分析词法分析、优化器分析、存储引擎执行 SQL;
  3. 事务:每次数据插入操作都会开启一个事务,频繁的开启和提交事务,也会增加数据库的负担;
  4. 索引:如果插入的字段存在对应的二级索引,那么就要在该二级索引上也要添加上对应的数据,涉及到大量的磁盘操作;

对于索引来说,可能会存在页分裂和页合并的情况,比如说插入时数据库的主键不是自增的。

对于单次循环插入,每次都需要重复以上三个方面的内容,非常影响性能;而对于批量插入来说,每次操作大量的数据,减少网络、数据库交互、事务等操作,提高插入的效率;

3.2 事务自动提交分析

对比方式 1 和 方式 2,前者是每次插入就进行一次事务操作,而后者是提前开启事务,等到所有的数据都 insert 后,再提交事务,前者耗时 242s,后者耗时 28s,显然只进行一次事务操作的插入效率高

3.2.1 事务执行流程

回答这个问题之前,我们需要了解一次事务执行的流程,以 insert 操作为例:

  1. 向 buffer pool 中写入数据,将数据写入到 flush 链表中,由后台线程定时同步到磁盘上
  2. 记录 undo log buffer ,数据插入之前,InnoDB 会在 Undo Log 记录对应的 delete 语句,用于在生事务回滚的情况下,将修改操作回滚到插入前的状态,undo log 先写入到 undo log buffer 中,由后台线程定时落盘
  3. 记录 redolog buffer,InnoDB 在 buffer pool 插入数据的同时,会把操作记录写入到 redolog buffer 中;
  4. 提交事务,InnoDB 会把 redo log 从 redolog buffer 写入到磁盘中(顺序写入),此时 redolog 处于 prepare 状态,接着执行器生成这个操作的 binlog 写入磁盘,最后把刚刚 redo log 改为 commit 状态,数据插入成功,这就是所谓的二阶段提交

这里主要涉及到两处内存操作和两处磁盘操作

  • 将 undolog 写入到 undolog buffer 中;

  • 将 redolog 写入到 redolog buffer 中;

  • 在事务提交后,InnoDB 会把 redo log 从 redolog buffer 写入到磁盘中;

  • 将该操作的 binlog 也写入到磁盘中

所以,对于方式1,每一次插入数据都要进行两次的磁盘 IO,然而磁盘的读取速度是非常耗时的,大量的磁盘 IO就会影响插入的性能。如果能够减少大量的磁盘 IO,即减少事务开启的次数,那么就可以大大减少插入的耗时。

3.2.2 事务操作涉及到的锁

涉及到事务就可能会涉及到锁的竞争。一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的 gap lock,如果有的话,插入操作就需要等待,直到拥有 gap lock 的事务释放了锁。

InnoDB 规定,在上述等待过程中,会在内存中生成一个锁结构,表明有事务想在间隙中插入一条新记录,但是现在在等待。所以 InnoDB 就把这种类型的锁名称命名为 Insert Intention Locks ,我们称为插入意向锁

3.3 多线程插入优化

通过并发执行多个插入操作来提高数据插入效率:

  • 并发执行:利用多核处理器的优势,通过多个线程并发执行插入操作,提高系统的吞吐量;
  • 减少锁竞争:多个线程批量插入,类似分段的思想,不同线程只会操作不同的数据段,减少不同线程的锁竞争;

3.4 数据库连接池的选择

至于数据库连接池的选择,这里提供一份大佬写的文章,里面详细比较了常见数据库连接池的性能测试。

这篇关于MySQL 插入10万条数据性能分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/718078

相关文章

MySQL 中的 CAST 函数详解及常见用法

《MySQL中的CAST函数详解及常见用法》CAST函数是MySQL中用于数据类型转换的重要函数,它允许你将一个值从一种数据类型转换为另一种数据类型,本文给大家介绍MySQL中的CAST... 目录mysql 中的 CAST 函数详解一、基本语法二、支持的数据类型三、常见用法示例1. 字符串转数字2. 数字

Mysql实现范围分区表(新增、删除、重组、查看)

《Mysql实现范围分区表(新增、删除、重组、查看)》MySQL分区表的四种类型(范围、哈希、列表、键值),主要介绍了范围分区的创建、查询、添加、删除及重组织操作,具有一定的参考价值,感兴趣的可以了解... 目录一、mysql分区表分类二、范围分区(Range Partitioning1、新建分区表:2、分

MySQL 定时新增分区的实现示例

《MySQL定时新增分区的实现示例》本文主要介绍了通过存储过程和定时任务实现MySQL分区的自动创建,解决大数据量下手动维护的繁琐问题,具有一定的参考价值,感兴趣的可以了解一下... mysql创建好分区之后,有时候会需要自动创建分区。比如,一些表数据量非常大,有些数据是热点数据,按照日期分区MululbU

SQL Server配置管理器无法打开的四种解决方法

《SQLServer配置管理器无法打开的四种解决方法》本文总结了SQLServer配置管理器无法打开的四种解决方法,文中通过图文示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录方法一:桌面图标进入方法二:运行窗口进入检查版本号对照表php方法三:查找文件路径方法四:检查 S

MySQL 删除数据详解(最新整理)

《MySQL删除数据详解(最新整理)》:本文主要介绍MySQL删除数据的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、前言二、mysql 中的三种删除方式1.DELETE语句✅ 基本语法: 示例:2.TRUNCATE语句✅ 基本语

MySQL中查找重复值的实现

《MySQL中查找重复值的实现》查找重复值是一项常见需求,比如在数据清理、数据分析、数据质量检查等场景下,我们常常需要找出表中某列或多列的重复值,具有一定的参考价值,感兴趣的可以了解一下... 目录技术背景实现步骤方法一:使用GROUP BY和HAVING子句方法二:仅返回重复值方法三:返回完整记录方法四:

从入门到精通MySQL联合查询

《从入门到精通MySQL联合查询》:本文主要介绍从入门到精通MySQL联合查询,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下... 目录摘要1. 多表联合查询时mysql内部原理2. 内连接3. 外连接4. 自连接5. 子查询6. 合并查询7. 插入查询结果摘要前面我们学习了数据库设计时要满

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

MySQL查询JSON数组字段包含特定字符串的方法

《MySQL查询JSON数组字段包含特定字符串的方法》在MySQL数据库中,当某个字段存储的是JSON数组,需要查询数组中包含特定字符串的记录时传统的LIKE语句无法直接使用,下面小编就为大家介绍两种... 目录问题背景解决方案对比1. 精确匹配方案(推荐)2. 模糊匹配方案参数化查询示例使用场景建议性能优

mysql表操作与查询功能详解

《mysql表操作与查询功能详解》本文系统讲解MySQL表操作与查询,涵盖创建、修改、复制表语法,基本查询结构及WHERE、GROUPBY等子句,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随... 目录01.表的操作1.1表操作概览1.2创建表1.3修改表1.4复制表02.基本查询操作2.1 SE