推广

面试官:如何通过 MyBatis 查询千万数据并保证内存不溢出?

iseeyu2年前 (2024-02-21)推广131

image

默认情况下 ResultSet 会使用 RowDataStatic 实例,在生成 RowDataStatic 对象时就会把 ResultSet 中所有记录读到内存里,之后通过 next() 再一条条从内存中读

RowDataCursor 的调用为批处理,然后进行内部缓存,流程如下:

  • 首先会查看自己内部缓冲区是否有数据没有返回,如果有则返回下一行
  • 如果都读取完毕,向 MySQL Server 触发一个新的请求读取 fetchSize 数量结果
  • 并将返回结果缓冲到内部缓冲区,然后返回第一行数据

当采用流式处理时,ResultSet 使用的是 RowDataDynamic 对象,而这个对象 next() 每次调用都会发起 IO 读取单行数据

总结来说就是,默认的 RowDataStatic 读取全部数据到客户端内存中,也就是我们的 JVM;RowDataCursor 一次读取 fetchSize 行,消费完成再发起请求调用;RowDataDynamic 每次 IO 调用读取一条数据

1.5 JDBC 通信原理

(1)普通查询

在 JDBC 与 MySQL 服务端的交互是通过 Socket 完成的,对应到网络编程,可以把 MySQL 当作一个 SocketServer,因此一个完整的请求链路应该是:

JDBC 客户端 -> 客户端 Socket -> MySQL -> 检索数据返回 -> MySQL 内核 Socket 缓冲区 -> 网络 -> 客户端 Socket Buffer -> JDBC 客户端

普通查询的方式在查询大数据量时,所在 JVM 可能会凉凉,原因如下:

  • MySQL Server 会将检索出的 SQL 结果集通过输出流写入到内核对应的 Socket Buffer
  • 内核缓冲区通过 JDBC 发起的 TCP 链路进行回传数据,此时数据会先进入 JDBC 客户端所在内核缓冲区
  • JDBC 发起 SQL 操作后,程序会被阻塞在输入流的 read 操作上,当缓冲区有数据时,程序会被唤醒进而将缓冲区数据读取到 JVM 内存中
  • MySQL Server 会不断发送数据,JDBC 不断读取缓冲区数据到 Java 内存中,虽然此时数据已到 JDBC 所在程序本地,但是 JDBC 还没有对 execute 方法调用处进行响应,因为需要等到对应数据读取完毕才会返回
  • 弊端就显而易见了,如果查询数据量过大,会不断经历 GC,然后就是内存溢出

(2)游标查询

通过上文得知,游标可以解决普通查询大数据量的内存溢出题,但是

小伙伴有没有思考过这么一个问题,MySQL 不知道客户端程序何时消费完成,此时另一连接对该表造成 DML 写入操作应该如何处理?

其实,在我们使用游标查询时,MySQL 需要建立一个临时空间来存放需要被读取的数据,所以不会和 DML 写入操作产生冲突

但是游标查询会引发以下现象:

  • IOPS 飙升,因为需要返回的数据需要写入到临时空间中,存在大量的 IO 读取和写入,此流程可能会引起其它业务的写入抖动
  • 磁盘空间飙升,因为写入临时空间的数据是在原表之外的,如果表数据过大,极端情况下可能会导致数据库磁盘写满,这时网络输出时没有变化的。而写入临时空间的数据会在 读取完成或客户端发起 ResultSet#close 操作时由 MySQL 回收
  • 客户端 JDBC 发起 SQL 查询,可能会有长时间等待 SQL 响应,这段时间为服务端准备数据阶段。但是 普通查询等待时间与游标查询等待时间原理上是不一致的,前者是一致在读取网络缓冲区的数据,没有响应到业务层面;后者是 MySQL 在准备临时数据空间,没有响应到 JDBC
  • 数据准备完成后,进行到传输数据阶段,网络响应开始飙升,IOPS 由”读写”转变为”读取”

采用游标查询的方式 通信效率比较低,因为客户端消费完 fetchSize 行数据,就需要发起请求到服务端请求,在数据库前期准备阶段 IOPS 会非常高,占用大量的磁盘空间以及性能

(3)流式查询

当客户端与 MySQL Server 端建立起连接并且交互查询时,MySQL Server 会通过输出流将 SQL 结果集返回输出,也就是 向本地的内核对应的 Socket Buffer 中写入数据,然后将内核中的数据通过 TCP 链路回传数据到 JDBC 对应的服务器内核缓冲区

  • JDBC 通过输入流 read 方法去读取内核缓冲区数据,因为开启了流式读取,每次业务程序接收到的数据只有一条
  • MySQL 服务端会向 JDBC 代表的客户端内核源源不断地输送数据,直到客户端请求 Socket 缓冲区满,这时的 MySQL 服务端会阻塞
  • 对于 JDBC 客户端而言,数据每次读取都是从本机器的内核缓冲区,所以性能会更快一些,一般情况不必担心本机内核无数据消费(除非 MySQL 服务端传递来的数据,在客户端不做任何业务逻辑,拿到数据直接放弃,会发生客户端消费比服务端超前的情况)

看起来,流式要比游标的方式更好一些,但是事情往往不像表面上那么简单

  • 相对于游标查询,流式对数据库的影响时间要更长一些
  • 另外流式查询依赖网络,导致网络拥塞可能性较大

02 流式游标内存分析


表数据量:500w

内存查看工具:JDK 自带 Jvisualvm

设置 JVM 参数: -Xmx512m -Xms512m

2.1 单次调用内存使用

流式查询内存性能报告如下

image

游标查询内存性能报告如下

image

根据内存占用情况来看,游标查询和流式查询都 能够很好地防止 OOM

2.2 并发调用内存使用

