MySQL数据查询太多会OOM吗?

线上 MySQL 直接 Select 千万条的100G数据,服务器会裂开吗?

假设对某100G表t执行全表扫描,把扫描结果保存在客户端:

# 该语句无任何判断条件,所以全表扫描,查到的每行都可直接放到结果集,然后返给客户端
mysql -h$host -P$port -u$user -p$pwd -e 
	"select * from t" > $target_file

那这“结果集”存在哪的?

服务端其实无需保存一个完整结果集,实际上MySQL取、发数据流程如下:

  1. 获取一行,写到net_buffer

    该内存大小由参数net_buffer_length定义,默认16k

  2. 继续获取行,直到写满net_buffer,然后发出去

  3. 若发送成功,则清空net_buffer,继续读取下一行,并写入net_buffer

  4. 若发送函数返回EAGAINWSAEWOULDBLOCK,表示本地网络栈(socket send buffer)写满,进入等待。直到网络栈重新可写,再继续发送

以上过程执行流程图如下:

可以看出:

  • 一个查询在发送过程中,占用MySQL内部的内存最大就是net_buffer_length,根本达不到100G
  • 同理,socket send buffer 也不会达到100G(默认定义/proc/sys/net/core/wmem_default),若socket send buffer被写满,就会暂停读数据的流程

所以MySQL其实是边读取边发送,若客户端接收得比较慢,会导致MySQL Server由于结果发不出去,该事务的执行时间就会变得很长。如下状态,即当Client不读socket receive buffer内容时,在Server端show processlist的结果 — 服务端发送阻塞:

IdUserHostDbCommandTimeStateInfo
11rootLocalhost:64001TestQuery0Startingshow processlist
12rootlocalhost:64003TestQuery10sending to clientselect * from citizen

若State一直“Sending to client”,说明Server端网络栈满。若客户端使用–quick参数,会使用mysql_use_result:读一行处理一行。假设某业务较复杂,每读一行数据后要处理的逻辑很慢,就会导致客户端要过很久才能读取下一行数据,可能就会出现上表的结果。

正常的线上业务,若一个查询的返回结果不多,推荐使用mysql_store_result接口,直接把查询结果保存到本地内存。这前提是查询返回结果不多,若太多,因执行一个大查询,导致客户端占用内存近20G,这时就需改用mysql_use_result接口。

若你看到MySQL有很多线程处于“Sending to client”,就得优化业务的查询结果了。若想快速减少处于该状态下的线程,可将net_buffer_length设大些。

有时会看到很多查询语句的state是“Sending data”,这又是为啥?我们得知晓一个查询语句的状态变化:

  1. MySQL查询语句进入执行阶段后,先把状态置为 Sending data
  2. 然后,发送执行结果的列相关的信息(meta data) 给客户端(Sending data)
  3. 再继续执行语句的流程(Sending data)
  4. 执行完成后,把状态置成空串(“”)

所以“Sending data”并不一定指“正在发送数据”,可能是处于执行器过程中的任意阶段。可构造锁等待场景—读全表被锁:

session1session2
begin
select * from citizen where id=1 for update;
select * from citizen lock in share mode;
(blocked)

执行show processlist:

IdUserHostDbCommandTimeStateInfo
11rootLocalhost:64001TestSleep59StartingNull
12rootlocalhost:64003TestQuery3Sending dataselect * from citizen lock in share mode
13rootLocalhost:64004TestQuery0Startingshow processlist

可见session2在等锁,state为Sending data,即

  • 仅当一个线程处“等待客户端接收结果”的状态,才会显示"Sending to client"
  • 若显示成“Sending data”,只是表明“正在执行”

所以,查询结果是分段发给客户端的,因此扫描全表,即使查询返回大量数据,也不会把内存搞满。

以上都是server层处理逻辑,InnoDB引擎层又是如何处理的呢?

InnoDB如何处理全表扫描?

InnoDB内存的一个作用,是保存更新的结果,再配合redo log,避免随机写盘。

内存的数据页是在Buffer Pool (简称为BP)管理,在WAL里BP起加速更新的作用。
BP还能加速查询。

  • 由于WAL,当事务提交时,磁盘上的数据页是旧的,若这时马上有个查询来读该数据页,是不是要马上把redo log应用到数据页?
    不需要。因为此时,内存数据页的结果是最新的,直接读内存页即可。这时查询无需读磁盘,直接从内存取结果,速度很快。所以,Buffer Pool能加速查询。

而BP对查询的加速效果,依赖于一个重要的指标,即:内存命中率。
可以在show engine innodb status结果中,查看一个系统当前的BP命中率。一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在99%以上。

执行show engine innodb status ,可以看到“Buffer pool hit rate”字样,显示的就是当前的命中率。比如下图命中率,就是100%。

