章
目
录
你知道在使用MyBatis Plus进行百万级大批量数据查询时,如何避免数据量过大导致查询很慢的问题呢?首先我们知道数据库记录查询,在大数据量操作的场景一般有以下几种情况:
- 数据迁移
- 数据导出
- 批量处理数据
在实际工作中,当需要查询的数据量很大时,通常会使用分页查询的方式,逐页将数据加载到内存中进行处理。这种方式可以有效地避免一次性加载过多数据导致的内存溢出(OOM)问题。
然而,并非所有情况都适合使用分页查询方式。有些场景下,我们可能不需要分页,或者需要一次性加载很大量的数据。但是,如果我们将所有数据一次性加载到内存中,很可能会导致内存溢出的问题,并且查询速度也会变慢。这是因为框架需要耗费大量的时间和内存来将数据库查询结果封装成我们所需的对象(如实体类)。
因此,在这些情况下,我们需要谨慎处理。可以考虑使用流式查询、分批加载数据或者优化查询语句,以减少内存消耗和提高查询效率。同时,也需要根据具体业务需求和系统资源情况来合理设置数据加载的策略,以避免内存溢出和查询性能问题。
举例:在业务系统需要从 MySQL 数据库里读取 100w 数据行进行处理,应该怎么做?
做法通常如下:
- 常规查询: 一次性读取 100w 数据到 JVM 内存中,或者分页读取
- 流式查询: 建立长连接,利用服务端游标,每次读取一条加载到 JVM 内存(多次获取,一次一行)
- 游标查询: 和流式一样,通过 fetchSize 参数,控制一次读取多少条数据(多次获取,一次多行)
常规查询
默认情况下,完整的检索结果集会将其存储在内存中。在大多数情况下,这是最有效的操作方式,并且由于 MySQL 网络协议的设计,因此更易于实现。
举例:
假设单表 100w 数据量,一般会采用分页的方式查询:
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
@Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
}
该方式比较简单,但对于大数据量的查询,不考虑 LIMIT 深分页优化的情况下,数据库服务器可能会受到很大压力,并且查询时间会非常长,甚至可能导致查询超时或崩溃。在处理大数据量查询时,我们需要考虑性能优化和合理的查询策略,以避免对数据库和服务器造成过大负担。这可能包括使用索引、合理分片数据、优化查询语句等手段。
流式查询
流式查询指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果,这样能够降低内存使用。
如果没有流式查询,我们想要从数据库取100w条记录而又没有足够的内存时,就只能采用分页查询。然而,分页查询的效率取决于表的设计,如果设计不好,可能无法执行高效的分页查询。因此,流式查询是一个数据库访问框架必须具备的功能。
在MyBatis中,使用流式查询可以避免数据量过大导致OOM。但要注意,流式查询期间数据库连接是保持打开状态的,所以:
1)执行流式查询后,数据库访问框架不负责关闭数据库连接,需要应用在取完数据后自行关闭连接。
2)必须先读取(或关闭)结果集中的所有行,然后才能对连接发出任何其他查询,否则将引发异常。
MyBatis 流式查询接口
MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor
的接口类用于流式查询,这个接口继承了 java.io.Closeable
和 java.lang.Iterable
接口,由此可知:
- Cursor 是可关闭的;
- Cursor 是可遍历的。
除此之外,Cursor 还提供了三个方法:
- isOpen(): 用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
- isConsumed(): 用于判断查询结果是否全部取完。
- getCurrentIndex(): 返回已经获取了多少条数据
使用流式查询,则要保持对产生结果集的语句所引用的表的并发访问,因为其 查询会独占连接,所以必须尽快处理
用流式查询原因
如果你需要处理一个非常大的查询结果集,又不想一次性将结果集加载到客户端内存中,那么流式查询是一个可行的选择。
在分库分表的场景下,单个表的查询结果集可能并不大,但是如果某个查询涉及到多个库和多个表,并且需要进行结果集的合并、排序等操作,仍然可能导致内存溢出的问题。经过详细研究sharding-sphere的代码,你会发现除了涉及到group by和order by字段不一致的情况外,其他场景都非常适合使用流式查询,这样可以最大程度地减少对客户端内存的消耗。
游标查询
为了避免内存泄漏的情况发生,我们可以采用游标方式进行数据查询处理。这种方式比常规查询更快速有效。
当需要处理大量数据时,特别是百万级的数据,游标方式的数据查询处理是一个不错的选择。它不仅可以减少内存的消耗,还可以避免一次性取出所有数据,而是可以逐条处理或者逐条取出部分数据进行批量处理。我们可以一次查询指定 fetchSize 的数据,直到将所有数据处理完毕。
Mybatis 的处理加了两个注解:@Options
和 @ResultType
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
// 方式一 多次获取,一次多行
@Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000000)
Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
// 方式二 一次获取,一次一行
@Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
@ResultType(BigDataSearchEntity.class)
void listData(@Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper, ResultHandler<BigDataSearchEntity> handler);
}
@Options说明
- ResultSet.FORWORD_ONLY:结果集的游标只能向下滚动
- ResultSet.SCROLL_INSENSITIVE:结果集的游标可以上下移动,当数据库变化时,当前结果集不变
- ResultSet.SCROLL_SENSITIVE:返回可滚动的结果集,当数据库变化时,当前结果集同步改变
- fetchSize:每次获取量
@ResultType说明
@ResultType(BigDataSearchEntity.class):转换成返回实体类型
注意:虽然上面的代码中都有 @Options
但实际操作却有不同:
- 方式一是多次查询,一次返回多条;
- 方式二是一次查询,一次返回一条;
原因说明
在Oracle中,查询时会一次性从服务器取出指定的fetch size条记录放在客户端,客户端处理完一个批次后再向服务器取下一个批次,直到所有数据处理完成。
而在MySQL中,当执行ResultSet.next()方法时,会通过数据库连接逐条返回数据。在数据发送到网络缓冲区的过程中,如果发生了网络拥塞,发送缓冲区被填满,会导致缓冲区无法及时刷新,此时MySQL的处理线程会被阻塞,从而避免数据将客户端内存撑爆。这种阻塞式的处理方式可以确保数据的稳定传输。
非流式查询和流式查询区别:
在非流式查询中,随着查询记录的增长,内存使用量会近乎直线增长。
而在流式查询中,内存使用量会保持稳定,不会随着记录的增长而增长。实际上,内存的大小取决于批处理大小(BATCH_SIZE)的设置。如果批处理大小设置得较大,那么内存使用量也会相应增加。因此,我们应该根据具体的业务情况来设置合适的BATCH_SIZE大小。
另外,我们要记住,在处理完一批结果后,需要释放存储每批数据的临时容器,即上文中提到的gxids.clear()。这样可以确保及时释放内存,避免内存泄漏的问题。
总结
以上就是MyBatis Plus 解决百万级大数据量查询慢问题所使用的方式方法,希望对你有帮助!