背景
大数据量操作的场景大抵如下:
- 数据迁徙
- 数据导出
- 批量解决数据
在理论工作中当指定查问数据过大时,咱们个别应用分页查问的形式一页一页的将数据放到内存解决。但有些状况不须要分页的形式查问数据或分很大一页查问数据时,如果一下子将数据全副加载进去到内存中,很可能会产生 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);
}
注:该示例应用的 MybatisPlus
该形式比较简单,如果在不思考 LIMIT 深分页优化状况下,预计你的数据库服务器就噶皮了,或者你能等上几十分钟或几小时,甚至几天工夫检索数据。
举荐一个开源收费的 Spring Boot 最全教程:
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 是可遍历的。
除此之外,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)
:转换成返回实体类型
留神:返回类型必须为 void,因为查问的后果在
ResultHandler
里解决数据,所以这个 hander 也是必须的,能够应用 lambda 实现一个顺次解决逻辑。
留神:
尽管下面的代码中都有 @Options
但实际操作却有不同:
- 形式一是屡次查问,一次返回多条;
- 形式二是一次查问,一次返回一条;
起因:
Oracle 是从服务器一次取出 fetch size
条记录放在客户端,客户端解决实现一个批次后再向服务器取下一个批次,直到所有数据处理实现。
MySQL 是在执行 ResultSet.next()
办法时,会通过数据库连贯一条一条的返回。flush buffer
的过程是阻塞式的,如果网络中产生了拥塞,send buffer
被填满,会导致 buffer 始终 flush 不进来,那 MySQL 的解决线程会阻塞,从而防止数据把客户端内存撑爆。
非流式查问和流式查问区别:
- 非流式查问:内存会随着查问记录的增长而近乎直线增长。
- 流式查问:内存会保持稳定,不会随着记录的增长而增长。其内存大小取决于批处理大小
BATCH_SIZE
的设置,该尺寸越大,内存会越大。所以 BATCH_SIZE 应该依据业务状况设置适合的大小。
另外要切记每次解决完一批后果要记得开释存储每批数据的长期容器,即上文中的gxids.clear()
;
版权申明:本文为 CSDN 博主「旷野历程」的原创文章,遵循 CC 4.0 BY-SA 版权协定,转载请附上原文出处链接及本申明。原文链接:https://blog.csdn.net/xhaimail/article/details/119386460
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)
2. 劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!