若所有查询需要的数据页都能够直接从内存得到,那是最好的,对应命中率100%。

InnoDB Buffer Pool的大小是由参数 innodb_buffer_pool_size确定,一般建议设置成可用物理内存的60%~80%。

在大约十年前,单机的数据量是上百个G,而物理内存是几个G;现在虽然很多服务器都能有128G甚至更高的内存,但是单机的数据量却达到了T级别。

所以,innodb_buffer_pool_size小于磁盘数据量很常见。若一个 Buffer Pool满了,而又要从磁盘读入一个数据页,那肯定是要淘汰一个旧数据页的。

InnoDB内存管理

使用的最近最少使用 (Least Recently Used, LRU)算法,淘汰最久未使用数据。

  • 基本LRU算法
    TODO

InnoDB管理BP的LRU算法,是用链表实现的:

  • state1,链表头部是P1,表示P1是最近刚被访问过的数据页
  • 此时,一个读请求访问P3,因此变成状态2,P3被移到最前
  • 状态3表示,这次访问的数据页不存在于链表,所以需要在BP中新申请一个数据页Px,加到链表头。但由于内存已满,不能申请新内存。于是清空链表末尾Pm数据页内存,存入Px的内容,放到链表头部

最终就是最久没有被访问的数据页Pm被淘汰。
若此时要做一个全表扫描,会咋样?若要扫描一个200G的表,而这个表是一个历史数据表,平时没有业务访问它。

那么,按此算法扫描,就会把当前BP里的数据全部淘汰,存入扫描过程中访问到的数据页的内容。也就是说BP里主要放的是这个历史数据表的数据。

对于一个正在做业务服务的库,这可不行呀。你会看到,BP内存命中率急剧下降,磁盘压力增加,SQL语句响应变慢。

所以,InnoDB不能直接使用原始的LRU。InnoDB对其进行了优化。

  • 改进的LRU算法

InnoDB按5:3比例把链表分成New区和Old区。图中LRU_old指向的就是old区域的第一个位置,是整个链表的5/8处。即靠近链表头部的5/8是New区域,靠近链表尾部的3/8是old区域。

改进后的LRU算法执行流程:

  1. 状态1,要访问P3,由于P3在New区,和优化前LRU一样,将其移到链表头部 =》状态2
  2. 之后要访问一个新的不存在于当前链表的数据页,这时依然是淘汰掉数据页Pm,但新插入的数据页Px,是放在LRU_old
  3. 处于old区的数据页,每次被访问的时候都要做如下判断:
    • 若该数据页在LRU链表中存在的时间超过1s,就把它移动到链表头部
    • 若该数据页在LRU链表中存在的时间短于1s,位置保持不变。1s是由参数innodb_old_blocks_time控制,默认值1000,单位ms。

该策略,就是为了处理类似全表扫描的操作量身定制。还是扫描200G历史数据表:
4. 扫描过程中,需要新插入的数据页,都被放到old区域
5. 一个数据页里面有多条记录,这个数据页会被多次访问到,但由于是顺序扫描,这个数据页第一次被访问和最后一次被访问的时间间隔不会超过1秒,因此还是会被保留在old区域
6. 再继续扫描后续的数据,之前的这个数据页之后也不会再被访问到,于是始终没有机会移到链表头部(New区),很快就会被淘汰出去。

可以看到,这个策略最大的收益,就是在扫描这个大表的过程中,虽然也用到了BP,但对young区完全没有影响,从而保证了Buffer Pool响应正常业务的查询命中率。

总结

MySQL采用的是边算边发的逻辑,因此对于数据量很大的查询结果来说,不会在server端保存完整的结果集。所以,如果客户端读结果不及时,会堵住MySQL的查询过程,但是不会把内存打爆。

而对于InnoDB引擎内部,由于有淘汰策略,大查询也不会导致内存暴涨。并且,由于InnoDB对LRU算法做了改进,冷数据的全表扫描,对Buffer Pool的影响也能做到可控。

全表扫描还是比较耗费IO资源的,所以业务高峰期还是不能直接在线上主库执行全表扫描的。

参考

  • 《MySQL实战45讲》
