- 文档
- 教程
- 知识库
【免费下载链接】source-code-hunter
😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等
本文基于 doocs/source-code-hunter 仓库中 docs/Mybatis/核心处理层/Mybatis-Cursor.md 一文展开,结合仓库内 MyBatis 核心处理层其余组件(StatementHandler、Executor、SqlSession、MapperMethod)的源码笔记,从
org.apache.ibatis.cursor包出发,完整梳理 MyBatis 游标(Cursor)的接口设计、默认实现、状态机流转与懒加载取数机制。读完本文,你将理解:为什么 Mapper 方法返回Cursor<T>时数据不是一次全部加载进内存、游标内部如何配合ResultSet逐行取数、RowBounds分页与游标如何协同,以及在实际项目中安全使用游标流式查询的注意事项。
一、为什么要引入 Cursor:从 List 到流式查询
MyBatis 常规的查询方法(如selectList)会将数据库返回的所有记录一次性映射为对象列表,再整体返回给调用方。当查询结果集很大(例如千万级数据导出、全表扫描)时,一次性加载所有对象会造成巨大的内存压力,甚至触发 OOM。
为了解决这一问题,MyBatis 提供了游标(Cursor)机制:查询结果以"流"的形式返回,调用方每迭代一次,才从数据库ResultSet中取出一行并完成映射。这样内存中同一时刻只保留少量对象,真正做到了按需加载、边读边取。
从源码结构看,游标机制贯穿 MyBatis 的接口层到核心处理层,是一条完整的调用链(详见下文第六节),而这一切的起点,就是org.apache.ibatis.cursor.Cursor接口。
二、Cursor 接口:一个既是迭代器又需要关闭的"游标"
源码位置:org.apache.ibatis.cursor.Cursor
public interface Cursor<T> extends Closeable, Iterable<T> { /** * 游标开始从数据库获取数据,返回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(); }这个接口的定义非常精炼,从继承关系就能读出设计意图:
- 继承
Iterable<T>:说明它是一个迭代器,调用方可以通过iterator()/for-each方式逐个消费数据; - 继承
Closeable:说明它背后持有需要释放的资源——底层数据库ResultSet与连接,使用完毕后必须关闭; isOpen():返回当前游标是否已经开始从数据库取数(即底层ResultSet是否已被消费);isConsumed():返回游标是否已被完全消费(所有符合查询条件的数据都已取出);getCurrentIndex():返回当前取到的数据索引,第一条数据的索引为 0,如果第一条数据尚未被取出则返回 -1。
这三个方法为调用方提供了一种"状态自检"能力:在迭代过程中可以随时判断游标处于何种阶段,进而决定是继续消费还是提前关闭。
三、DefaultCursor:Cursor 接口的默认实现
MyBatis 为Cursor接口提供了唯一的标准实现类DefaultCursor<T>(位于org.apache.ibatis.cursor.defaults包)。它内部聚合了结果集处理所需的一系列组件,是理解游标机制的"主战场"。
public class DefaultCursor<T> implements Cursor<T> { /** * 对象包装结果处理类 */ protected final ObjectWrapperResultHandler<T> 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 核心字段的职责
| 字段 | 类型 | 职责 |
|---|---|---|
objectWrapperResultHandler | ObjectWrapperResultHandler<T> | 包装结果处理器,负责承接ResultSetHandler映射出的单行结果对象 |
resultSetHandler | DefaultResultSetHandler | MyBatis 核心结果集处理器,负责将ResultSet当前行映射为业务对象 |
resultMap | ResultMap | 结果映射配置,指明当前行如何映射为对象 |
rsw | ResultSetWrapper | 对 JDBCResultSet的包装,提供列信息与便捷访问 |
rowBounds | RowBounds | 逻辑分页参数,记录 offset(偏移量)与 limit(限额) |
cursorIterator | CursorIterator | 游标内部迭代器,是调用方iterator()返回的对象 |
iteratorRetrieved | boolean | 标记迭代器是否已被获取(一个游标只能取出一次迭代器) |
status | CursorStatus | 游标当前状态(见 3.2 状态机) |
indexWithRowBound | int | 结合分页的取数索引,默认 -1 |
从这些字段可以看出,DefaultCursor并不直接持有ResultSet,而是通过ResultSetWrapper rsw间接访问 JDBC 结果集;真正的行映射工作也并非由游标自己完成,而是委托给DefaultResultSetHandler。游标本身只负责"何时取、取到后如何流转"。
3.2 游标状态机:CREATED → OPEN → CONSUMED / CLOSED
DefaultCursor内部通过一个私有枚举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 Iterator<T> 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():关闭底层 ResultSet
close()方法体现了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内部持有的 JDBCResultSet; SQLException被显式忽略(// ignore),避免关闭资源时的异常干扰业务逻辑;- 无论成功与否,
finally中都会将状态置为CLOSED。
四、游标核心取数逻辑:懒加载与逐行映射
DefaultCursor的精华在于它的取数过程:只有迭代器真正要求"下一个"时,才去数据库取一行。这一行为由CursorIterator与ObjectWrapperResultHandler协同完成。
4.1 ObjectWrapperResultHandler:单行结果承接器
ObjectWrapperResultHandler是DefaultCursor的受保护静态内部类,实现了ResultHandler<T>接口:
/** * 对象处理结果的包装类 * @param <T> */ protected static class ObjectWrapperResultHandler<T> implements ResultHandler<T> { /** * 数据结果 */ 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.fetched:ResultSet已无更多数据(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.Iterator<T>,调用方拿到的就是它:
/** * 游标迭代器 */ protected class CursorIterator implements Iterator<T> { /** * 下一个数据 * 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自增并返回;若确实无数据则抛出NoSuchElementException;remove():游标只读、不可删除元素,直接抛出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 = CREATED,iteratorRetrieved = false,indexWithRowBound = -1; - 调用方执行
cursor.iterator(),校验通过后iteratorRetrieved = true,返回CursorIterator; for-each首次触发hasNext():fetched为false→ 调用fetchNextUsingRowBound()→fetchNextObjectFromDatabase():status置为OPEN,resultSetHandler.handleRowValues(...)取第一行;- 若成功:
fetched = true、indexWithRowBound递增为 0; - 若
RowBounds.offset > 0:while 循环继续丢弃前几行; - 若已无数据或达到 limit:
close()并置为CONSUMED;
next()返回暂存对象,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 方法声明返回Cursor<T>,MyBatis 就会走游标查询分支,而不会走selectList全量加载路径。
6.2 接口层:SqlSession.selectCursor
SqlSession接口为游标查询提供了三个重载(见 6、SqlSession组件.md):
// 除了返回值是Cursor对象,其它与selectList相同 <T> Cursor<T> selectCursor(String statement); <T> Cursor<T> selectCursor(String statement, Object parameter); <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);它最终会调用Executor的queryCursor()方法。
6.3 核心层:Executor → StatementHandler → ResultSetHandler
在 5、Executor组件.md 中,Executor接口定义了:
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;SimpleExecutor.doQueryCursor()的实现是:
@Override protected <E> Cursor<E> 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.<E>queryCursor(stmt); }ReuseExecutor、BatchExecutor的doQueryCursor()与doQuery()实现类似(BatchExecutor会先flushStatements()保证读到最新数据)。随后链路进入 4、StatementHandler.md:
RoutingStatementHandler.queryCursor()委托给具体策略(SimpleStatementHandler/PreparedStatementHandler);PreparedStatementHandler.queryCursor()执行 SQL 后调用resultSetHandler.handleCursorResultSets(ps):
@Override public <E> Cursor<E> 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 接口方法 (返回 Cursor<T>) └─ 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.md:
returnsCursor返回值类型判定 - docs/Mybatis/核心处理层/Mybatis-MapperMethod.md:
executeForCursor分流逻辑 - docs/Mybatis/核心处理层/6、SqlSession组件.md:
selectCursor接口定义与DefaultSqlSession实现 - docs/Mybatis/核心处理层/5、Executor组件.md:
Executor.queryCursor()与三种 Executor 实现 - docs/Mybatis/核心处理层/4、StatementHandler.md:
queryCursor()与handleCursorResultSets()的调用
- 文档
- 教程
- 知识库
【免费下载链接】source-code-hunter
😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等
相关推荐
Mybatis Cursor 游标机制源码解析:从接口定义到 DefaultCursor 的按需取数实现
Mybatis Cursor 游标机制源码解析:从接口定义到 DefaultCursor 的按需取数实现 导读 当一次查询可能返回海量数据(例如导出百万级记录、
文档教程技术博客知识库pgx查询取消:CancelRequest实现原理深度剖析
pgx查询取消:CancelRequest实现原理深度剖析 引言:被阻塞的查询困境与解决方案 在高并发PostgreSQL数据库应用中,长时间运行的查询可能导致
数据库后端LlamaIndex 查询引擎流式输出(Streaming)实战指南:从零延迟体验到源码级原理
LlamaIndex 查询引擎流式输出(Streaming)实战指南:从零延迟体验到源码级原理 本篇技术指南以 LlamaIndex 官方文档 Streamin
人工智能RAG大模型
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考