并发调用:Jmete 1 秒 10 个线程并发调用

流式查询内存性能报告如下

image

并发调用对于内存占用情况也很 OK,不存在叠加式增加

流式查询并发调用时间平均消耗:≈ 55s

游标查询内存性能报告如下

image

游标查询并发调用时间平均消耗:≈ 83s

因为设备限制,以及部分情况只会在极端下产生,所以没有进行生产、测试多环境验证,小伙伴感兴趣可以自行测试

03 MyBatis 如何使用流式查询


上文都是在描述如何使用 JDBC 原生 API 进行查询,ORM 框架 Mybatis 也针对流式查询进行了封装

ResultHandler 接口只包含 handleResult 方法,可以获取到已转换后的 Java 实体类

@Slf4j
@Service
public class MyBatisStreamService {
    @Resource
    private MyBatisStreamMapper myBatisStreamMapper;

    public void mybatisStreamQuery() {
        long start = System.currentTimeMillis();
        myBatisStreamMapper.mybatisStreamQuery(new ResultHandler<YOU_TABLE_DO>() {
            @Override
            public void handleResult(ResultContext<? extends YOU_TABLE_DO> resultContext) { }
        });
        log.info("   MyBatis查询耗时 :: {} ", System.currentTimeMillis() - start);
    }
}

除了下述注解式的应用方式,也可以使用 .xml 文件的形式

@Mapper
public interface MyBatisStreamMapper {
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
    @ResultType(YOU_TABLE_DO.class)
    @Select("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE")
    void mybatisStreamQuery(ResultHandler<YOU_TABLE_DO> handler);
}

Mybatis 流式查询调用时间消耗:≈ 18s

JDBC 流式与 MyBatis 封装的流式读取对比

  • MyBatis 相对于原生的流式还是慢上了不少,但是考虑到底层的封装的特性,这点性能还是可以接受的
  • 从内存占比而言,两者波动相差无几
  • MyBatis 相对于原生 JDBC 更为的方便,因为封装了回调函数以及序列化对象等特性

两者具体的使用,可以针对项目实际情况而定,没有最好的,只有最适合的

结言

流式查询、游标查询可以避免 OOM,数据量大可以考虑此方案。但是这两种方式会占用数据库连接,使用中不会释放,所以线上针对大数据量业务用到游标和流式操作,一定要进行并发控制

另外针对 JDBC 原生流式查询,Mybatis 中也进行了封装,虽然会慢一些,但是 功能以及代码的整洁程度会好上不少

作者:龙台的技术笔记
原文链接:
https://blog.csdn.net/qq_37781649/article/details/112169908

扫描二维码推送至手机访问。

版权声明:本文由西安泽虎代运营发布,如需转载请注明出处。

转载请注明出处https://www.0291.com.cn/post/57504.html

相关文章

或将放弃展示播放量,B站要回归内容本心

如何衡量一个视频在网络中的热度,播放量无疑是个简单且直接的指标。然而在国内互联网行业,“播放量”却被各大视频平台弃之如履,在长视频平台爱奇艺、优酷、腾讯视频,以及短视频平台抖音、快手、视频号之后,日前有传言称哔哩哔哩可能也要放弃在前台显示播放量了。据悉在3月10日,B站在上...

萃弈:北美手游市场品牌出海增长白皮书。

萃弈:北美手游市场品牌出海增长白皮书。

上海– 2022年8月9日–全球领先的广告科技公司萃弈(The Trade Desk)联合调研公司NielsenIQBASES游戏今日共同发布《北美手游市场品牌出海增长白皮书》(以下简称"白皮书")。基于近期相关调研,白皮书显示,虽然中国手游厂商在北美地区表现出色,但如何在数字世界和北美手游消费群体...

搜索引擎SEO优化普遍的5个误区。

搜索引擎SEO优化普遍的5个误区。

针对一切1个seo优化工作人员来讲,全是会有个成才的全过程,尤其做公司引擎搜索SEO优化的那时候,一直走某些弯道,这让刚新员工入职的SEO工作人员,十分烦恼。乃至本质不清晰自身哪些地方不太好,而且在长期性持续这类不正确,而当你做为SEO组织确诊的那时候,通常徒添十分多的艰难。接下去小编就跟大伙儿共...

从0到1玩转抖音短视频推广运营 !

从0到1玩转抖音短视频推广运营 !

  今天主要分享有5大板块的内容: 一、抖音的推荐机制算法揭秘 1、去中心化 微信只要关注才可以看到内容,私人微信号要加了好友才可以看到。 而抖音是一个去中心化的平台,都是机器算法的,针对内容的优质而给对应的流量 2、每个账号都有新手期 全部账号的新的账号都有一个新...

自己怎么在网上开店(入驻淘宝开店步骤)

自己怎么在网上开店(入驻淘宝开店步骤)

  新手怎么在网上开网店,第一个环节一定是选择一个好的产品,只有选择对了符合自身情况的产品才能继续往下走。选择的产品可以是现实中的实物产品,也可以是网络上的虚拟产品,而如果自己没有对应的产品也可以考虑做无货源的电商,打包发货让上一环负责。      选好产品之后接下来就要选择一个合适的平台,...

439互联网大平台营销通案48份

本次收录方案关键司推介:【互联网】商家指南、开户、资源介绍、广告样式、广告产品手册、、投放、营销策略、行业营销、整合营销、营销通案等;包括:腾讯、百度、搜狐、微信、知乎、美图、美团、虎牙、抖音、抖音巨量引擎、快手、小红书、B站、Soul、下厨房、宝宝树、去哪儿、懂车帝、星图...

现在,非常期待与您的又一次邂逅

我们努力让每一部企业宣传片和抖音短视频成为商业大片