MyBatis 游标本来是为省内存Options(resultSetType FORWARD_ONLY)配selectCursor逐条消费。可百万级查询依然 OOM 时与其调 JVM 参数不如让 Codex 检查整条游标链路Codex 要稳定可用可以先通过 TaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content创建 API Key再把 Base URL https://taotoken.net/api 填进模型配置。1. 百万行查询还在 OOM先确认游标是否真的“只向前的”原作者遇到的是典型场景department表有百万级数据一次SELECT *全部载入内存很容易触发OutOfMemoryError。他的解法是用Options(resultSetType ResultSetType.FORWARD_ONLY)配合sqlSession.selectCursor()把一次性装载改成逐条读取。这个方向在理论上是成立的但在实际项目里你按这个模板写完仍然 OOM通常是“游标”并没有真正进入流式状态而不是代码逻辑写错了。1.1 游标省内存的前提是驱动配合游标不是查询优化它只是改变了结果集的读取节奏。FORWARD_ONLY告诉 JDBC 驱动“我只能从前往后读”驱动才有底气把已读的行丢掉如果结果集支持回退或跳跃驱动就必须把读过的行保存在客户端内存自然降不下来。但这里还有一个隐藏变量具体的数据库驱动是否真的实现了“只进结果集 流式读取”。MySQL Connector/J、Oracle JDBC、PostgreSQL JDBC 对fetchSize的默认处理不同有的驱动即使看到FORWARD_ONLY仍会先把所有行拉回客户端内存。所以排查时要同时检查 MyBatis 配置和驱动行为不能只看Options。1.2 滚动类型越强MyBatis 越不敢丢数据resultSetType的三个取值对应结果集的移动能力。FORWARD_ONLY只能向下滚动TYPE_SCROLL_INSENSITIVE可以上下滚动且不受数据库后续变化影响TYPE_SCROLL_SENSITIVE可以上下滚动且能看到变化。MyBatis 一旦检测到你有滚动需求就会把读过的那部分数据留在堆里方便游标回退这等于把“省内存”的目标直接架空。如果你把类型设成了可滚动或者全局配置里覆盖了resultSetType那么游标写得再标准也救不了内存。2. 照原文 selectCursor 写法最容易被忽略的三个陷阱原作者的示例是标准姿势Mapper 方法上加OptionsService 里用sqlSessionTemplate.getSqlSessionFactory().openSession()手动开 session再通过sqlSession.selectCursor(statement)拿到游标最后在finally中关闭 cursor 和 sqlSession。代码本身没问题但照进真实项目后有三个地方可能让它悄悄失效。2.1 陷阱一注解上的 resultSetType 被全局配置覆盖Spring Boot 项目里application.yml可能配了mybatis.configuration.default-result-set-type或者SqlSessionFactoryBean里设置了默认值。全局配置和注解同时存在时最终生效的值不一定是你写在Options里的那个。Codex 排查时会先翻全局配置再回到 Mapper 方法上核对Options确认真正被执行的 statement 到底带的是什么resultSetType。2.2 陷阱二方法签名没有落到 Cursor 重载sqlSession.selectCursor(String statement)只会对返回类型为Cursor的 MappedStatement 生效。如果你的 Mapper 方法签名是ListDepartmentEntityMyBatis 仍会走普通查询路径所有结果被一次性组装成列表内存压力一点没减。另外还要注意 MyBatis 版本3.4.0 对注解返回Cursor的支持并不完整3.4.1 才修复。老项目如果用的是早期版本即使签名写对了也可能踩到已知 bug。把这些版本信息一起喂给 Codex比你自己翻 release note 快得多。2.3 陷阱三驱动没进入流式读取也没有对应的 fetchSizeFORWARD_ONLY只是 JDBC 层的“许可”驱动可以自由决定是否真的逐行拉取。MySQL Connector/J 在部分版本中需要额外设置fetchSize才会走流式读取Oracle JDBC 对setFetchSize的响应也不等同于“只取一行”。原作者把重点放在resultSetType上是合理的但在排障场景里你还需要检查fetchSize是否被正确设置。下面这段是适合交给 Codex 检查的 Mapper 写法把fetchSize也显式带上Options(resultSetType ResultSetType.FORWARD_ONLY, fetchSize 500) Select(SELECT * FROM department WHERE status 0) CursorDepartmentEntity scanActiveDepartments();3. 把排查交给 Codex用 TaoToken 建一条稳定 API 通道游标 OOM 这类问题很符合“代码短、链路长”的特征Mapper 十几行Service 二十几行但它们之间隔着 MyBatis 配置、驱动行为、连接池复用方式。用 Codex 去检查这条链路是个好选择但很多开发者的 Codex 卡在了模型额度、多 Key 切换、供应商配置这些环节。这里我用 TaoToken 做统一接入通道让 Codex 通过同一个 Base URL 拿到可用模型。3.1 打开官网注册并创建 API Key先打开 TaoToken完成注册后进入 API Keys 页面创建一把新 Key。把 Key 存在本地后续所有模型调用都用这个YOUR_API_KEY占位符来代替。如果你已经在其他 AI 编程工具里用过 TaoToken也可以复用同一把 Key这样在 TaoToken 控制台看调用量时所有工具的消耗都落在同一个账号下方便排查时对账。3.2 Codex 的 config.toml 与 Base URLCodex 通过~/.codex/config.toml管理模型供应商。自定义供应商的配置大致如下model_providers [ { name taotoken, base_url https://taotoken.net/api, env_key TAOTOKEN_API_KEY, wire_api chat } ] model taotoken/YOUR_MODEL_ID这里有两个容易混淆的点。base_url必须是 https://taotoken.net/api末尾不要加/v1也不要塞 UTM 参数官网落地页 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 是给人注册、看模型广场、看用量用的和填进工具的接口地址是两个东西。设置完环境变量后在项目里启动 Codex 试问一句“这个 selectCursor 的关闭顺序有没有问题”收到流式回复就说明通道已通export TAOTOKEN_API_KEYYOUR_API_KEY3.3 模型 ID 去哪里核对model taotoken/YOUR_MODEL_ID里的具体模型 ID以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 的模型广场当时列表为准不要凭记忆填也不要套用旧教程里带日期的模型名。模型广场上展示的 ID 和你当前套餐是否匹配直接决定 Codex 会不会返回 model not found。4. Codex 检查游标时会动哪些文件哪些必须你在本机跑通道配好后先整理现场材料再提问。Codex 需要看到的不只是一句“OOM 了”而是以下内容Mapper 接口完整代码、Service 里selectCursor的使用片段、mybatis-config.xml或application.yml中关于resultSetType和fetchSize的配置、数据库类型和驱动版本、完整 OOM 堆栈。信息越完整Codex 给的诊断越能靠近根因而不是给一堆泛泛的“检查内存泄漏”建议。4.1 Codex 的检查顺序常见顺序是第一步核对 Mapper 方法上的Options是否真的生效第二步检查selectCursor的 statement 名称是否对应返回Cursor的方法第三步看Cursor和SqlSession是否在finally或try-with-resources里关闭干净第四步结合驱动类型给出fetchSize建议。它还可能建议你临时去掉 Service 里的业务转换逻辑只做裸读先用最小代码确认内存是否还在涨。4.2 生成 SQL 和改 JVM 参数执行权留在本地Codex 可以生成诊断 SQL、解释执行计划、给出 JVM 参数调整建议但它不应该直接连你的生产库去执行诊断 SQL也不应该让它调用regsvr32、impdp这类系统级操作。你要扮演执行者Codex 给出 SQL你在本地 SQL*Plus 或数据库客户端里运行把行数和耗时贴回对话Codex 建议调-Xmx你在服务器上改完重启把新的堆转储结果再交给它分析。这样既保留了 AI 的排查速度又不会让它拿到生产权限乱执行。4.3 接入层常见报错和对应处理Codex 走 TaoToken 通道时最常见的两个报错是 401 和 404。401 说明TAOTOKEN_API_KEY环境变量没生效或 Key 复制错了回到 API Keys 页面重新创建并导出404 基本是model里的 ID 不在模型广场列表内去模型广场复制准确 ID。另一个容易踩的是请求地址拼错有人习惯性在 Base URL 后面加/v1导致路径不匹配TaoToken 填进 Codex 的位置只认 https://taotoken.net/api。5. 压测验证后回 TaoToken 控制台对一下这次调用Codex 帮你改完配置后验证不能省。最稳妥的办法是拿一张 10 万行的表先跑一遍原查询用jstat -gc pid观察 Old 区趋势看内存是否还随读取行数线性上升确认稳定后再把数据量提到与生产接近的级别比如 50 万、100 万行。如果 Old Gen 保持平稳说明FORWARD_ONLY和驱动fetchSize的组合真正生效了。5.1 用模型对话确认模型 ID 可用如果你中途切换过模型建议先到 TaoToken 模型对话 里用同一把 Key 发一条测试消息确认这个模型 ID 能被服务端解析。这样后续 Codex 的每一次回答都建立在可用模型之上不会把“模型调不通”误判成“MyBatis 游标写法有问题”。5.2 回控制台看消耗来源排查过程可能来回调十几次 Codex耗时和 token 消耗都不少。隔一段时间到 API Keys 控制台 看下调用记录确认请求都落在你新创建的YOUR_API_KEY上。如果接下来还有类似的 MyBatis 批量任务问题要持续排查可以提前看一下 Coding Plan 的套餐额度按自己的调用频率选档避免排查到一半卡在额度上。我个人比较喜欢这套流程先让 Codex 列出“要查哪几个配置”再在本地小范围压测最后才动大表的全量任务。顺序不乱的话游标 OOM 基本能定位到具体环节而不是把问题归结成一句“内存不够”。