MyBatis流式查询实战:解决大数据查询OOM问题

MyBatis流式查询实战:解决大数据查询OOM问题
30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度最近在做一个数据报表导出的功能业务方要求能导出近一年的订单明细。开发时图省事直接写了个SELECT * FROM orders WHERE create_time BETWEEN ...然后在 Service 层用ListOrder接收。本地测试时数据量小一切正常。结果上线后运营在后台一点“导出”服务内存直接飙到 90%紧接着就是一连串的OutOfMemoryError: Java heap space告警服务差点挂掉。事后复盘一张大表几百万条记录用List一次性加载到 JVM 内存中不 OOM 才怪。这种“一行代码挤爆内存”的坑很多后端开发都踩过。今天我们就来彻底解决这个问题反手祭出MyBatis 流式查询Streaming Query这把利器让你在处理海量数据时既能完成任务又能稳住内存。本文是上篇将深入讲解流式查询的原理、适用场景并给出完整的代码实战。本文适合正在或即将处理大量数据查询的 Java 后端开发者。遇到过查询导致 OOM 或 Full GC 问题的朋友。希望深入理解 MyBatis 高级特性的学习者。你将学到传统查询方式为何会导致 OOM。MyBatis 流式查询的核心原理与底层机制。三种实现流式查询的具体方法ResultHandler,Cursor,Options。每种方法的完整代码示例、配置要点和适用场景。流式查询的常见“坑点”与最佳实践。1. 背景与核心概念为什么需要流式查询1.1 传统查询的“内存炸弹”在典型的 MyBatis 查询中我们通常这样写// Mapper接口 ListOrder selectLargeOrders(Param(startTime) Date startTime, Param(endTime) Date endTime); // Service层调用 ListOrder orders orderMapper.selectLargeOrders(startTime, endTime); for (Order order : orders) { // 处理订单 }这段代码的逻辑非常清晰但隐患巨大。当selectLargeOrders执行时MyBatis通过 JDBC会做以下几件事执行 SQL从数据库获取所有结果。JDBC Driver 会将所有结果数据一次性加载到 JVM 堆内存中并封装成ResultSet。MyBatis 遍历这个ResultSet为每一行数据创建一个Order对象并添加到List中。最后这个包含了所有查询结果的ListOrder被返回。问题在于第2步和第3步。如果查询结果是 100 万条记录每条记录映射的Order对象占用 1KB 内存那么仅对象本身就需要约 1GB 的堆内存。这还没算上List内部数组扩容、JDBC 结果集缓存等开销。很容易就触发了 JVM 堆内存上限导致OutOfMemoryError。1.2 流式查询边读边处理的救星流式查询Streaming Query的核心思想是“按需获取逐条处理”。它改变了数据从数据库到应用内存的传输方式传统方式数据库 → (全部数据) → JDBC ResultSet → (全部数据) → MyBatis List → 应用处理流式方式数据库 → (一条/一批数据) → JDBC ResultSet → MyBatis 映射为对象 → 应用处理 → (处理完获取下一条)在流式查询中JDBC Driver 和数据库游标Cursor配合不会一次性将所有结果集数据都缓存到客户端内存。MyBatis 会逐行从ResultSet中读取数据映射成对象后立即交给开发者定义的回调函数处理或者通过一个“游标”接口让开发者手动控制遍历。处理完一行后该行数据所占用的内存就可以被释放或等待 GC。简单比喻传统查询像用桶从井里打水一桶水全部数据提上来再喝。流式查询像接上了水管打开水龙头开始遍历水数据流过来一点就喝掉处理一点不需要一个能装下整口井水的大桶。1.3 关键概念区分流式查询 vs 分页查询分页查询通过LIMIT offset, size或ROWNUM等数据库分页语法每次只查询一部分数据。它需要多次数据库交互且offset值很大时性能会下降。适用于前端分页展示的场景。流式查询一次查询建立一个持久的数据通道逐条流式传输。它只有一次数据库交互适合后端一次性处理全部数据的场景如数据导出、批量计算、ETL。游标Cursor数据库层面的一个概念用于标识和遍历结果集。流式查询依赖于数据库游标通常是“客户端游标”或“敏感游标”来保持结果集的状态以便逐条获取。OOMOutOfMemoryError当 JVM 堆内存不足以分配新对象时抛出的错误。流式查询是预防查询类 OOM 的有效手段之一。2. 环境准备与版本说明在开始编码之前请确保你的环境符合以下要求。本文的示例将基于最常见的环境组合。基础环境操作系统Windows / Linux / macOS 均可。JavaJDK 8 或更高版本本文示例使用 JDK 8 语法。构建工具Maven 或 Gradle本文使用 Maven 进行依赖管理。IDEIntelliJ IDEA、Eclipse 或 VS Code。核心依赖版本这是实现流式查询的关键。请检查你的pom.xml文件。properties mybatis.version3.5.10/mybatis.version !-- 如果使用 Spring Boot -- spring-boot.version2.7.18/spring-boot.version /properties dependencies !-- MyBatis 核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- 数据库驱动 (以 MySQL 为例) -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version !-- 注意对于流式查询MySQL驱动版本很关键推荐8.x -- /dependency !-- 如果使用 Spring Boot 集成 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency !-- 测试与工具 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version scopeprovided/scope /dependency dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency /dependencies版本要点说明MyBatis 版本3.5.x 及以上版本对流式查询的支持更完善。确保不要使用过于陈旧的版本。数据库驱动这是流式查询能否生效的重中之重。不同的数据库驱动其流式读取的默认行为和配置方式差异很大。MySQL必须使用com.mysql.cj.jdbc.result.ResultSetImpl支持的驱动即mysql-connector-java的8.x版本。5.x 版本驱动在某些配置下可能无法真正实现流式。PostgreSQL驱动默认支持流式但可能需要配置fetchSize。Oracle需要显式设置fetchSize并注意游标类型。Spring Boot如果你使用 Spring Bootmybatis-spring-boot-starter会自动配置很多 Bean但流式查询的一些特殊配置仍需手动处理。示例项目结构streaming-query-demo ├── src/main/java │ └── com.example.demo │ ├── entity │ │ └── Order.java # 订单实体类 │ ├── mapper │ │ └── OrderMapper.java # MyBatis Mapper接口 │ ├── handler │ │ └── OrderResultHandler.java # 结果处理器 │ └── DemoApplication.java # 启动类Spring Boot ├── src/main/resources │ ├── application.yml # Spring Boot 配置文件 │ └── mapper/OrderMapper.xml # MyBatis XML映射文件可选 └── pom.xml # Maven依赖3. 核心原理与配置拆解在深入代码之前理解 MyBatis 流式查询背后的工作原理和关键配置能帮你更好地使用和排查问题。3.1 JDBC 的fetchSize与游标流式查询的基石是 JDBC 的java.sql.Statement接口及其setFetchSize方法。fetchSize是什么它提示 JDBC 驱动每次从数据库网络连接中获取多少行数据到客户端的缓存中。默认值由驱动决定通常是“全部获取”。如何工作当设置fetchSize为一个较小的正数如 100、1000时JDBC 驱动会与数据库协商使用一个“可滚动”、“敏感”的游标。数据库会保持这个游标打开每次客户端请求数据时驱动通过网络传输fetchSize指定的行数而不是一次性传输所有数据。fetchSizeInteger.MIN_VALUE的特殊含义对于 MySQL 驱动这个特殊值会启用真正的“逐行流式”模式驱动会一行一行地从网络流中读取数据内存占用最小。但并非所有驱动都支持此值。在 MyBatis 中设置fetchSize你可以在 Mapper 方法上通过Options注解设置也可以在mybatis-config.xml全局配置或在 SQL 映射文件的select标签中设置。3.2 MyBatis 的三种流式查询方式MyBatis 提供了三种主要方式来实现流式处理它们底层都依赖于正确的 JDBCfetchSize配置。ResultHandler最原始、最灵活的方式。你需要实现org.apache.ibatis.session.ResultHandler接口。MyBatis 在遍历结果集时每映射完一个对象就调用一次你的handleResult方法。你完全控制处理逻辑但 MyBatis 不会返回任何结果列表给调用方。CursorTMyBatis 3.4.0 引入的更优雅的 API。Mapper 方法返回一个CursorT对象。你可以像使用Iterator一样遍历它 (cursor.forEach()或while (cursor.hasNext()))。遍历完成后Cursor 必须关闭以释放数据库资源。Options(resultSetType FORWARD_ONLY, fetchSize xxx)这是一种声明式的方式。通过注解配置结果集类型为仅向前 (FORWARD_ONLY) 并设置合适的fetchSize结合返回类型为List但实际由 MyBatis 内部优化有时也能达到类似流式的效果但控制力不如前两种。3.3 数据库连接与事务的注意事项流式查询对数据库连接持有时间更长这是一个非常重要的点。在传统查询中SQL 执行完毕拿到所有数据后数据库连接就可以释放回连接池。在流式查询中由于要逐条处理整个处理过程都必须保持数据库连接和事务是打开的。直到你处理完最后一条数据或主动关闭Cursor连接才会释放。后果如果处理逻辑很慢比如每处理一条数据调用一次外部 API连接会被长时间占用容易导致数据库连接池耗尽。必须确保流式查询在一个独立的事务中执行并且该事务的隔离级别要合适。长时间的事务可能带来锁竞争等问题。在 Web 应用中要避免在 HTTP 请求线程中执行耗时的流式查询否则会阻塞请求线程。应考虑使用异步任务或消息队列。4. 完整实战案例三种方式实现订单数据流式导出假设我们有一个t_order表结构如下CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 用户ID, amount DECIMAL(10,2) NOT NULL COMMENT 订单金额, status TINYINT NOT NULL COMMENT 状态, create_time DATETIME NOT NULL COMMENT 创建时间, INDEX idx_create_time (create_time) ) COMMENT 订单表;实体类Order.javapackage com.example.demo.entity; import lombok.Data; import java.math.BigDecimal; import java.util.Date; Data public class Order { private Long id; private String orderNo; private Long userId; private BigDecimal amount; private Integer status; private Date createTime; }4.1 方式一使用ResultHandler处理这是最基础的方式适合处理逻辑复杂且不需要将结果集返回给上层调用者的场景。第一步实现ResultHandler创建一个类来实现ResultHandlerOrder接口。这里我们模拟将订单数据写入文件。package com.example.demo.handler; import com.example.demo.entity.Order; import org.apache.ibatis.session.ResultHandler; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; public class OrderResultHandler implements ResultHandlerOrder { private final BufferedWriter writer; private int count 0; public OrderResultHandler(String filePath) throws IOException { this.writer new BufferedWriter(new FileWriter(filePath)); // 写入CSV头 writer.write(订单ID,订单号,用户ID,金额,状态,创建时间); writer.newLine(); } Override public void handleResult(ResultContext? extends Order resultContext) { Order order resultContext.getResultObject(); count; try { // 将订单对象转换为CSV行并写入文件 String line String.format(%d,%s,%d,%.2f,%d,%s, order.getId(), order.getOrderNo(), order.getUserId(), order.getAmount(), order.getStatus(), order.getCreateTime()); writer.write(line); writer.newLine(); // 每处理1000条刷一次磁盘避免内存缓冲过大 if (count % 1000 0) { writer.flush(); System.out.println(已处理 count 条订单); } } catch (IOException e) { // 这里应该根据业务需要处理异常例如记录日志、抛出运行时异常等 // 注意在ResultHandler中抛出异常会中断整个结果处理过程 throw new RuntimeException(写入文件失败, e); } } public void finish() throws IOException { writer.flush(); writer.close(); System.out.println(处理完成总计 count 条订单); } }第二步在 Mapper 接口中定义方法注意使用ResultHandler的 Mapper 方法返回类型必须是void。package com.example.demo.mapper; import com.example.demo.entity.Order; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.session.ResultHandler; public interface OrderMapper { /** * 流式查询订单数据使用 ResultHandler 处理 * param startTime 开始时间 * param endTime 结束时间 * param handler 结果处理器 */ void selectOrdersStreamingByHandler(Param(startTime) java.util.Date startTime, Param(endTime) java.util.Date endTime, ResultHandlerOrder handler); }第三步编写 XML 映射文件或使用注解在OrderMapper.xml中配置 SQL。关键点必须设置fetchSize。?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.OrderMapper select idselectOrdersStreamingByHandler fetchSize100 SELECT id, order_no, user_id, amount, status, create_time FROM t_order WHERE create_time BETWEEN #{startTime} AND #{endTime} ORDER BY create_time ASC /select /mapperfetchSize100指示 JDBC 每次从数据库获取 100 行。这个值需要权衡太小会增加网络往返次数太大则内存缓冲较大。对于百万级数据100-1000 是个不错的起点。第四步在 Service 层调用package com.example.demo.service; import com.example.demo.handler.OrderResultHandler; import com.example.demo.mapper.OrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; Service public class OrderExportService { Autowired private OrderMapper orderMapper; Transactional // 重要确保整个流式查询在一个事务内 public void exportOrdersToFile(Date startTime, Date endTime, String filePath) throws Exception { // 1. 创建处理器 OrderResultHandler handler new OrderResultHandler(filePath); try { // 2. 执行查询MyBatis会自动将结果流式传递给handler orderMapper.selectOrdersStreamingByHandler(startTime, endTime, handler); } finally { // 3. 确保资源被关闭 handler.finish(); } System.out.println(导出成功文件位于: filePath); } }方式一总结优点控制粒度最细可以在handleResult方法内做任何事写入文件、发送消息、批量入库等。缺点代码侵入性较强需要自己实现接口和资源管理。Mapper 方法无返回值调用方式特殊。适用场景简单的数据导出、转换任务处理逻辑固定且不需要将数据返回。4.2 方式二使用CursorT迭代这是更现代、更符合编程直觉的方式Cursor实现了Iterable接口可以用 for-each 循环或迭代器遍历。第一步Mapper 接口方法返回CursorTpackage com.example.demo.mapper; import com.example.demo.entity.Order; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.cursor.Cursor; import java.util.Date; public interface OrderMapper { /** * 流式查询订单数据返回 Cursor * param startTime 开始时间 * param endTime 结束时间 * return CursorOrder 游标需要手动关闭 */ CursorOrder selectOrdersStreamingByCursor(Param(startTime) Date startTime, Param(endTime) Date endTime); }第二步XML 映射文件配置同样需要设置fetchSize。mapper namespacecom.example.demo.mapper.OrderMapper select idselectOrdersStreamingByCursor resultTypecom.example.demo.entity.Order fetchSize100 SELECT id, order_no, user_id, amount, status, create_time FROM t_order WHERE create_time BETWEEN #{startTime} AND #{endTime} ORDER BY create_time ASC /select /mapper第三步Service 层调用与遍历这是最关键的一步必须正确处理Cursor的关闭package com.example.demo.service; import com.example.demo.entity.Order; import com.example.demo.mapper.OrderMapper; import org.apache.ibatis.cursor.Cursor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.BufferedWriter; import java.io.FileWriter; import java.util.Date; Service public class OrderExportService { Autowired private OrderMapper orderMapper; Transactional // 事务必须打开且覆盖整个遍历过程 public void exportOrdersToFileWithCursor(Date startTime, Date endTime, String filePath) throws Exception { // 1. 获取游标 (此时SQL已执行连接被占用) try (CursorOrder cursor orderMapper.selectOrdersStreamingByCursor(startTime, endTime); BufferedWriter writer new BufferedWriter(new FileWriter(filePath))) { // 写入CSV头 writer.write(订单ID,订单号,用户ID,金额,状态,创建时间); writer.newLine(); int count 0; // 2. 遍历游标 for (Order order : cursor) { // 这里会触发逐行获取数据 count; String line String.format(%d,%s,%d,%.2f,%d,%s, order.getId(), order.getOrderNo(), order.getUserId(), order.getAmount(), order.getStatus(), order.getCreateTime()); writer.write(line); writer.newLine(); if (count % 1000 0) { writer.flush(); System.out.println(已处理 count 条订单); } } writer.flush(); System.out.println(导出完成总计 count 条订单); } // 3. 自动关闭游标和Writertry-with-resources语法 // 游标关闭后数据库连接才会释放 } }代码详解try (CursorOrder cursor ...)使用try-with-resources语法确保Cursor一定会被关闭这是强制要求Cursor实现了Closeable接口。Transactional这个注解必须存在并且其作用范围要覆盖整个try块。如果事务在遍历完成前就提交了数据库可能会关闭游标导致后续遍历失败。for (Order order : cursor)这个循环是“惰性”的。它并不会一次性把所有Order对象加载到内存的List里而是每次迭代时MyBatis 才从数据库连接中获取下一行数据并映射成对象。因此内存中最多同时存在少量Order对象数量约等于fetchSize。方式二总结优点API 优雅使用熟悉的迭代模式。可以在遍历过程中灵活控制比如分批处理、条件中断。缺点必须牢记关闭Cursor且事务范围要控制好。适用场景绝大多数需要流式处理并逐条控制的场景是目前最推荐的方式。4.3 方式三使用Options注解配置这种方式更像是一种“优化提示”让 MyBatis 在返回List时也采用流式的方式从数据库获取数据但最终仍然返回一个完整的List。注意如果数据量真的极大最终这个List仍然可能很大存在风险。它更适用于“数据量较大但尚可接受”的场景或者作为其他流式方式的一种补充配置。第一步在 Mapper 方法上使用Optionspackage com.example.demo.mapper; import com.example.demo.entity.Order; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.cursor.Cursor; import java.util.Date; import java.util.List; public interface OrderMapper { /** * 使用 Options 配置流式行为但返回 List。 * 注意这要求调用者能快速消费完这个List否则仍有OOM风险。 */ Options(resultSetType ResultSetType.FORWARD_ONLY, fetchSize 100) Select(SELECT id, order_no, user_id, amount, status, create_time FROM t_order WHERE create_time BETWEEN #{startTime} AND #{endTime} ORDER BY create_time ASC) ListOrder selectOrdersWithOptions(Param(startTime) Date startTime, Param(endTime) Date endTime); }Options(resultSetType ResultSetType.FORWARD_ONLY, fetchSize 100)这是关键配置。resultSetType FORWARD_ONLY指定结果集类型为“仅向前”这是流式读取的常用类型效率最高。fetchSize 100设置每次获取的行数。第二步Service 层调用调用方式与普通查询无异但你需要清楚返回的List是“流式填充”的。Transactional public void processOrdersWithOptions(Date startTime, Date endTime) { ListOrder orders orderMapper.selectOrdersWithOptions(startTime, endTime); // 注意此时SQL已执行完毕但数据可能还在从网络流中读取到List里。 // 如果这个List被后续代码快速遍历和处理内存压力较小。 // 但如果将这个巨大的List保存在内存中或传递给其他方法风险依旧存在。 int count 0; for (Order order : orders) { // 处理订单 count; if (count % 1000 0) { System.out.println(处理中: count); } } System.out.println(处理完成共 count 条); }方式三总结优点对调用者透明无需学习新的 API (Cursor或ResultHandler)。缺点返回类型仍是List容易让调用者误以为所有数据已安全在内存中从而进行危险操作。它不能从根本上防止 OOM只是优化了数据从数据库到List的填充过程。适用场景数据量在几十万级别且你确认调用方会立即遍历处理而不长期持有该List的引用。对于真正的海量数据百万级以上优先使用Cursor。5. 常见问题与排查思路在实际使用流式查询时你可能会遇到以下问题。这里提供一个排查清单。问题现象可能原因排查步骤与解决方案流式查询没有效果内存依然飙升1.数据库驱动不支持或配置错误尤其是 MySQL 5.x 驱动。2.fetchSize未设置或设置无效。3.SQL 本身包含大量聚合或复杂计算导致结果在数据库端就很大。1.检查驱动版本升级到 MySQL Connector/J 8.x。2.检查fetchSize在 Mapper XML 或Options中确认已设置如fetchSize100或Integer.MIN_VALUE。3.检查 SQL使用EXPLAIN分析避免在数据库层产生巨大中间结果集。遍历Cursor时出现Connection is closed或ResultSet is closed错误1.Cursor未在事务范围内遍历事务提前提交导致连接关闭。2.手动关闭了SqlSession或数据库连接。3.遍历过程中发生异常未正确关闭Cursor。1.确保Transactional注解覆盖整个遍历过程且方法不是privateSpring AOP 代理要求。2.使用 try-with-resources 确保Cursor关闭。3. 检查是否有其他代码干扰了SqlSession的生命周期。流式查询速度非常慢1.fetchSize设置过小导致网络往返次数激增。2.数据库服务器压力大或网络延迟高。3.处理每条数据的逻辑太耗时阻塞了数据流。1.适当调大fetchSize如从 100 调到 1000在内存和速度间权衡。2. 在数据库低峰期执行任务。3.优化处理逻辑考虑批量处理如每1000条提交一次。MySQL 报错Streaming result set com.mysql.cj.protocol.a.result.ResultsetRowsStreamingXXX is still active在流式结果集未关闭时试图在同一连接上执行新的查询。1.确保流式查询完成后才执行其他操作。2. 考虑为流式查询使用一个独立的、专用的数据库连接可通过配置单独的数据源实现。程序在处理中途崩溃数据库连接未释放Cursor未被关闭导致数据库游标和连接一直处于打开状态可能耗尽数据库资源。1.始终使用 try-with-resources。2. 在finally块中显式检查并关闭Cursor。3. 设置数据库连接的超时时间如wait_timeout让数据库端自动清理僵尸连接。6. 最佳实践与工程建议掌握了基本用法后将这些最佳实践应用到项目中能让你的流式查询更稳健、高效。6.1 配置与依赖管理锁定驱动版本在pom.xml中明确指定数据库驱动版本避免因版本升级导致行为变化。对于 MySQL坚持使用 8.x。全局defaultFetchSize可以在mybatis-config.xml中设置一个合理的全局默认fetchSize避免每个查询都单独配置。settings !-- 设置默认的 fetchSize会被具体语句的配置覆盖 -- setting namedefaultFetchSize value1000/ /settings6.2 事务与连接管理使用PROPAGATION_REQUIRES_NEW如果流式查询是大型任务的一部分考虑为其创建一个独立的新事务 (Transactional(propagation Propagation.REQUIRES_NEW))避免长时间占用主事务连接。监控连接池在应用监控中关注数据库连接池的活跃连接数、最大等待时间。流式查询可能导致连接持有时间变长需要适当调整连接池参数如maxLifetime、connectionTimeout。设置查询超时在 SQL 映射中设置timeout属性防止一个异常的流式查询永远不结束。select idselectLargeData fetchSize100 timeout300 !-- 超时300秒 -- ... /select6.3 数据处理与性能批处理即使在流式处理中也不建议每条记录都进行一次 I/O 操作如写入文件、调用远程接口。应该积累一定数量如 1000 条后进行批量操作这能极大提升性能。异步处理将耗时的流式处理任务提交到线程池或消息队列中异步执行避免阻塞 HTTP 请求线程。可以使用 Spring 的Async或CompletableFuture。内存监控在处理超大数据集时即使使用流式查询也要关注 JVM 内存。因为你的处理逻辑可能会创建大量中间对象。适当调整 JVM 堆大小 (-Xmx)并考虑使用更节省内存的数据结构。6.4 代码健壮性资源关闭模板编写一个工具方法封装Cursor的遍历和资源关闭逻辑确保所有流式查询都遵循同一套安全模式。异常处理在ResultHandler或Cursor的遍历循环中做好异常处理。一旦发生异常应确保已打开的文件、网络连接等资源能被正确清理并考虑是否要记录已处理到的位置以便重试。日志与进度在流式处理中记录处理进度如每处理1万条打印一条日志便于监控和问题排查。6.5 数据库端优化索引确保流式查询的WHERE条件和ORDER BY字段上有合适的索引否则数据库全表扫描本身就会很慢。减少返回列只SELECT需要的列避免SELECT *。这能减少网络传输量和内存占用。分区表如果数据量极其庞大考虑使用数据库分区表流式查询时可以结合分区条件减少单次扫描的数据量。在上篇中我们深入探讨了 MyBatis 流式查询的原理、三种实现方式以及相关的实践和避坑指南。核心要点是用Cursor替代List用try-with-resources确保关闭用Transactional保持连接用合适的fetchSize平衡内存与性能。在下篇中我们将继续深入探讨更高级的主题ResultHandler与Cursor的底层源码分析看看 MyBatis 和 JDBC 是如何协作完成流式传输的。与 Spring 管理事务的深度集成解决在复杂事务传播行为下流式查询的陷阱。在分布式环境与分库分表如 ShardingSphere中使用流式查询的注意事项。性能对比测试流式查询 vs 分页查询 vs 传统查询用数据说话。一个完整的、生产级的数据导出服务架构设计涵盖异步、断点续传、进度上报等。处理大数据是后端开发的必修课而流式查询是工具箱中至关重要的一件利器。理解它用好它你的应用在面对海量数据时将更加从容。建议你立即在本地创建一个测试项目用一张有几十万测试数据的表亲自体验一下三种方式的差异感受内存变化这比读十篇文章都管用。 30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度