ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

mybatis-plus流式查询实战:8款redis可视化工具与JAVA工具集协同调优

mybatis-plus流式查询实战:8款redis可视化工具与JAVA工具集协同调优 1. 大数据量导出为什么会把内存打爆先说结论MyBatis-Plus 默认的selectList在导出百万行数据时会把整个结果集一次性映射成 Java 对象塞进堆内存堆占用随行数线性上涨通常几十万行就能触发 Full GC上百万行直接 OOM。流式查询Cursor解决的就是这个问题——它让 JDBC 驱动逐行拉取应用侧边读边写堆里始终只保留当前批次的对象。这个场景在真实项目里非常典型运营后台要导出全量订单、财务要拉一年流水、数据团队要同步用户表到数仓。你可能会想分页查不就行了分页确实能控制单次内存但深分页的limit offset在百万级数据上会越翻越慢而且多次查询之间数据可能变化导出结果不一致。流式查询是更干净的做法。但流式查询不是加个Cursor就完事。它有三个容易踩的坑第一数据库连接在整个游标消费期间保持打开如果消费逻辑慢连接池会被占满第二必须手动关闭游标和 SqlSession否则连接泄漏第三流式查询期间如果对同一张表做写操作可能读到不一致的数据。我试过在一个导出接口里直接用selectList拉 80 万行堆内存峰值冲到 2.3GB接口超时。改成流式查询后堆峰值稳定在 180MB 左右降了一个数量级。这篇文章就把这套配置、Redis 缓存命中率排查、以及 JVM 调优验证的完整链路拆开讲每一步都能直接复制。适合谁看正在做数据导出、报表生成、数据同步的 Java 后端用 MyBatis-Plus 但没系统调过流式查询的开发者想搞清楚 Redis 可视化工具怎么帮自己定位缓存问题的同学。核心检索词先明确mybatis-plus 流式查询、redis 可视化工具、JAVA 工具集这三块会贯穿全文。2. 流式查询的前置配置与 TaoToken 接入在写流式查询之前先把环境准备好。这里说的前置配置分两块一是 MyBatis-Plus 和数据库连接池的参数二是如果你要用 AI 辅助生成或审查这些配置代码需要一个稳定的模型调用入口。先说数据库连接池。流式查询对连接池的要求和普通查询不一样。普通查询是「借连接-执行-归还」几毫秒就还回去了流式查询是「借连接-执行-逐行消费-归还」消费期间连接一直被占用。所以连接池的maxPoolSize不能太小否则并发导出时其他请求拿不到连接。但也不能太大否则数据库侧连接数爆掉。以 HikariCP 为例推荐配置spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # 流式查询场景建议开启避免连接被长时间占用后失效 keepalive-time: 300000MyBatis-Plus 侧需要确认Cursor类型能被正确映射。MyBatis 从 3.4.0 开始支持CursorT返回值MyBatis-Plus 3.x 完全兼容。你只需要在 Mapper 接口里把返回类型写成CursorT不需要额外配置。然后是 TaoToken 的接入。TaoToken 是一个模型调用聚合入口你可以把它理解成「一个 Base URL 一个 Key就能调用多种模型」。在流式查询这种需要反复调试 SQL、审查配置、分析报错的场景里有个顺手的模型入口能省不少时间。接入方式很简单以 OpenAI 兼容的 SDK 为例// 在 application.yml 或环境变量里配置 // base_url: https://taotoken.net/api // api_key: 你的 Key // model: 你选用的模型 ID // Java 侧用 OkHttp 或官方 SDK 调用 String baseUrl https://taotoken.net/api; String apiKey System.getenv(TAOTOKEN_API_KEY);如果你用的是 Claude Code 这类编码工具可以在配置里指定 Base URL 为https://taotoken.net/apiKey 填你自己的Model ID 按需选择。这样在写流式查询代码时可以让模型帮你检查游标关闭逻辑、事务边界、连接泄漏风险。需要提醒的是流式查询的代码审查重点在「资源关闭」和「事务范围」这两块让模型帮你过一遍比人眼扫要可靠。但模型给的代码一定要自己跑一遍尤其是try-with-resources的嵌套顺序。配置好之后先别急着写业务代码用一段最小验证确认 Cursor 能正常工作。下一节给完整可复制的配置和代码。3. 可复制的流式查询配置与 Redis 可视化连接参数这一节是全文的核心操作部分。我会给出完整的 Mapper、Service、Controller 代码以及 8 款 Redis 可视化工具的连接参数配置方式。3.1 Mapper 层Cursor 返回类型import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.cursor.Cursor; Mapper public interface OrderMapper extends BaseMapperOrder { /** * 流式查询逐行拉取降低内存占用 * 注意调用方必须在消费完后关闭 Cursor */ Select(select id, order_no, user_id, amount, status, create_time from t_order where create_time #{startTime} and create_time #{endTime}) CursorOrder streamByTimeRange(Param(startTime) String startTime, Param(endTime) String endTime); }关键点SQL 里不要写limit流式查询本身就是逐行拉取加 limit 反而限制了导出范围。如果你确实需要限制行数在消费侧计数控制而不是在 SQL 里加 limit。3.2 Service 层事务边界与资源关闭import org.apache.ibatis.cursor.Cursor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; Service public class OrderExportService { private final OrderMapper orderMapper; public OrderExportService(OrderMapper orderMapper) { this.orderMapper orderMapper; } /** * 流式导出到文件 * 必须在事务内执行否则 Cursor 会在方法返回后失效 */ Transactional(readOnly true) public long exportToFile(String startTime, String endTime, String filePath) throws IOException { long count 0; try (CursorOrder cursor orderMapper.streamByTimeRange(startTime, endTime); BufferedWriter writer new BufferedWriter(new FileWriter(filePath))) { for (Order order : cursor) { writer.write(order.toCsvLine()); writer.newLine(); count; // 每 10000 行 flush 一次避免缓冲区过大 if (count % 10000 0) { writer.flush(); } } writer.flush(); } return count; } }这里有两个必须注意的点。第一Transactional(readOnly true)不能省。流式查询依赖事务保持连接打开如果方法没有事务Cursor 在方法返回后会被关闭遍历时报Cursor is closed。第二try-with-resources里 Cursor 和 Writer 的声明顺序有讲究——Cursor 先声明Writer 后声明关闭时 Writer 先关、Cursor 后关这样不会出现「还在写文件但游标已关」的情况。3.3 Controller 层异步导出避免请求超时import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/export) public class ExportController { private final OrderExportService exportService; public ExportController(OrderExportService exportService) { this.exportService exportService; } PostMapping(/orders) public String exportOrders(RequestParam String startTime, RequestParam String endTime) { String filePath /data/export/orders_ System.currentTimeMillis() .csv; // 实际项目里应该丢到线程池异步执行 new Thread(() - { try { long count exportService.exportToFile(startTime, endTime, filePath); System.out.println(导出完成行数 count); } catch (Exception e) { System.err.println(导出失败 e.getMessage()); } }).start(); return 导出任务已提交文件路径 filePath; } }生产环境不要用new Thread用ThreadPoolTaskExecutor并设置合理的队列和拒绝策略。导出任务属于长耗时任务线程池核心线程数不宜过大建议 2-4 个避免同时多个导出把数据库连接占满。3.4 8 款 Redis 可视化工具连接参数流式导出过程中如果导出逻辑里查了 Redis 缓存比如查用户信息、字典数据缓存命中率会直接影响导出速度。下面 8 款工具都能连上你的 Redis 实例看命中率连接参数格式基本一致工具平台连接方式关键参数Redis Desktop ManagerWin/Mac/Linux图形界面Host、Port、Password、DBmedisMac图形界面Host、Port、AuthAnotherRedisDesktopManager全平台图形界面Host、Port、Auth、SSH TunnelFastoRedis全平台图形界面Host、Port、PasswordRedisPlus全平台图形界面Host、Port、Password、DBRedMacApp StoreHost、Port、PasswordRedis Insight全平台图形界面Host、Port、Username、PasswordIedis2IDEA 插件插件配置Host、Port、Password连接参数统一说明Host 填你的 Redis 地址Port 默认 6379Password 填requirepass配置的值DB 默认 0。如果你用的是云 Redis通常还需要在控制台配置白名单。连上之后重点看两个指标keyspace_hits和keyspace_misses。命中率 hits / (hits misses)。如果命中率低于 90%说明缓存策略有问题导出时大量请求穿透到数据库流式查询的优势会被抵消。在 Redis Insight 里你可以直接在命令行执行INFO stats看keyspace_hits和keyspace_misses的累计值。AnotherRedisDesktopManager 有图形化的命中率展示更直观。3.5 JAVA 工具集在排查中的用法导出慢查询定位光看 SQL 不够还要看 JVM 侧。这里给几个实用工具类的用法。第一个是 SQL 类型解析工具。导出接口里如果混了写操作流式查询可能读到不一致数据。用SqlUtils.getSqlType(sql)可以快速判断一条 SQL 是 SELECT 还是 UPDATEString sql select * from t_order where create_time 2024-01-01; SqlType type SqlUtils.getSqlType(sql); System.out.println(type.name()); // SELECT System.out.println(type.isRead()); // true第二个是流拷贝工具StreamUtil.io导出时如果要把数据同时写文件和发消息队列可以用它做流的分发避免重复查询。第三个是SmoothReload延时工具。导出任务如果触发了数据源重载多个连接同时重建会冲击数据库。用SmoothReload加一个随机延时把重载打散SmoothReload reload new SmoothReload(1000); reload.waitForReload(); // 执行数据源重载逻辑第四个是BeanJoinUtils。流式查询出来的订单数据如果要关联用户信息不要在循环里逐条查N1 问题用innerJoin批量关联ListOrder orders new ArrayList(); // 从 Cursor 收集一批 BeanJoinUtils.innerJoin( orders, Order::getUserId, userIds - userMapper.selectBatchIds(userIds).stream() .collect(Collectors.toMap(User::getId, u - u)), Order::setUser );注意流式查询场景下不要把所有行都收集到 List 再 join那样内存又上去了。正确做法是分批收集比如每 5000 行做一次 join 和写出。4. 验证请求与成功结果配置写完了怎么确认流式查询真的生效了给你三个验证步骤。4.1 验证 Cursor 是否逐行拉取在消费循环里打印内存和行数Transactional(readOnly true) public void verifyStream() { Runtime runtime Runtime.getRuntime(); long count 0; try (CursorOrder cursor orderMapper.streamByTimeRange(2024-01-01, 2024-12-31)) { for (Order order : cursor) { count; if (count % 100000 0) { long usedMB (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024; System.out.println(已处理 count 行堆占用 usedMB MB); } } } System.out.println(总行数 count); }如果流式查询生效你会看到堆占用在 100-300MB 之间波动不会随行数持续上涨。如果堆占用一路涨到 GB 级别说明 Cursor 没生效可能原因Mapper 返回类型写成了List、方法没有事务、或者 JDBC 驱动不支持游标。4.2 验证 Redis 缓存命中率导出前后各执行一次INFO stats对比keyspace_hits和keyspace_misses的增量# 导出前 keyspace_hits: 1200000 keyspace_misses: 80000 # 导出后 keyspace_hits: 1350000 keyspace_misses: 95000增量命中率 (1350000-1200000) / ((1350000-1200000)(95000-80000)) 150000/165000 ≈ 90.9%。如果低于 85%检查导出逻辑里是不是有大量get未命中考虑预热缓存或调整 key 设计。4.3 验证 JVM 调优效果用jstat观察 GC 频率jstat -gcutil pid 1000 10流式查询生效时老年代O占用应该稳定Full GCFGC次数不增长。如果 FGC 持续增长说明还有大对象在堆里堆积检查是不是在循环里new了大集合。成功结果参考80 万行订单导出改造前堆峰值 2.3GB、耗时 4 分 20 秒、Full GC 12 次改造后堆峰值 180MB、耗时 3 分 50 秒、Full GC 0 次。内存降了一个数量级耗时还略有下降因为减少了 GC 停顿。5. 本篇常见错误排查这一节对照真实报错给排查路径。5.1Cursor is closed或Cursor already closed报错原文java.lang.IllegalStateException: Cursor is closed原因方法没有加Transactional或者事务传播级别是NOT_SUPPORTED。流式查询依赖事务保持连接方法返回后事务提交Cursor 随之关闭。排查确认 Service 方法上有Transactional(readOnly true)且调用链上没有把事务挂起。如果用了Async注意异步方法默认不在原事务里需要单独加事务注解。5.2local proxy failed或连接超时报错原文org.apache.ibatis.exceptions.PersistenceException: local proxy failed或Connection is not available, request timed out原因连接池被流式查询占满。并发导出时每个导出占一个连接如果maximum-pool-size是 10同时 10 个导出就把池占满了其他请求拿不到连接。排查看 HikariCP 的activeConnections指标。解决方式限制并发导出数线程池核心线程数设小或者给导出单独配一个数据源。5.3Error reading from database或reading choices相关报错报错原文Error reading from database或模型调用时出现reading choices解析失败原因流式查询期间数据库连接被服务端断开比如超过wait_timeout或者模型返回的流式响应格式不符合预期。排查数据库侧调大wait_timeout和net_read_timeout模型调用侧确认 Base URL 和 Model ID 匹配TaoToken 的接入地址是https://taotoken.net/apiKey 和 Model ID 三件套要一致。5.4401 Unauthorized报错原文401 Unauthorized或invalid api key原因API Key 错误或过期。如果你在配置模型调用检查 Key 是否复制完整有没有多余空格。排查重新生成 Key确认 Base URL 是https://taotoken.net/api不要带多余路径。如果用的是 Claude Code检查配置文件里的base_url和api_key字段。5.5 OAuth 相关报错报错原文OAuth token expired或authentication failed原因如果你用的是需要 OAuth 的编码工具token 过期了。排查重新走一遍授权流程或者改用 API Key 方式接入。TaoToken 的 API Key 方式不需要 OAuth直接填 Key 即可。5.6 导出文件为空或行数不对原因流式查询的 SQL 条件写错或者事务隔离级别导致读不到数据。排查先在数据库客户端直接跑 SQL 确认有数据检查Transactional的isolation参数默认DEFAULT通常没问题如果设成了SERIALIZABLE可能锁表。6. 把流式查询、Redis 监控、JVM 调优串成一条线最后说点实操经验。流式查询不是孤立的技术点它和缓存、JVM、连接池是联动的。导出任务开始前先用 Redis 可视化工具看一眼缓存命中率命中率低就先预热导出过程中用jstat盯着 GC如果 Full GC 频繁检查是不是在循环里做了重操作导出结束后对比内存峰值和耗时确认流式查询真的生效了。如果你想让模型帮你审查这套配置可以用 TaoToken 接入Base URL 填https://taotoken.net/apiKey 和 Model ID 按需配置。把 Mapper、Service、Controller 三段代码贴进去让模型重点检查「事务边界」和「资源关闭顺序」这两块最容易出问题。需要提醒的是流式查询的Transactional会让整个导出过程处于一个长事务里。如果导出耗时几分钟数据库的 undo log 会持续增长高并发下可能影响其他事务。生产环境建议给导出单独配数据源或者用只读从库。还有一个容易忽略的点流式查询期间不要对同一张表做 DDL。MySQL 的 DDL 会隐式提交事务导致 Cursor 提前关闭。导出任务尽量安排在业务低峰期。Redis 可视化工具的选择上日常排查用 AnotherRedisDesktopManager 就够了跨平台、支持 SSH 隧道、有命中率图表。IDEA 用户可以直接装 Iedis2 插件不用切窗口。Redis Insight 适合需要看慢查询和内存分析的场景。JVM 调优方面流式查询场景下堆内存不用设太大4GB 足够重点是让对象快速回收。可以加-XX:UseG1GC -XX:MaxGCPauseMillis200G1 对大堆的停顿控制更好。如果导出任务频繁考虑用-XX:ExplicitGCInvokesConcurrent避免System.gc()触发 Full GC。这套组合拳打下来全量导出的内存占用降一个数量级是可以稳定复现的。关键是把每个环节都验证到位不要只看代码写完就上线。
返回列表