<think>好的,我现在需要帮用户解决一个关于Java处理大规模数据(500万条)的分批查询和更新,同时避免OOM的问题。用户提到了分页、cid字段以及批量操作,可能还涉及条件查询。首先,我得理清楚整个流程。 首先,用户需要处理500万条数据,这样的数据量如果一次性加载到内存肯定会OOM,所以必须分批次处理。分页查询是常见的做法,但传统分页(使用LIMIT offset, size)在大数据量下效率会很低,尤其是当offset很大时。比如,当用户查询第100万页的时候,数据库需要扫描前面的所有数据,这会非常慢。因此,应该采用基于游标的分页方式,也就是利用cid(假设是自增主键或者有序的字段)来分批获取数据,每次记录最后处理的cid,下一次查询从该cid之后开始。这样可以避免offset的性能问题。 接下来,关于批量更新,需要使用JDBC的批处理功能,比如addBatch()和executeBatch(),这样可以减少网络传输次数,提高效率。同时,合理设置批处理的大小,比如每1000条提交一次,避免单个事务太大导致内存问题或者数据库锁竞争。 然后,考虑到事务管理,如果每条批处理作为一个独立事务,可能会影响性能,但可以防止长时间事务导致的锁问题。不过,用户可能需要根据业务需求调整提交频率,比如每批处理提交一次,或者在处理完成后整体提交。 另外,代码中需要关闭资源,防止连接泄漏。使用try-with-resources来自动关闭Connection、PreparedStatement和ResultSet是个好习惯。 还需要注意,如果cid字段不是连续的,可能会有间隙,这时候需要确保分页逻辑正确。另外,如果数据在分批处理过程中被修改,可能会导致漏处理或者重复处理,所以最好在业务低峰期进行,或者有相应的机制处理这种情况。 用户可能遇到的另一个问题是查询条件,如果有其他条件,需要在WHERE子句中包含,并且确保cid的条件正确应用。比如,查询条件应该是WHERE cid > ? AND [其他条件],这样保证每次只获取下一批数据。 最后,关于OOM的避免,除了分页之外,还需要确保每批处理的数据量适中,不会占用过多内存。例如,设置合适的fetchSize,让数据库流式返回结果,而不是一次性加载到内存。不过,这可能需要数据库驱动的支持。 现在,我需要将这些思路转化为具体的代码示例,确保分页正确,使用批处理,资源正确关闭,以及处理性能问题。同时,代码中需要包含注释,解释关键步骤,帮助用户理解。</think>针对Java处理500万条数据的批量查询与更新需求,核心解决思路是**分页批处理+游标优化+JDBC批处理**。以下是完整实现方案: --- ### 一、分页查询优化方案(避免传统LIMIT性能问题) ```java public void batchProcess(String condition, int batchSize) throws SQLException { Long lastProcessedCid = 0L; // 初始化游标 try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交 while (true) { // 使用游标分页(关键点) String query = "SELECT cid, other_columns FROM large_table " + "WHERE cid > ? AND " + condition + " ORDER BY cid LIMIT ?"; try (PreparedStatement pstmt = conn.prepareStatement(query)) { pstmt.setLong(1, lastProcessedCid); pstmt.setInt(2, batchSize); ResultSet rs = pstmt.executeQuery(); List<Long> cidList = new ArrayList<>(); while (rs.next()) { Long currentCid = rs.getLong("cid"); // 处理业务逻辑... cidList.add(currentCid); lastProcessedCid = currentCid; // 更新游标 } if (cidList.isEmpty()) break; // 终止条件 // 批量更新(示例) batchUpdate(conn, cidList); } conn.commit(); // 每批次提交一次 } } } ``` --- ### 二、批量更新防OOM方案 ```java private void batchUpdate(Connection conn, List<Long> cidList) throws SQLException { String updateSql = "UPDATE large_table SET status = ? WHERE cid = ?"; try (PreparedStatement pstmt = conn.prepareStatement(updateSql)) { int count = 0; for (Long cid : cidList) { pstmt.setString(1, "processed"); pstmt.setLong(2, cid); pstmt.addBatch(); // 加入批处理 if (++count % 1000 == 0) { // 每1000条执行一次 pstmt.executeBatch(); pstmt.clearBatch(); } } pstmt.executeBatch(); // 处理剩余数据 } } ``` --- ### 三、关键技术点说明 1. **游标分页优化** 使用`WHERE cid > ?`替代`LIMIT offset`,避免深分页性能问题[^1]。假设cid是自增主键或有序字段。 2. **批处理配置建议** - 批处理大小建议1000-5000条/次 - 配合连接池配置:`rewriteBatchedStatements=true`(MySQL批处理优化参数) 3. **事务控制** ```java conn.setAutoCommit(false); // 手动控制事务 // 每批次处理完成后显式提交 conn.commit(); ``` 4. **内存控制** - 使用分页限制每批次加载数据量 - 及时清除已处理对象的引用 - 添加JVM参数:`-XX:+UseG1GC -XX:MaxGCPauseMillis=200` --- ### 四、扩展优化建议 1. **多线程批处理** 可拆分为多个游标范围并行处理: ```java // 示例分片逻辑 Long maxCid = getMaxCid(); int threads = 10; Long step = maxCid / threads; ExecutorService executor = Executors.newFixedThreadPool(threads); ``` 2. **连接池配置** 建议使用HikariCP配置: ```properties maximumPoolSize=20 connectionTimeout=30000 idleTimeout=600000 ``` ---
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值