大数据量操作一般用在数据迁移,数据导出,批量处理数据
在实际工作中当中,查询数据过大,我们使用分页查询的方式一页一页的将数据放到内存处理。但有些情况不需要分页的方式查询数据或者分很大一页查询数据时,如果一下子将数据全部加载出来到内存中,很可能会发生OOM(内存溢出),而且查询会很慢,大量的时间和内存耗费在把数据库查询的结果封装成我们想要的对象。
问题:系统需要从mysql数据库里读取100w数据进行处理,需要怎么做?
- 常规查询:一次性读取100w数据到jvm内存中,或者分页读取
- 流式查询:建立长连接,利用服务端游标,每次读取一条加载到jvm内存(多次获取,一次一行)
- 游标查询:和流式一样,通过fetchSize参数,控制一次读取多少条数据(多次获取,一次多行)
常规查询
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
@Select("select * from big_data ${ew.customSqlSegment}")
Page<BigDataSearchEntity> pageList(@Param("page) Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BIgDataSearchEntity> queryWrapper);
}
如果不考虑limit深分页优化的情况下,或者等上几十分钟或几小时
流式查询
Mybatis提供一个叫org.apache.ibatis.cursor.Cursor的接口类用于流式查询,这个接口继承了java.io.Closeable和java.lang.Iterable接口,所以Cursor是可关闭,可遍历的。Cursor提供了三个方法:
- isOpen():用于在取数据之前判断Cursor对象是否是打开状态,只有当打开时Cursor才能取数据
- isConsumed():用于判断查询结果是否全部取完
- getCurrentIndex():返回已经获取了多少条数据
使用流式查询,则要保持对产生结果集的语句所引用的表的并发访问,因为其查询会独占连接
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
// 一次获取,一次一行
@Select("select * from big_data ${ew.customSqlSegment} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
@ResultType(BigDataSearchEntity.class)
void listData(@Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper, ResultHandler<BigDataSearchEntity> handler);
}
如果有一个很大的查询结果需要遍历处理,又不想一次性将结果集装入客户端内存,就可以考虑使用流式查询;分库分表场景下,单个表的查询结果集虽然不大,但如果某个查询跨了多个库多个表,又要做结果集的合并、排序等动作,依然有可能撑爆内存;详细研究sharding-sphere的代码不难发现,除了group by 与order by字段不一样之外,其它的场景都非常适合使用流式查询,可以最大限度的降低对客户端内存的消耗。
游标查询
对大量数据进行处理时,可以采用游标方式进行数据查询处理。不仅可以节省内存的消耗,而且还不需要一次性取出所有数据,可以进行逐条处理或逐条取出部分批量处理。一次查询指定 fetchSize 的数据,直到把数据全部处理完。
Mybatis 的处理加了两个注解:@Options和@ResultType
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
// 多次获取,一次多行
@Select("select * from big_data ${ew.customSqlSegment} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000000)
Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
}
@Options
- ResultSet.FORWORD_ONLY:结果集的游标只能向下滚动
- ResultSet.SCROLL_INSENSITIVE:结果集的游标可以上下移动,当数据库变化时,当前结果集不变
- ResultSet.SCROLL_SENSITIVE:返回可滚动的结果集,当数据库变化时,当前结果集同步改变
- fetchSize:每次获取量
@ResultType
- @ResultType(BigDataSearchEntity.class):转换成返回实体类型(注意:返回类型必须为 void ,因为查询的结果在 ResultHandler 里处理数据,所以这个 hander 也是必须的,可以使用 lambda 实现一个依次处理逻辑)
Oracle 是从服务器一次取出 fetch size 条记录放在客户端,客户端处理完成一个批次后再向服务器取下一个批次,直到所有数据处理完成。
MySQL 是在执行 ResultSet.next() 方法时,会通过数据库连接一条一条的返回。flush buffer 的过程是阻塞式的,如果网络中发生了拥塞,send buffer 被填满,会导致 buffer 一直 flush 不出去,那 MySQL 的处理线程会阻塞,从而避免数据把客户端内存撑爆。
另外要切记每次处理完一批结果要记得释放存储每批数据的临时容器。