背??景大数据量操作的场景大致如下:数据迁移数据导出批处理数据在实际工作中,当指定查询数据过大时,我们一般采用逐页查询的方式将数据放入逐页处理内存。但是,在某些情况下,当不需要分页查询数据或大页查询数据时,如果一次性将所有数据加载到内存中,很可能会出现OOM(outofmemory);而且查询会很慢,因为框架需要大量的时间和内存来将数据库查询的结果封装成我们想要的对象(实体类)。示例:业务系统需要从MySQL数据库中读取100w行数据进行处理,应该怎么做?做法通常是这样的:常规查询:一次读取100w条数据到JVM内存中,或者分页读取流式查询:建立长连接,使用服务端游标,一次读取一条,加载进入JVM内存(多次获取,一次一行)游标查询:和流式一样,通过fetchSize参数,控制一次读取多少条数据(多次获取,一次多行)对于常规查询,通过默认情况下,完整的检索结果集将存储在内存中。在大多数情况下,这是最高效的操作方式,而且由于MySQL网络协议的设计,更容易实现。示例:假设单表数据量为100w,一般采用分页查询:@MapperpublicinterfaceBigDataSearchMapperextendsBaseMapper{@Select("SELECTbds.*FROMbig_data_searchbds${ew.customSqlSegment}")PagepageList(@Param("page")Page页面,@Param(Constants.WRAPPER)QueryWrapperqueryWrapper);}注意:本例中使用的MybatisPlus方法比较简单,如果不考虑LIMIT深度分页优化,有些情况估计你的数据库服务器已经死了,或者等个几十分钟或者几个小时,甚至几天检索数据。推荐一个开源免费的SpringBoot最全教程:https://github.com/javastacks/spring-boot-best-practice流式查询流式查询是指查询成功后返回一个迭代器而不是集合,应用每次从迭代器中取出一个查询结果。流式查询的好处是能够减少内存使用。如果没有流式查询,当我们想从数据库中取100w条记录但内存不够时,就不得不通过分页查询,而分页查询的效率取决于表的设计。如果设计不好,就无法进行高效的分页查询。因此,流式查询是数据库访问框架必备的功能。MyBatis中使用了流式查询来避免数据量过大导致的OOM,但是在流式查询的过程中,数据库连接是一直保持打开的,所以需要注意的是,执行一次流式查询之后,数据库访问框架是不负责的。数据库连接已关闭,应用程序需要在获取数据后自行关闭。在连接上发出任何其他查询之前,必须读取(或关闭)结果集中的所有行,否则将抛出异常。MyBatis流式查询接口MyBatis提供了一个接口类org.apache.ibatis.cursor.Cursor用于流式查询。该接口继承了java.io.Closeable和java.lang.Iterable接口,所以我们可以看到:Cursor是可关闭的;游标是可遍历的。此外,Cursor还提供了三个方法:isOpen():用于在取数据前判断Cursor对象是否打开。游标只有在打开时才能取数据;isConsumed():用于判断是否所有的查询结果都取完了。getCurrentIndex():返回获取了多少条数据。使用流式查询,必须保持对生成结果集的语句引用的表的并发访问,因为它的查询会独占连接,所以必须尽快处理。为什么要使用流式查询?如果查询结果比较大,需要遍历处理,又不想一次性将结果集加载到客户端内存中,可以考虑使用流式查询;在分库分表的场景下,虽然单表的查询结果集不大,但是,如果一个查询跨多库多表,需要对结果集进行归并排序,还是有可能爆的进入记忆;详细研究sharding-sphere的代码后不难发现,除了groupby和orderby字段之外,其他场景都非常适合使用流式查询,可以最大限度的减少客户端内存的消耗。当游标查询处理大量数据时,为了防止内存泄漏,也可以使用游标方法进行数据查询处理。这种类型的处理比常规查询快得多。在查询百万级数据时,也可以使用游标的方式进行数据查询处理,这样不仅节省了内存消耗,而且不需要一次把所有的数据都取下来,可以一个一个处理,也可以批量取一部分一一处理。一次查询指定fetchSize的数据,直到处理完所有数据。Mybatis的处理增加了两个注解:@Options和@ResultType@MapperpublicinterfaceBigDataSearchMapperextendsBaseMapper{//多次获取的方式,一次获取多行@Select("SELECTbds.*FROMbig_data_searchbds${ew.customSqlSegment}")@Options(resultSetType=ResultSetType.FORWARD_ONLY,fetchSize=1000000)PagepageList(@Param("page")Page页面,@Param(Constants.WRAPPER)QueryWrapper)//方法2:一次一行@Select("SELECTbds.*FROMbig_data_searchbds${ew.customSqlSegment}")@Options(resultSetType=ResultSetType.FORWARD_ONLY,fetchSize=100000)@ResultType(BigDataSearchEntity.class)voidlistData(@Param(Constants.WRAPPER)QueryWrapperqueryWrapper,ResultHandler处理程序);}@OptionsResultSet.FORWORD_ONLY:结果集的游标只能向下滚动ResultSet.SCROLL_INSENSITIVE:结果集的游标可以上下滚动Move,当数据库发生变化时,当前结果集不变ResultSet.SCROLL_SENSITIVE:返回可滚动的结果集,当数据库发生变化时,当前结果集同步变化fetchSize:每次fetch的量@ResultType@ResultType(BigDataSearchEntity.class):转换成返回实体类型注意:返回类型必须为void,因为查询结果在ResultHandler中处理数据,所以这个hander也是必须的,可以使用lambda实现一个顺序处理逻辑注意:虽然@Options中有上面的代码,实际的操作是不同的:第一种方式是多次查询,一次返回多条记录;第二种方法是查询一次,一次返回一条记录;原因:oracle每次从服务器取fetchsize的记录放到客户端,客户端完成一个batch再从服务器再取一个batch,直到所有数据处理完成。MySQL在执行ResultSet.next()方法时,会通过数据库连接一一返回。刷新缓冲区的过程是阻塞的。如果网络拥塞,发送缓冲区被填满,则不会一直刷新缓冲区,阻塞MySQL处理线程,防止数据爆客户端内存。非流式查询和流式查询的区别:非流式查询:内存会随着查询记录的增长几乎呈线性增长。流式查询:内存会保持稳定,不会随着记录的增长而增长。它的内存大小取决于batchsizeBATCH_SIZE的设置,size越大,内存越大。所以BATCH_SIZE应该根据业务情况设置合适的大小。另外,记得每处理完一批结果,就释放存放每批数据的临时容器,也就是上面的gxids.clear();版权声明:本文为CSDN博主“荒野之旅”原创文章,遵循CC4.0BY-SA版权协议,转载请附上原文出处链接及本声明。