
这次我们来看一个 Java 开发中非常实际的内存问题当你的 MyBatis 查询返回海量数据时如何避免一行代码就把内存撑爆。这个问题在数据导出、报表生成、大数据量分页等场景下频繁出现直接导致 OOMOutOfMemoryError服务崩溃。本文的核心是MyBatis 流式查询。它不是新概念但很多开发者要么不知道要么知道却用不对。我们将彻底拆解它的原理、适用场景并给出从环境配置到代码实战的完整方案。如果你正在处理百万级数据查询或者对ResultHandler、游标Cursor这些概念感到模糊这篇文章可以直接收藏。我们将重点关注流式查询是什么它与普通查询的本质区别以及如何绕过 JVM 堆内存限制。两种实现方式基于ResultHandler的“推”模式和基于Cursor的“拉”模式各自的优缺点和适用场景。实战代码与避坑指南提供可运行的 Spring Boot MyBatis 示例并指出事务管理、连接超时、ORM 映射等关键陷阱。性能与资源观察如何监控查询过程中的内存和数据库连接占用确保方案稳定。本文适合所有使用 MyBatis 进行数据持久化的 Java 后端开发者特别是那些需要处理大数据集又受困于内存压力的同学。我们将从问题现象出发一步步构建解决方案。1. 核心能力速览流式查询 vs 普通查询在深入代码之前我们先通过一个表格快速理解流式查询的核心价值它决定了你是否需要以及何时需要使用它。能力项普通查询 (默认方式)流式查询 (本文方案)数据加载机制一次性加载JDBC 驱动将查询结果全部取回MyBatis 将其全部映射为对象列表 (ListT)存储在 JVM 堆内存中。逐条/分批加载JDBC 驱动按需从数据库服务器传输结果集数据。MyBatis 逐条或分批处理并映射对象处理完的数据可立即被 GC 回收。内存占用高。与查询结果集大小正相关极易触发 OOM。例如查询 100 万条记录每条记录映射对象 1KB则内存峰值约 1GB。低且稳定。内存占用与单次处理的数据量fetchSize相关与总数据量无关。通常只需几十到几百 KB 的缓冲区。适用数据量中小数据集通常建议 10万条具体取决于单条数据大小和 JVM 堆配置。超大数据集十万、百万甚至千万级。是处理海量数据查询的标配方案。返回类型ListT,Map, 或单个对象。CursorT(用于“拉”模式) 或使用ResultHandler(用于“推”模式)。无法直接返回List。代码复杂度简单直观。直接调用selectList等方法。较高。需要管理游标、确保资源关闭、注意事务边界。典型使用场景分页查询、管理后台列表、配置数据加载。数据全量导出到 CSV/Excel、大数据分析、ETL 数据迁移、日志批量处理。核心结论如果你的查询可能返回超过内存承受能力的数据量并且你需要对每条数据执行后续操作如写入文件、发送消息、汇总计算那么流式查询是你的必选项。它用稍高的代码复杂度换取了系统的稳定性和处理能力的质变。2. 适用场景与使用边界流式查询是一把利器但并非所有场景都适用。明确边界能避免误用和性能倒退。2.1 最适合的场景强烈推荐使用数据导出这是最经典的场景。将数据库中的百万条记录导出为 CSV 或 Excel 文件。使用流式查询可以边读边写内存中只保留少量数据。批量数据处理ETL从源数据库读取大量数据经过转换后写入目标数据库或数据仓库。流式查询可以避免在转换过程中内存爆掉。大数据分析与统计需要对全表数据进行遍历计算如求和、计数、去重统计但不需要在内存中同时持有所有对象。可以通过流式读取逐条累加。消息队列数据灌入从数据库读取历史数据逐条或分批发送到 Kafka、RocketMQ 等消息中间件。日志/流水记录的后台处理处理用户操作日志、交易流水等随时间累积的海量数据。2.2 不适合的场景不要使用需要随机访问或多次遍历数据流式查询像水流流过即消失。如果你需要反复访问列表中的第 N 条数据或者需要对全部数据排序、分组后再处理则必须先将所有数据加载到内存中。流式查询不提供这种能力。分页查询分页查询LIMIT offset, size本身就是为了避免大数据量。数据库已在服务器端完成了数据裁剪返回的数据量很小使用普通查询即可。流式查询在此场景无优势反而增加复杂度。实时交互式小列表管理后台的表格、下拉列表等数据量通常很小直接使用List返回是最佳实践。事务中混合了其他非流式查询流式查询对数据库连接有特殊要求后续会详述在复杂事务中混用可能导致连接持有时间过长或意外关闭。使用边界与合规提醒数据库连接资源流式查询会长时间占用一个数据库连接直到所有数据读取完毕。必须在代码中确保 finally 块或 try-with-resources 语句关闭游标防止连接泄漏。事务超时长时间运行的流式查询可能触发事务超时。需要根据业务合理设置事务超时时间或考虑在非事务环境下执行。网络稳定性在流式传输过程中网络中断会导致查询失败。对于关键业务需要设计重试机制。ORM 映射开销尽管内存占用低但 MyBatis 为每一行数据创建对象并进行属性映射的开销依然存在。如果单行数据列非常多、非常宽这个 CPU 开销不可忽视。3. 环境准备与前置条件为了演示流式查询我们需要一个标准的 Spring Boot MyBatis 环境。以下是搭建最小化演示环境的步骤。3.1 基础环境清单组件要求说明JDK8 或以上推荐 11, 17流式查询特性在 JDBC 4.0 规范中已支持。构建工具Maven 或 Gradle本文使用 Maven 示例。Spring Boot2.x 或 3.x本文基于 Spring Boot 2.7.x。3.x 版本在配置上基本一致。MyBatisMyBatis Spring Boot Starter版本与 Spring Boot 对应例如2.3.0。数据库MySQL 5.7 / PostgreSQL / 其他支持 JDBC 流式结果集的数据库关键数据库驱动必须支持流式结果集。MySQL Connector/J 和 PostgreSQL JDBC Driver 都支持。IDEIntelliJ IDEA, Eclipse, VS Code 等任意你熟悉的 Java 开发环境。3.2 项目初始化与依赖使用 Spring Initializr 或手动创建一个 Maven 项目核心依赖如下!-- pom.xml -- dependencies !-- Spring Boot Web (用于创建测试接口) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 整合 Spring Boot -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version !-- 请使用与Spring Boot匹配的版本 -- /dependency !-- 数据库驱动 (以MySQL为例) -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope !-- 注意8.x驱动类名和URL与5.x不同 -- /dependency !-- 方便测试可选 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies关键点mysql-connector-java驱动版本建议使用 8.0.x。对于 MySQL确保在连接 URL 中设置了useCursorFetchtrue参数以启用服务端游标支持对于大数据量更高效我们会在配置部分说明。3.3 数据库与测试数据准备创建一个简单的表并插入足够多的测试数据以模拟大数据量查询。-- 创建用户表 CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(255) DEFAULT NULL, email varchar(255) DEFAULT NULL, age int(11) DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 插入模拟数据 (例如插入50万条) -- 可以使用存储过程或程序批量插入这里示意性插入少量数据 INSERT INTO user (name, email, age) VALUES (张三, zhangsanexample.com, 25), (李四, lisiexample.com, 30); -- ... 实际测试需要更多数据为了快速制造大量测试数据可以在 MySQL 中运行一个简单的循环语句注意在生产环境谨慎操作DELIMITER // CREATE PROCEDURE generate_test_data() BEGIN DECLARE i INT DEFAULT 1; WHILE i 1000000 DO INSERT INTO user (name, email, age) VALUES (CONCAT(User-, i), CONCAT(user, i, test.com), FLOOR(RAND() * 80) 18); SET i i 1; END WHILE; END // DELIMITER ; -- 调用存储过程 CALL generate_test_data();4. MyBatis 流式查询的两种实现方式MyBatis 提供了两种主要的流式查询方式它们底层都依赖于 JDBC 的ResultSet流式读取能力但在 API 使用上截然不同。4.1 方式一ResultHandler“推”模式在这种模式下你定义一个处理器ResultHandlerMyBatis 会在遍历结果集时主动将每一条映射好的对象“推”给你的处理器。你无法控制遍历的节奏但代码结构清晰。1. 定义实体类和 Mapper 接口// User.java Data // 使用Lombok public class User { private Long id; private String name; private String email; private Integer age; private Date createdAt; }// UserMapper.java Mapper public interface UserMapper { // 普通查询方法用于对比 ListUser selectAllUsers(); // 流式查询方法使用 ResultHandler // 注意返回类型必须是 void结果通过 handler 参数传递 void selectAllUsersStreaming(ResultHandlerUser handler); }2. 编写 Mapper XML!-- UserMapper.xml -- ?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.UserMapper !-- 普通查询 -- select idselectAllUsers resultTypeUser SELECT id, name, email, age, created_at as createdAt FROM user /select !-- 流式查询SQL 与普通查询完全一样但配置了 fetchSize 和 resultSetType -- select idselectAllUsersStreaming fetchSize100 resultSetTypeFORWARD_ONLY resultTypeUser SELECT id, name, email, age, created_at as createdAt FROM user /select /mapper关键配置解释fetchSize”100” 提示 JDBC 驱动每次从数据库网络缓冲区获取的行数。这是一个性能调优参数-1表示使用驱动默认值。设置为一个正数如 100, 1000可以在网络往返次数和内存占用间取得平衡。resultSetType”FORWARD_ONLY” 指定结果集类型为“仅向前”。这是流式查询的必要条件它告诉数据库和驱动结果集不会回滚或随机访问从而允许驱动以流式方式传输数据。3. 实现 ResultHandler 并调用// 自定义ResultHandler Component public class UserStreamHandler implements ResultHandlerUser { private static final Logger log LoggerFactory.getLogger(UserStreamHandler.class); private int count 0; Override public void handleResult(ResultContext? extends User resultContext) { // 每获取到一条记录该方法被调用一次 User user resultContext.getResultObject(); count; // 模拟处理这里可以是写入文件、发送消息、累加计算等 // 例如每处理1000条打印一次日志 if (count % 1000 0) { log.info(已处理 {} 条记录当前用户: {}, count, user.getName()); // 重要在此处可以手动触发垃圾回收但不要频繁调用 // if (count % 10000 0) { System.gc(); } } // 关键处理完的对象在方法结束后即可被GC回收 // 此处不要将user添加到外部集合中否则失去流式意义 } public int getCount() { return count; } }4. 在 Service 中调用Service Slf4j public class UserService { Autowired private UserMapper userMapper; Autowired private UserStreamHandler userStreamHandler; /** * 普通查询 - 可能导致OOM */ public ListUser getAllUsersNormal() { return userMapper.selectAllUsers(); // 数据量大时这里直接返回List内存暴涨 } /** * 流式查询 - 使用ResultHandler */ Transactional // 注意事务边界流式查询必须在一个事务内或者关闭自动提交。 public void processUsersStreaming() { // 重置计数器如果Handler是单例 // userStreamHandler.resetCount(); log.info(开始流式处理用户数据...); long startTime System.currentTimeMillis(); // 调用Mapper方法传入handler userMapper.selectAllUsersStreaming(userStreamHandler); long endTime System.currentTimeMillis(); log.info(流式处理完成。总计处理 {} 条记录耗时 {} ms, userStreamHandler.getCount(), (endTime - startTime)); } }ResultHandler模式特点优点代码逻辑集中处理流程清晰。MyBatis 负责遍历和映射你只关心处理逻辑。缺点控制权在 MyBatis 手中你无法暂停、跳过或者以非顺序方式处理数据。如果handleResult方法中的处理逻辑很慢会阻塞整个结果集的获取。4.2 方式二Cursor“拉”模式在这种模式下Mapper 方法返回一个CursorT对象。你可以像使用Iterator一样主动地、按需地“拉取”数据控制权在你手中。1. 修改 Mapper 接口// UserMapper.java Mapper public interface UserMapper { // 返回 Cursor 的流式查询方法 CursorUser selectAllUsersByCursor(); }2. 编写 Mapper XML!-- UserMapper.xml -- select idselectAllUsersByCursor fetchSize100 resultSetTypeFORWARD_ONLY resultTypeUser SELECT id, name, email, age, created_at as createdAt FROM user /selectXML 配置与ResultHandler模式完全一致都需要fetchSize和resultSetType。3. 在 Service 中调用 CursorService Slf4j public class UserService { Autowired private UserMapper userMapper; /** * 流式查询 - 使用Cursor */ Transactional // 至关重要Cursor必须在一个数据库事务中打开和使用。 public void processUsersByCursor() { log.info(开始使用Cursor流式处理用户数据...); long startTime System.currentTimeMillis(); int count 0; // 关键使用 try-with-resources 确保 Cursor 被关闭从而释放数据库连接 try (CursorUser cursor userMapper.selectAllUsersByCursor()) { for (User user : cursor) { // Cursor 实现了 Iterable 接口 count; // 模拟处理业务 if (count % 1000 0) { log.info(已处理 {} 条记录当前用户: {}, count, user.getName()); } // 处理完的user对象在循环结束后可被GC } } catch (Exception e) { log.error(流式处理数据失败, e); throw new RuntimeException(e); } long endTime System.currentTimeMillis(); log.info(Cursor流式处理完成。总计处理 {} 条记录耗时 {} ms, count, (endTime - startTime)); } }Cursor模式特点优点控制灵活。你可以决定何时调用next()获取下一条数据可以在循环中根据条件break也可以将游标传递给其他方法处理。更符合传统的“拉取”编程模型。缺点需要手动管理资源必须用try-with-resources或finally块关闭Cursor。如果忘记关闭会导致数据库连接泄漏这是严重的 Bug。5. 关键配置与深度调优仅仅使用fetchSize和resultSetType可能还不够为了流式查询稳定高效必须关注以下配置。5.1 数据库连接配置以 MySQL 为例在application.yml或application.properties中配置数据源# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiuseCursorFetchtrue # 关键参数 username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: # 连接池配置根据流式查询特点调整 maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 # 连接超时时间流式查询可能耗时较长 idle-timeout: 600000 max-lifetime: 1800000关键参数useCursorFetchtrue对于 MySQL这个参数指示驱动使用服务端游标Server-side Cursor。默认情况下JDBC 驱动会一次性将所有结果集拉取到客户端内存中即“普通查询”。启用此参数后驱动会告诉 MySQL 服务器“我要流式读取”服务器会按fetchSize分批发送数据极大地减少客户端内存压力。这是 MySQL 流式查询生效的核心开关。注意并非所有数据库都需要或支持类似参数。PostgreSQL 默认行为就更适合流式通常无需特殊配置。5.2 MyBatis 配置可以在全局配置中设置默认的fetchSize避免在每个 Mapper XML 中重复写。# application.yml mybatis: configuration: default-fetch-size: 100 # 全局默认fetchSize会被XML中的配置覆盖 # 其他配置...5.3 事务管理配置极其重要流式查询尤其是Cursor模式必须在一个有效的事务中执行。原因在于为了保持结果集的可读性数据库连接Connection在结果集遍历完成前不能被关闭或归还到连接池。而 Spring 的声明式事务Transactional正是管理连接生命周期的最佳工具。Service public class UserService { Transactional // 必须添加事务注解 public void processUsersByCursor() { try (CursorUser cursor userMapper.selectAllUsersByCursor()) { // ... 遍历 cursor } } Transactional // 同样需要事务 public void processUsersStreaming() { userMapper.selectAllUsersStreaming(handler); } }如果没有事务MyBatis 在执行完 Mapper 方法后会立即关闭SqlSession进而关闭底层的数据库连接。此时Cursor还未遍历完连接就被关闭了后续遍历会抛出Connection is closed异常。事务超时设置流式处理百万数据可能耗时几分钟甚至更久。默认的事务超时时间可能不够。你需要根据业务情况调整。Transactional(timeout 600) // 单位秒设置10分钟超时 public void processLargeDataByCursor() { // ... }非事务场景如果你的处理逻辑就是一次性读取全部数据并处理且不允许失败回滚也可以考虑在非事务环境下但需要手动管理连接会话。这非常复杂且容易出错不推荐。6. 功能测试与效果验证模拟 OOM 与流式拯救现在让我们通过一个简单的测试来直观感受普通查询的 OOM 风险和流式查询的稳定性。6.1 测试准备制造内存压力首先确保你的测试数据量足够大例如 50 万条以上。然后调整 JVM 启动参数故意设置一个较小的堆内存以便快速触发 OOM方便观察。在 IDE 的启动配置或命令行中添加-Xmx256m -Xms256m -XX:HeapDumpOnOutOfMemoryError这会将最大堆内存限制在 256MB。6.2 测试用例1普通查询预期OOMRestController Slf4j public class TestController { Autowired private UserService userService; GetMapping(/test/normal) public String testNormalQuery() { log.info(开始普通查询测试...); long start System.currentTimeMillis(); try { ListUser users userService.getAllUsersNormal(); // 这里会加载所有数据到List long end System.currentTimeMillis(); log.info(普通查询成功获取 {} 条数据耗时 {} ms, users.size(), (end - start)); return Success. Count: users.size(); } catch (Exception e) { log.error(普通查询发生异常: , e); return Failed: e.getClass().getName() - e.getMessage(); } } }预期结果当数据量超过 JVM 堆内存容量时在userService.getAllUsersNormal()执行过程中你会看到java.lang.OutOfMemoryError: Java heap space错误。服务可能崩溃或无响应。6.3 测试用例2Cursor 流式查询预期成功RestController Slf4j public class TestController { Autowired private UserService userService; GetMapping(/test/cursor) public String testCursorStreaming() { log.info(开始Cursor流式查询测试...); long start System.currentTimeMillis(); try { userService.processUsersByCursor(); // 内部使用Cursor遍历 long end System.currentTimeMillis(); log.info(Cursor流式查询处理完成耗时 {} ms, (end - start)); return Cursor Streaming Process Success.; } catch (Exception e) { log.error(Cursor流式查询发生异常: , e); return Failed: e.getClass().getName() - e.getMessage(); } } }预期结果即使 JVM 堆内存很小如 256MB这个接口也能成功处理百万级数据。通过监控工具如 JConsole、VisualVM观察你会看到堆内存使用率是一条平稳的波浪线而不是直线上升直至爆掉。因为内存中始终只持有少量User对象。6.4 测试用例3ResultHandler 流式查询预期成功GetMapping(/test/handler) public String testHandlerStreaming() { log.info(开始ResultHandler流式查询测试...); long start System.currentTimeMillis(); try { userService.processUsersStreaming(); long end System.currentTimeMillis(); log.info(ResultHandler流式查询处理完成耗时 {} ms, (end - start)); return Handler Streaming Process Success.; } catch (Exception e) { log.error(ResultHandler流式查询发生异常: , e); return Failed: e.getClass().getName() - e.getMessage(); } }预期结果与 Cursor 模式类似内存占用平稳处理成功。成功判断标准接口返回成功信息无 OOM 异常。应用日志显示处理了预期数量的记录。JVM 堆内存监控图表显示内存使用率平稳无持续飙升。数据库连接数稳定处理完成后连接被正确释放。7. 资源占用与性能观察理解了原理并跑通测试后我们需要关注流式查询在真实场景下的表现。7.1 如何观察资源占用JVM 内存监控工具JConsole, VisualVM, JDK Mission Control, 或应用性能管理APM工具如 SkyWalking, Prometheus Grafana。观察指标Heap Memory Usage。流式查询下该图表应呈现锯齿状或平稳状峰值远小于堆最大值。数据库连接监控工具数据库自身的监控如 MySQLSHOW PROCESSLIST或连接池监控如 HikariCP 的spring-boot-starter-actuator端点。观察指标活跃连接数、连接持有时间。流式查询会长时间占用一个连接这是正常的。但要确保处理完成后连接被关闭。系统资源监控工具top,htop,docker stats。观察指标进程的 CPU 使用率和物理内存RSS。流式查询的 CPU 使用率可能较高因为持续进行对象映射和业务处理但物理内存应保持稳定。7.2 性能影响因素与调优fetchSize参数太小如 10增加网络往返次数降低整体吞吐量。太大如 10000单次网络传输数据包过大可能在客户端驱动层缓冲较多数据轻微增加内存压力但减少了网络次数。建议从默认值或 100-1000 开始测试根据网络延迟和单行数据大小调整。可以通过 JMeter 或单元测试对比不同fetchSize下的总耗时。JDBC 驱动与数据库版本确保使用较新版本的数据库驱动如 MySQL Connector/J 8.0它们对流式查询的支持和性能优化更好。数据库服务器版本也可能影响流式传输效率。业务处理逻辑的耗时流式查询的瓶颈往往不在“读数据”而在“处理数据”。如果handleResult方法或Cursor循环体内的业务逻辑非常耗时如复杂的计算、同步调用外部 API整体处理时间会线性增长。优化方向考虑将处理逻辑异步化、批量化或者引入并行处理但要注意线程安全和数据库连接限制。网络带宽与延迟如果应用与数据库跨机房或跨地域网络延迟会显著影响流式查询的体验因为每个fetchSize批次都可能有一次网络往返。优化方向适当增大fetchSize或者将处理程序部署到离数据库更近的位置。8. 常见问题与排查方法在实际使用流式查询时你可能会遇到以下问题。这里提供排查思路。问题现象可能原因排查方式解决方案Cursor遍历时抛出Connection is closed1. 方法未添加Transactional注解。2. 事务在遍历完成前被提前提交或回滚。3. 手动关闭了SqlSession。1. 检查 Service 方法是否有Transactional。2. 检查是否有其他代码调用了TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或类似操作。3. 检查是否在遍历前调用了sqlSession.close()。确保整个遍历过程在一个事务内。使用try-with-resources管理Cursor。流式查询速度很慢甚至比普通查询还慢1.fetchSize设置过小网络交互频繁。2. 业务处理逻辑 (handleResult或循环体) 太耗时。3. 数据库服务器压力大或 SQL 本身慢。4. 未正确启用数据库的流式支持如 MySQL 未加useCursorFetchtrue。1. 检查fetchSize配置。2. 在业务逻辑中添加日志计算单条处理耗时。3. 在数据库端执行 SQL检查执行计划。4. 检查数据库连接 URL 参数。1. 调大fetchSize。2. 优化业务逻辑考虑异步或批量处理。3. 优化 SQL 和索引。4. 确保连接参数正确。内存使用仍然很高1. 在ResultHandler.handleResult或Cursor循环中将对象添加到了外部的集合如List,Map导致所有对象仍被引用。2. 单条数据映射的对象本身非常大包含大字段如LONGTEXT,BLOB。3. JVM 堆内存设置太小即使流式处理基础对象创建也需要空间。1.仔细检查代码确保没有在流式处理过程中积累数据。2. 检查实体类字段和数据库表结构。3. 使用 JVM 监控工具观察 GC 情况和对象分配。1.修正代码流式处理完的对象应立即解除强引用。2. 考虑分拆大字段表或使用Transient注解忽略大字段的映射。3. 适当增加 JVM 堆内存。MySQL 抛出Streaming result set is still active在同一个连接上前一个流式查询的结果集未关闭就尝试执行新的查询。检查代码逻辑确保Cursor已关闭或ResultHandler处理已完成再执行其他数据库操作。严格遵守“一个连接上一个活跃流式结果集”的限制。使用try-with-resources确保关闭。处理到一半程序崩溃连接未释放程序异常退出未执行到关闭Cursor或结束事务的代码。查看应用日志和数据库连接列表。1. 加强异常处理在catch块或finally块中确保资源释放。2. 设置合理的连接池超时时间让连接池回收僵死连接。9. 最佳实践与使用建议始终使用try-with-resources处理Cursor这是防止数据库连接泄漏的最有效手段。try (CursorUser cursor mapper.selectCursor()) { // 处理 cursor } // 自动调用 cursor.close()明确事务边界为包含流式查询的方法添加Transactional并评估事务超时时间是否足够长。分离查询与处理Mapper 只负责数据访问复杂的业务处理逻辑应放在 Service 层。在 Service 层进行流式遍历和处理。谨慎处理异常在流式处理循环中单条数据的处理失败不应导致整个任务终止。应考虑捕获单条记录的异常并记录日志然后继续处理下一条。性能测试与监控上线在大数据量场景下务必进行充分的性能测试监控内存、CPU、数据库连接和慢查询日志。根据监控结果调整fetchSize、JVM 参数和连接池配置。考虑替代方案对于极端大数据量亿级流式查询可能依然不够。需要考虑分页批次处理虽然“深分页”有性能问题但如果是按主键ID等有序条件分批WHERE id ? LIMIT ?则是非常高效的方案。数据库原生导出工具如mysqldump,SELECT ... INTO OUTFILE。大数据平台直接将数据同步到 Hive、Spark 等平台进行处理。代码可读性流式查询代码比普通查询复杂。应在关键位置添加清晰的注释说明为何使用流式查询、事务如何管理、资源如何关闭。流式查询是 MyBatis 处理海量数据的神兵利器它将你从 OOM 的恐惧中解放出来。核心在于理解其“逐条消耗”而非“一次性加载”的机制并掌握ResultHandler和Cursor两种模式。成功的关键点可以总结为正确的配置fetchSize,resultSetType, 连接参数、严格的事务管理、以及确保资源被可靠关闭。在实际项目中首次引入流式查询时建议从一个非核心的、数据量大的导出功能开始试点。完整走通配置、编码、测试和监控的全流程验证其稳定性和效果。一旦掌握了这套模式你就可以 confidently 应对任何大数据量查询场景再也不用担心一行代码就把内存挤爆了。