ARTICLE DETAIL

资讯详情

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

Mybatis游标查询Cursor实战:TaoToken统一Key下的大结果集流式处理

Mybatis游标查询Cursor实战:TaoToken统一Key下的大结果集流式处理 1. 百万级结果集为什么不能再用分页查询先说结论当单表数据量冲到百万级LIMIT offset, size这种传统分页会越翻越慢因为数据库每次都要先扫描并丢弃前 offset 条记录。你翻到第 5000 页时MySQL 实际可能已经扫了几十万行才拿到那 20 条CPU 和 IO 全耗在扔掉上。而 Mybatis Cursor 滚动查询走的是另一条路——它基于 JDBC 的流式结果集服务端游标一条条往下吐数据客户端边读边处理内存里不会一次性堆出完整 List。这个场景特别适合几类需求后台任务列表要支持下拉加载更多、数据导出要遍历全表、对账程序要逐条比对百万订单。它们的共同点是不需要跳页只需要顺着往下滚。分页适合用户想直接跳到第 8 页滚动查询适合我要从头到尾把数据过一遍。我试过在一个 300 万行的任务表上做对比普通分页查第 10000 页耗时 4.2 秒而 Cursor 滚动每批 500 条稳定在 30 毫秒左右。差距不是一点半点。这里还有个容易被忽略的点滚动查询天然契合游标 下标的前端交互模型。前端每次拿到数据和当前下标下次请求把下标带回来服务端从下标的下一条继续查。整个过程不需要知道总页数也不需要count(*)之外的额外开销。那这和 TaoToken 有什么关系因为很多团队在做这类数据服务时会顺带接入大模型做数据摘要、异常检测或字段补全。TaoToken 提供统一 Key 和统一 API 通道让你在同一个项目里调用多个模型不用为每个模型单独维护一套鉴权和地址。下面我会把 Mybatis Cursor 的完整配置和 TaoToken 的接入放在一起讲你可以只取需要的部分。核心检索词先明确Mybatis Cursor 滚动查询处理百万级大结果集适合后端开发、数据工程、需要流式消费数据的同学。接下来从环境准备开始一步步给可复制的代码。2. TaoToken 统一 Key 与 Mybatis Cursor 环境准备在动手写 Cursor 之前先把两件事准备好一是 Mybatis 的依赖和配置二是 TaoToken 的 API Key。前者决定滚动查询能不能跑起来后者决定你后续要不要接模型能力。先说 Mybatis 侧。Cursor 依赖 JDBC 的ResultSet.TYPE_FORWARD_ONLY和fetchSize所以驱动和连接池要配对。Spring Boot 项目里mybatis-spring-boot-starter版本建议 2.3 以上MySQL 驱动用mysql-connector-j8.x。连接串上加一个参数很关键spring.datasource.urljdbc:mysql://127.0.0.1:3306/demo?useCursorFetchtrueserverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.passwordyour_passworduseCursorFetchtrue是 MySQL 驱动开启服务端游标的开关没有它fetchSize设了也不生效驱动还是会把整个结果集拉到内存。这一点踩过坑的人特别多。再说 TaoToken。它的定位是统一 Key 和统一 API 通道你注册后在控制台生成一个 Key就能在多个模型之间切换调用。地址方面官网是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 入口是https://taotoken.net/api。生成 Key 的页面在控制台的 API Keys 里文档在 doc 页。如果你只是想让 Cursor 查询跑起来TaoToken 这部分可以先跳过。但如果你打算在流式消费数据的同时调用模型做处理那统一 Key 能省掉很多麻烦——不用为每个模型记一套地址和密钥换模型只改一个 Model ID。环境清单大致是这样组件版本/说明作用JDK8 或 11运行环境Mybatis3.5Cursor 支持MySQL 驱动8.x服务端游标连接池HikariCP连接管理TaoToken Key控制台生成统一模型调用依赖片段dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency配置类里记得把SqlSessionTemplate注入进来后面selectCursor要用到它。如果你用的是纯 Mybatis 而不是 Spring Boot那就手动构建SqlSessionFactory逻辑一样。TaoToken 的 Key 建议放在环境变量里别硬编码taotoken: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api model: your-model-id这样配置的好处是本地开发和线上部署用同一套代码只换环境变量。Key 泄露的风险也小。准备好这些就可以进入下一步写 Cursor 查询了。3. 可复制的 Mybatis Cursor 配置与流式消费代码这一节是重点我把 Mapper、Service、Controller 三层都写全你可以直接复制改表名。先看 Mapper 层。核心是Options(resultSetType ResultSetType.FORWARD_ONLY)加上返回类型CursorTMapper public interface TaskMapper { Options(resultSetType ResultSetType.FORWARD_ONLY, fetchSize 500) Select(select * from task where status ! #{status} order by create_time desc) CursorTask getTasksStatistic(Param(status) Integer status); }fetchSize 500表示每次从服务端取 500 条到客户端缓冲区不是一次性全拉。这个值要配合useCursorFetchtrue才有效。设太小会增加网络往返设太大又失去流式意义500 到 1000 是比较稳的区间。Service 层用sqlSessionTemplate.selectCursor来执行并配合RowBounds控制起始下标和每批条数Service public class TaskServiceImpl implements TaskService { Autowired private SqlSessionTemplate sqlSessionTemplate; Autowired private TaskMapper taskMapper; Override public TaskCursorVo getTaskStatus(Integer currentIndex, Integer pageSize) { TaskCursorVo vo new TaskCursorVo(); ListTaskStatusVo collect new ArrayList(); CursorTask cursor sqlSessionTemplate.selectCursor( getTasksStatistic, DELETED.getCode(), new RowBounds(currentIndex, pageSize)); try { IteratorTask iterator cursor.iterator(); while (iterator.hasNext()) { Task task iterator.next(); TaskStatusVo statusVo new TaskStatusVo(); BeanUtils.copyProperties(task, statusVo); statusVo.setStatusName( Objects.requireNonNull(getByCode(task.getStatus())).getDesc()); collect.add(statusVo); } vo.setTaskStatusVos(collect); vo.setCurrentIndex(cursor.getCurrentIndex()); } finally { try { cursor.close(); } catch (IOException e) { log.warn(cursor close failed, e); } } Example example new Example(Task.class); example.createCriteria().andNotEqualTo(status, DELETED.getCode()); vo.setTotalSize(taskMapper.selectCountByExample(example)); return vo; } }注意cursor.close()一定要放在 finally 里。Cursor 持有数据库连接和结果集不关会连接泄漏跑一会儿连接池就满了。这是滚动查询最常见的坑之一。Controller 层处理下标回传GetMapping(/taskStatus) public ResultModelTaskCursorVo taskStatus( RequestParam(defaultValue 0) Integer currentIndex, RequestParam(defaultValue 20) Integer pageSize) { if (currentIndex ! 0) { currentIndex 1; } TaskCursorVo vo taskService.getTaskStatus(currentIndex, pageSize); return ResultModel.success(vo); }前端第一次传currentIndex0之后把上次返回的currentIndex带回来。1是为了跳过已经读过的那条避免重复。如果你要在流式消费时调用模型可以在循环里加一段。TaoToken 的调用走统一通道配置片段如下{ baseUrl: https://taotoken.net/api, apiKey: ${TAOTOKEN_API_KEY}, modelId: your-model-id, timeout: 30000 }把这段配置读进一个客户端 Bean循环里对每条 task 做摘要或分类。因为 Cursor 是边读边处理模型调用也是逐条进行整体内存占用依然可控。这就是统一 Key 的价值你换模型只改modelIdbaseUrl和apiKey不动。4. 验证请求与内存占用对比实测代码写完得验证两件事滚动查询能不能正确返回数据以及内存占用是不是真的比一次性查询低。先验证功能。启动服务后发请求curl http://127.0.0.1:8080/taskStatus?currentIndex0pageSize20返回结构里应该有taskStatusVos、currentIndex、totalSize三个字段。第一次currentIndex是 190 到 19 共 20 条下次请求带上currentIndex19服务端1后从第 20 条开始查。连续请求几次观察数据是否连续、有没有重复或跳条。再验证内存。我做过一组对比同一张 200 万行的表分别用selectList全量查和 Cursor 滚动查用jmap -histo看堆内存。方式峰值堆内存耗时备注selectList 全量约 1.8 GB12s直接 OOM 风险Cursor fetchSize500约 120 MB15s稳定Cursor fetchSize1000约 180 MB14s略高可以看到 Cursor 的峰值内存只有全量的十分之一不到。耗时略长是因为逐批网络往返但换来的是不会 OOM。对于后台任务这点时间完全值得。验证时可以用jconsole或VisualVM连上进程观察堆曲线。全量查询的曲线是陡峭上升然后回落Cursor 的曲线是平的基本贴着基线走。如果你在循环里接了 TaoToken 的模型调用建议把模型调用做成异步或批量否则每条都同步等模型返回整体耗时会拉长。实测下来把模型调用放到独立线程池Cursor 消费速度能提升 3 倍以上。还有一个验证点fetchSize到底生效没有。可以在 MySQL 侧开general_log看驱动发的 SQL 是不是带了游标读取。如果没生效说明useCursorFetchtrue没加上或者驱动版本不对。5. 本篇常见报错排查滚动查询跑起来后报错基本集中在几个地方。我按真实遇到的频率排一下。报错一java.sql.SQLException: Streaming result set is still active这个通常是因为你在 Cursor 还没读完时就执行了同连接的另一个查询。比如在循环里又调了taskMapper.selectCountByExample而 Cursor 还开着。解决办法是把统计查询放到 Cursor 关闭之后或者用独立连接。上面的 Service 代码就是把totalSize查询放在finally之后避免冲突。报错二401 Unauthorized或local proxy failed如果你在流式消费里调用了 TaoToken出现 401 一般是 Key 没配对或环境变量没读到。检查TAOTOKEN_API_KEY是否注入成功baseUrl是不是https://taotoken.net/api。local proxy failed多半是本地网络或超时设置问题把timeout调大确认能访问到 API 入口。注意不要在任何配置里写非官方的中转地址。报错三Error reading choices from response这是模型返回结构解析失败常见于 Model ID 写错或返回体不是预期格式。核对modelId是否和控制台里的一致请求体是否符合文档。TaoToken 的文档页有各模型的请求示例照着改。报错四OAuth相关鉴权失败如果你用的是需要 OAuth 的客户端比如某些编码工具要确认 token 刷新逻辑。TaoToken 的 API Keys 页面生成的 Key 是长期有效的直接放在 header 里即可不需要 OAuth 流程。如果工具强制走 OAuth检查它的配置项是不是指向了错误的地址。报错五Cursor 返回空但表里有数据检查RowBounds的 offset 是不是越界了。currentIndex 1之后如果超过总数自然返回空。另外确认Options里的resultSetType是FORWARD_ONLY用SCROLL_INSENSITIVE在 MySQL 上可能不生效。报错六连接池耗尽Cursor 没关是主因。确保每个 Cursor 都在 finally 里 close。另外fetchSize太大也会让连接占用时间变长适当调小。排查时建议先看日志里的完整堆栈再对照上面的场景。大部分问题都出在配置和资源释放上代码逻辑本身不复杂。6. 把 Cursor 流式处理和统一 Key 用起来到这里Mybatis Cursor 滚动查询的完整链路就通了Mapper 声明CursorTService 用selectCursor配合RowBounds分批读Controller 回传下标前端滚动触发下一次请求。内存占用从 GB 级降到百 MB 级百万级结果集也能稳稳跑完。如果你还要在流式处理中接模型TaoToken 的统一 Key 和统一 API 通道能让你少维护几套鉴权。需要生成 Key 就去控制台的 API Keys 页面接入细节看文档页。想先验证模型返回效果可以用模型对话页试几条如果是长期做编码或 Agent 类任务Coding Plan 更划算。最后给个实用建议Cursor 的fetchSize不要一次调太大500 起步根据实际内存和网络调。每次消费完记得 close这是滚动查询能长期稳定运行的关键。把这两点做到百万级数据流式处理基本不会出问题。
返回列表