MyBatis 游标本来是为省内存:@Options(resultSetType = FORWARD_ONLY)配selectCursor逐条消费。可百万级查询依然 OOM 时,与其调 JVM 参数,不如让 Codex 检查整条游标链路;Codex 要稳定可用,可以先通过 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_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 方法上加@Options,Service 里用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 方法签名是List<DepartmentEntity>,MyBatis 仍会走普通查询路径,所有结果被一次性组装成列表,内存压力一点没减。另外还要注意 MyBatis 版本:3.4.0 对注解返回Cursor的支持并不完整,3.4.1 才修复。老项目如果用的是早期版本,即使签名写对了也可能踩到已知 bug。把这些版本信息一起喂给 Codex,比你自己翻 release note 快得多。
2.3 陷阱三:驱动没进入流式读取,也没有对应的 fetchSize
FORWARD_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") Cursor<DepartmentEntity> 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 URL
Codex 通过~/.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_source=taotoken_aicg_blog_end&utm_content= 是给人注册、看模型广场、看用量用的,和填进工具的接口地址是两个东西。设置完环境变量后,在项目里启动 Codex 试问一句“这个 selectCursor 的关闭顺序有没有问题”,收到流式回复就说明通道已通:
export TAOTOKEN_API_KEY="YOUR_API_KEY"3.3 模型 ID 去哪里核对
model = "taotoken/YOUR_MODEL_ID"里的具体模型 ID,以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_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 基本能定位到具体环节,而不是把问题归结成一句“内存不够”。