ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MyBatis 游标 Cursor 源码深度解析:流式查询的延迟取数实现原理

MyBatis 游标 Cursor 源码深度解析:流式查询的延迟取数实现原理 文档教程知识库【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址https://gitcode.com/doocs/source-code-hunter点击查看免费下载本文基于 doocs/source-code-hunter 仓库中 docs/Mybatis/核心处理层/Mybatis-Cursor.md 一文展开结合仓库内 MyBatis 核心处理层其余组件StatementHandler、Executor、SqlSession、MapperMethod的源码笔记从org.apache.ibatis.cursor包出发完整梳理 MyBatis 游标Cursor的接口设计、默认实现、状态机流转与懒加载取数机制。读完本文你将理解为什么 Mapper 方法返回CursorT时数据不是一次全部加载进内存、游标内部如何配合ResultSet逐行取数、RowBounds分页与游标如何协同以及在实际项目中安全使用游标流式查询的注意事项。一、为什么要引入 Cursor从 List 到流式查询MyBatis 常规的查询方法如selectList会将数据库返回的所有记录一次性映射为对象列表再整体返回给调用方。当查询结果集很大例如千万级数据导出、全表扫描时一次性加载所有对象会造成巨大的内存压力甚至触发 OOM。为了解决这一问题MyBatis 提供了游标Cursor机制查询结果以流的形式返回调用方每迭代一次才从数据库ResultSet中取出一行并完成映射。这样内存中同一时刻只保留少量对象真正做到了按需加载、边读边取。从源码结构看游标机制贯穿 MyBatis 的接口层到核心处理层是一条完整的调用链详见下文第六节而这一切的起点就是org.apache.ibatis.cursor.Cursor接口。二、Cursor 接口一个既是迭代器又需要关闭的游标源码位置org.apache.ibatis.cursor.Cursorpublic interface CursorT extends Closeable, IterableT { /** * 游标开始从数据库获取数据,返回true,反之false * * return true if the cursor has started to fetch items from database. */ boolean isOpen(); /** * 数据库元素都被获取,返回true,反之false * * return true if the cursor is fully consumed and has returned all elements matching the query. */ boolean isConsumed(); /** * 获取数据索引,从0开始,没有返回-1 * Get the current item index. The first item has the index 0. * * return -1 if the first cursor item has not been retrieved. The index of the current item retrieved. */ int getCurrentIndex(); }这个接口的定义非常精炼从继承关系就能读出设计意图继承IterableT说明它是一个迭代器调用方可以通过iterator()/for-each方式逐个消费数据继承Closeable说明它背后持有需要释放的资源——底层数据库ResultSet与连接使用完毕后必须关闭isOpen()返回当前游标是否已经开始从数据库取数即底层ResultSet是否已被消费isConsumed()返回游标是否已被完全消费所有符合查询条件的数据都已取出getCurrentIndex()返回当前取到的数据索引第一条数据的索引为 0如果第一条数据尚未被取出则返回 -1。这三个方法为调用方提供了一种状态自检能力在迭代过程中可以随时判断游标处于何种阶段进而决定是继续消费还是提前关闭。三、DefaultCursorCursor 接口的默认实现MyBatis 为Cursor接口提供了唯一的标准实现类DefaultCursorT位于org.apache.ibatis.cursor.defaults包。它内部聚合了结果集处理所需的一系列组件是理解游标机制的主战场。public class DefaultCursorT implements CursorT { /** * 对象包装结果处理类 */ protected final ObjectWrapperResultHandlerT objectWrapperResultHandler new ObjectWrapperResultHandler(); // ResultSetHandler stuff /** * ResultSet 处理器 */ private final DefaultResultSetHandler resultSetHandler; /** * 结果映射 */ private final ResultMap resultMap; /** * ResultSet 包装对象 */ private final ResultSetWrapper rsw; /** * 分页的 */ private final RowBounds rowBounds; /** * 游标的迭代器 */ private final CursorIterator cursorIterator new CursorIterator(); /** * 游标开启判断 */ private boolean iteratorRetrieved; /** * 游标状态,默认是创建未使用 */ private CursorStatus status CursorStatus.CREATED; /** * 分页索引,默认-1 */ private int indexWithRowBound -1; /** * 构造方法 * * param resultSetHandler * param resultMap * param rsw * param rowBounds */ public DefaultCursor(DefaultResultSetHandler resultSetHandler, ResultMap resultMap, ResultSetWrapper rsw, RowBounds rowBounds) { this.resultSetHandler resultSetHandler; this.resultMap resultMap; this.rsw rsw; this.rowBounds rowBounds; } // ... 省略方法实现见下文分节 }3.1 核心字段的职责字段类型职责objectWrapperResultHandlerObjectWrapperResultHandlerT包装结果处理器负责承接ResultSetHandler映射出的单行结果对象resultSetHandlerDefaultResultSetHandlerMyBatis 核心结果集处理器负责将ResultSet当前行映射为业务对象resultMapResultMap结果映射配置指明当前行如何映射为对象rswResultSetWrapper对 JDBCResultSet的包装提供列信息与便捷访问rowBoundsRowBounds逻辑分页参数记录 offset偏移量与 limit限额cursorIteratorCursorIterator游标内部迭代器是调用方iterator()返回的对象iteratorRetrievedboolean标记迭代器是否已被获取一个游标只能取出一次迭代器statusCursorStatus游标当前状态见 3.2 状态机indexWithRowBoundint结合分页的取数索引默认 -1从这些字段可以看出DefaultCursor并不直接持有ResultSet而是通过ResultSetWrapper rsw间接访问 JDBC 结果集真正的行映射工作也并非由游标自己完成而是委托给DefaultResultSetHandler。游标本身只负责何时取、取到后如何流转。3.2 游标状态机CREATED → OPEN → CONSUMED / CLOSEDDefaultCursor内部通过一个私有枚举CursorStatus管理游标的生命周期这是理解整个类行为的关键/** * 游标的状态 */ private enum CursorStatus { /** * 新创建的游标, ResultSet 还没有使用过 * A freshly created cursor, database ResultSet consuming has not started. */ CREATED, /** * 游标使用过, ResultSet 被使用 * A cursor currently in use, database ResultSet consuming has started. */ OPEN, /** * 游标关闭, 可能没有被消费完全 * A closed cursor, not fully consumed. */ CLOSED, /** * 游标彻底消费完毕, 关闭了 * A fully consumed cursor, a consumed cursor is always closed. */ CONSUMED }四种状态的含义如下状态含义触发时机CREATED刚创建ResultSet尚未开始消费DefaultCursor实例化时默认值OPEN使用中ResultSet已被消费每次调用fetchNextObjectFromDatabase()取数前CLOSED已关闭但可能未消费完全调用close()方法时CONSUMED完全消费完毕已隐含关闭取数时发现没有更多数据或达到 limit 上限时isOpen()与isConsumed()正是对status的简单判断Override public boolean isOpen() { return status CursorStatus.OPEN; } Override public boolean isConsumed() { return status CursorStatus.CONSUMED; }而isClosed()是内部私有方法CLOSED和CONSUMED均视为已关闭private boolean isClosed() { return status CursorStatus.CLOSED || status CursorStatus.CONSUMED; }从源码注释可以看出设计意图CONSUMED状态的游标必然是关闭的a consumed cursor is always closed而CLOSED状态的游标则可能是被调用方提前中断、并未消费完全。3.3 iterator()一个游标只能被迭代一次游标通过iterator()方法对外暴露迭代器MyBatis 对它的使用有严格的限制Override public IteratorT iterator() { // 是否获取过 if (iteratorRetrieved) { throw new IllegalStateException(Cannot open more than one iterator on a Cursor); } // 是否关闭 if (isClosed()) { throw new IllegalStateException(A Cursor is already closed.); } iteratorRetrieved true; return cursorIterator; }这里有两个关键约束单次迭代iteratorRetrieved标记一旦置为true再次调用iterator()会抛出IllegalStateException(Cannot open more than one iterator on a Cursor)。这是因为底层ResultSet是单向、不可回退的不可能支持多次遍历关闭后不可迭代若游标已经处于CLOSED或CONSUMED状态同样抛出IllegalStateException(A Cursor is already closed.)。3.4 close()关闭底层 ResultSetclose()方法体现了Cursor继承Closeable的初衷——释放底层数据库资源Override public void close() { // 判断是否关闭 if (isClosed()) { return; } ResultSet rs rsw.getResultSet(); try { if (rs ! null) { rs.close(); } } catch (SQLException e) { // ignore } finally { // 设置游标状态 status CursorStatus.CLOSED; } }实现要点幂等设计如果已经关闭则直接返回重复调用close()是安全的真正关闭的是ResultSetWrapper内部持有的 JDBCResultSetSQLException被显式忽略// ignore避免关闭资源时的异常干扰业务逻辑无论成功与否finally中都会将状态置为CLOSED。四、游标核心取数逻辑懒加载与逐行映射DefaultCursor的精华在于它的取数过程只有迭代器真正要求下一个时才去数据库取一行。这一行为由CursorIterator与ObjectWrapperResultHandler协同完成。4.1 ObjectWrapperResultHandler单行结果承接器ObjectWrapperResultHandler是DefaultCursor的受保护静态内部类实现了ResultHandlerT接口/** * 对象处理结果的包装类 * param T */ protected static class ObjectWrapperResultHandlerT implements ResultHandlerT { /** * 数据结果 */ protected T result; /** * 是否null */ protected boolean fetched; /** * 从{link ResultContext} 获取结果对象 * param context */ Override public void handleResult(ResultContext? extends T context) { this.result context.getResultObject(); context.stop(); fetched true; } }它的工作方式非常巧妙handleResult(ResultContext)被ResultSetHandler回调从中取出当前行的映射结果对象存入result字段紧接着调用context.stop()立即终止本次结果集处理——这正是每次只处理一行的关键MyBatis 常规的结果集处理会遍历完整个ResultSet而游标模式通过stop()让处理过程在取完一行后立刻停下fetched置为true作为本次是否成功取到一行的标志位供外层判断。4.2 fetchNextObjectFromDatabase()取一行的完整流程这是游标取数的发动机方法/** * 从数据库获取数据 * return */ protected T fetchNextObjectFromDatabase() { if (isClosed()) { return null; } try { objectWrapperResultHandler.fetched false; // 游标状态设置 status CursorStatus.OPEN; if (!rsw.getResultSet().isClosed()) { // 处理数据结果放入objectWrapperResultHandler resultSetHandler.handleRowValues(rsw, resultMap, objectWrapperResultHandler, RowBounds.DEFAULT, null); } } catch (SQLException e) { throw new RuntimeException(e); } // 获取处理结果 T next objectWrapperResultHandler.result; // 结果不为空 if (objectWrapperResultHandler.fetched) { // 索引1 indexWithRowBound; } // No more object or limit reached // 如果没有数据, 或者 当前读取条数 偏移量限额量 if (!objectWrapperResultHandler.fetched || getReadItemsCount() rowBounds.getOffset() rowBounds.getLimit()) { // 关闭游标 close(); status CursorStatus.CONSUMED; } // 设置结果为null objectWrapperResultHandler.result null; return next; }逐行拆解这个过程关闭检查若游标已关闭直接返回null重置标志位将fetched置为false准备新一轮取数状态流转status置为OPEN表示已开始消费ResultSet委托取数在ResultSet未关闭的前提下调用resultSetHandler.handleRowValues(...)处理当前行结果经由ObjectWrapperResultHandler.handleResult()落入result字段并因context.stop()只处理这一行结果搬移next取出本次结果若fetched true说明取到了有效行indexWithRowBound自增终止条件判断当满足以下任一条件时关闭并置为CONSUMED!objectWrapperResultHandler.fetchedResultSet已无更多数据handleRowValues没有触发任何回调getReadItemsCount() rowBounds.getOffset() rowBounds.getLimit()读取条数已达到偏移量 限额量即配合RowBounds取够了目标行数清空承接器将result置回null避免脏数据残留同时保证下一次hasNext()能依据fetched正确判断是否需要再取数。注意这里的getReadItemsCount()定义/** * 下一个索引 * return */ private int getReadItemsCount() { return indexWithRowBound 1; }即已读取条数 当前索引 1。4.3 CursorIterator对外暴露的懒加载迭代器CursorIterator是DefaultCursor的内部类实现了java.util.IteratorT调用方拿到的就是它/** * 游标迭代器 */ protected class CursorIterator implements IteratorT { /** * 下一个数据 * Holder for the next object to be returned. */ T object; /** * 下一个的索引 * Index of objects returned using next(), and as such, visible to users. */ int iteratorIndex -1; /** * 是否有下一个值 * return */ Override public boolean hasNext() { if (!objectWrapperResultHandler.fetched) { object fetchNextUsingRowBound(); } return objectWrapperResultHandler.fetched; } /** * 下一个值 * return */ Override public T next() { // Fill next with object fetched from hasNext() T next object; if (!objectWrapperResultHandler.fetched) { next fetchNextUsingRowBound(); } if (objectWrapperResultHandler.fetched) { objectWrapperResultHandler.fetched false; object null; iteratorIndex; return next; } throw new NoSuchElementException(); } /** * 不可执行抛出异常 */ Override public void remove() { throw new UnsupportedOperationException(Cannot remove element from Cursor); } }这段代码体现了典型的懒加载迭代器设计配合DefaultCursor.hasNext/next的标准 Java 迭代协议hasNext()只有当fetched为false当前没有缓存结果时才真正调用fetchNextUsingRowBound()去数据库取数并暂存到object字段返回值由fetched决定。因此实际的数据读取发生在hasNext()而非next()next()优先复用hasNext()阶段预取的对象若当前没有预取结果则再取一次取到后重置fetched、清空object、iteratorIndex自增并返回若确实无数据则抛出NoSuchElementExceptionremove()游标只读、不可删除元素直接抛出UnsupportedOperationException。iteratorIndex是对用户可见的索引自 0 开始随next()成功调用递增它与 3.4 节关闭的ResultSet一样共同支撑起getCurrentIndex()的计算。4.4 fetchNextUsingRowBound()RowBounds 偏移量处理getCurrentIndex()与偏移量计算依赖rowBounds而跳过偏移量的逻辑在fetchNextUsingRowBound()中/** * 去到真正的数据行 * return */ protected T fetchNextUsingRowBound() { T result fetchNextObjectFromDatabase(); while (objectWrapperResultHandler.fetched indexWithRowBound rowBounds.getOffset()) { result fetchNextObjectFromDatabase(); } return result; }实现思路先取一行只要取到了数据且当前索引仍小于 offset就继续取下一行直到越过偏移量到达真正的目标起始行。也就是说游标模式下RowBounds的 offset 是通过实际读取并丢弃前 offset 行来实现的。而getCurrentIndex()的返回值为Override public int getCurrentIndex() { return rowBounds.getOffset() cursorIterator.iteratorIndex; }即偏移量 迭代器索引这样返回给调用方的索引是相对整个结果集而言的绝对位置而非相对游标起点的位置。五、状态流转全景一次完整迭代的时序将前面几节串联起来一次典型的游标消费过程如下查询方法返回DefaultCursor实例此时status CREATEDiteratorRetrieved falseindexWithRowBound -1调用方执行cursor.iterator()校验通过后iteratorRetrieved true返回CursorIteratorfor-each首次触发hasNext()fetched为false→ 调用fetchNextUsingRowBound()→fetchNextObjectFromDatabase()status置为OPENresultSetHandler.handleRowValues(...)取第一行若成功fetched true、indexWithRowBound递增为 0若RowBounds.offset 0while 循环继续丢弃前几行若已无数据或达到 limitclose()并置为CONSUMEDnext()返回暂存对象iteratorIndex递增重复 3、4 直到hasNext()返回false底层ResultSet耗尽或达到 limit此时游标已自动进入CONSUMED状态若中途需要中断如提前 break调用方应主动调用cursor.close()使状态进入CLOSED并关闭底层ResultSet。六、游标的完整调用链从 Mapper 方法到 DefaultCursor游标并非孤立组件它在 MyBatis 中贯穿接口层 → 核心处理层。结合仓库中其他源码笔记可以还原出完整链路6.1 触发点MapperMethod 根据返回值类型分流MyBatis 会为 Mapper 接口方法解析方法签名判断返回值类型。在 Mybatis-MethodSignature.md 中可以看到MethodSignature专门记录了returnsCursor标志/** * 返回的是否是一个游标 */ private final boolean returnsCursor; // ... this.returnsCursor Cursor.class.equals(this.returnType);当方法返回类型恰好是Cursor或其子类型判断时returnsCursor true。随后在MapperMethod.execute()见 Mybatis-MapperMethod.md中根据该标志选择执行路径} else if (method.returnsCursor()) { result executeForCursor(sqlSession, args); }即只要 Mapper 方法声明返回CursorTMyBatis 就会走游标查询分支而不会走selectList全量加载路径。6.2 接口层SqlSession.selectCursorSqlSession接口为游标查询提供了三个重载见 6、SqlSession组件.md// 除了返回值是Cursor对象其它与selectList相同 T CursorT selectCursor(String statement); T CursorT selectCursor(String statement, Object parameter); T CursorT selectCursor(String statement, Object parameter, RowBounds rowBounds);它最终会调用Executor的queryCursor()方法。6.3 核心层Executor → StatementHandler → ResultSetHandler在 5、Executor组件.md 中Executor接口定义了E CursorE queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;SimpleExecutor.doQueryCursor()的实现是Override protected E CursorE doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { Configuration configuration ms.getConfiguration(); StatementHandler handler configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql); Statement stmt prepareStatement(handler, ms.getStatementLog()); return handler.EqueryCursor(stmt); }ReuseExecutor、BatchExecutor的doQueryCursor()与doQuery()实现类似BatchExecutor会先flushStatements()保证读到最新数据。随后链路进入 4、StatementHandler.mdRoutingStatementHandler.queryCursor()委托给具体策略SimpleStatementHandler/PreparedStatementHandlerPreparedStatementHandler.queryCursor()执行 SQL 后调用resultSetHandler.handleCursorResultSets(ps)Override public E CursorE queryCursor(Statement statement) throws SQLException { PreparedStatement ps (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleCursorResultSets(ps); }DefaultResultSetHandler.handleCursorResultSets()会从ResultSet构造ResultSetWrapper并结合resultMap、rowBounds创建出DefaultCursor实例返回——这正是本文第三、四节分析的对象的诞生之处。6.4 一条完整的调用链Mapper 接口方法 (返回 CursorT) └─ MapperMethod.execute() → executeForCursor() [returnsCursor 分流] └─ SqlSession.selectCursor(statement, param, rowBounds) └─ Executor.queryCursor(ms, param, rowBounds) └─ SimpleExecutor.doQueryCursor() └─ StatementHandler.queryCursor(stmt) └─ PreparedStatementHandler.queryCursor(ps) └─ DefaultResultSetHandler.handleCursorResultSets(ps) └─ new DefaultCursor(resultSetHandler, resultMap, rsw, rowBounds) └─ 调用方 iterator() → CursorIterator └─ hasNext()/next() → fetchNextUsingRowBound() └─ fetchNextObjectFromDatabase() └─ resultSetHandler.handleRowValues() 逐行映射七、实战使用建议与注意事项基于上述源码机制在实际项目中使用游标流式查询时有几个必须遵守的约定务必在 try-with-resources 或 finally 中关闭游标。DefaultCursor持有底层的 JDBCResultSet即使迭代完全结束会自动置为CONSUMED中途提前中断break、异常时也应及时close()释放数据库资源避免连接泄漏游标不可复用。iterator()只能调用一次否则抛出IllegalStateException不要尝试对同一个游标进行多次遍历迭代期间保持 SqlSession 与连接存活。由于取数是懒加载的hasNext()时才真正访问ResultSet如果在迭代完成前关闭了SqlSession或归还连接后续取数将失败RowBounds.offset有实际读取代价。游标模式不会像 SQL 分页那样在数据库层跳过前 N 行而是逐行读取并丢弃 offset 之前的行见 4.4 节offset 很大时会产生额外开销游标只读。remove()方法直接抛异常不要尝试通过迭代器修改数据配合fetchSize使用效果更佳。BaseStatementHandler.setFetchSize()会读取MappedStatement或全局配置的fetchSize并设置到 JDBCStatement上见 4、StatementHandler.md合理设置fetchSize可控制每次从数据库拉取到客户端的行数进一步优化流式查询的内存占用。八、小结本文以 Mybatis-Cursor.md 为主线从Cursor接口的三个方法出发深入剖析了DefaultCursor的字段设计、CursorStatus四态状态机、ObjectWrapperResultHandler的单行承接机制以及CursorIterator懒加载取数的完整流程并结合仓库中MethodSignature、MapperMethod、SqlSession、Executor、StatementHandler等源码笔记还原了游标从 Mapper 方法到DefaultCursor的完整调用链。可以看到MyBatis 游标的设计核心在于将结果集处理从一次全量拆解为逐行触发——ResultHandler的context.stop()中断机制 Iterator协议 状态机三者配合在保持ResultSet打开的前提下实现了真正的流式消费。理解这一机制不仅能让你在数据导出、全表扫描等大结果集场景下写出内存安全的代码也能更深入地体会 MyBatis 在接口层易用性与底层资源可控性之间所做的精妙权衡。延伸阅读docs/Mybatis/核心处理层/Mybatis-Cursor.md本文主线的原始源码笔记docs/Mybatis/核心处理层/Mybatis-MethodSignature.mdreturnsCursor返回值类型判定docs/Mybatis/核心处理层/Mybatis-MapperMethod.mdexecuteForCursor分流逻辑docs/Mybatis/核心处理层/6、SqlSession组件.mdselectCursor接口定义与DefaultSqlSession实现docs/Mybatis/核心处理层/5、Executor组件.mdExecutor.queryCursor()与三种 Executor 实现docs/Mybatis/核心处理层/4、StatementHandler.mdqueryCursor()与handleCursorResultSets()的调用赞分享文档教程知识库【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址https://gitcode.com/doocs/source-code-hunter点击查看免费下载相关推荐Mybatis Cursor 游标机制源码解析从接口定义到 DefaultCursor 的按需取数实现Mybatis Cursor 游标机制源码解析从接口定义到 DefaultCursor 的按需取数实现 导读 当一次查询可能返回海量数据例如导出百万级记录、文档教程技术博客知识库pgx查询取消CancelRequest实现原理深度剖析pgx查询取消CancelRequest实现原理深度剖析 引言被阻塞的查询困境与解决方案 在高并发PostgreSQL数据库应用中长时间运行的查询可能导致数据库后端LlamaIndex 查询引擎流式输出Streaming实战指南从零延迟体验到源码级原理LlamaIndex 查询引擎流式输出Streaming实战指南从零延迟体验到源码级原理 本篇技术指南以 LlamaIndex 官方文档 Streamin人工智能RAG大模型上一篇OpCore-Simplify三步搞定Hackintosh配置的终极指南下一篇Encog多线程训练指南充分利用多核CPU加速机器学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表