MySQL 读取百万数据进行处理,该怎么做?前后端交互百万数据需要考虑什么问题?

背景:

大数据量操作的场景大致如下:

  1. 数据迁移
  2. 数据导出
  3. 批量数据处理
    在实际工作当中当指定查询数据过大时,我们一般会使用分页查询的方式一页一页的将数据存放到内存处理。但是有些情况不需要分页的方式查询或者分页查询的size很大时,很可能会导致内存溢出OOM的情况,而且数据库查询数据时,速度会很慢,因为首先要通过SQL去数据库中获取数据,这个过程受到IO,操作系统硬件等性能的影响,同时将获取到的数据重新封装成一个poij对象,也耗费时间。

问题描述:

在业务系统中需要从MySQL数据库中读取百万数据进行处理,应该怎么调优?

常规查询:一次性读取百万数据到jvm内存中,或者分页查询

默认情况下,完整的检索结果集会将其存储在内存中。在大多数情况下,这是最有效的操作方式,并且由于 MySQL 网络协议的设计,因此更易于实现

@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 深分页优化情况下,估计你的数据库服务器就嗝屁了,或者你能等上几十分钟或几小时,甚至几天时间检索数据

流式查询:建立长连接,利用服务端游标,每次读取一条加载到jvm内存(多次获取,一次一行)

流式查询指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。

如果没有流式查询,我们想要从数据库取 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参数,控制一次读取多少条数据(多次获取,一次多行)

对大量数据进行处理时,为防止内存泄漏情况发生,也可以采用游标方式进行数据查询处理。这种处理方式比常规查询要快很多。

当查询百万级的数据的时候,还可以使用游标方式进行数据查询处理,不仅可以节省内存的消耗,而且还不需要一次性取出所有数据,可以进行逐条处理或逐条取出部分批量处理。一次查询指定 fetchSize 的数据,直到把数据全部处理完。

Mybatis 的处理加了两个注解:@Options 和 @ResultType
@Mapper
public interface BigDataSearchMapper extends BaseMapper {

// 方式一 多次获取,一次多行
@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 但实际操作却有不同:

方式一是多次查询,一次返回多条;
方式二是一次查询,一次返回一条;

非流式查询和流式查询区别:

非流式查询:内存会随着查询记录的增长而近乎直线增长。
流式查询:内存会保持稳定,不会随着记录的增长而增长。其内存大小取决于批处理大小BATCH_SIZE的设置,该尺寸越大,内存会越大。所以BATCH_SIZE应该根据业务情况设置合适的大小。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值