MyBatis Cursor + Reactor Flux.create
MyBatis 提供了 Cursor 接口,它本质上是对 JDBC ResultSet 的一个包装。当使用 Cursor 时,MyBatis 不会一次性将所有结果加载到内存中,而是逐行从数据库读取。我们可以将这个逐行的、阻塞的迭代过程,转换成一个非阻塞的、异步的 Flux 数据流。
数据流如下:MySQL -> JDBC ResultSet (流式) -> MyBatis Cursor -> (在专用线程中迭代) -> Flux.create -> Spring WebFlux Controller -> Client
实现步骤与代码示例
第一步:MyBatis Mapper 层配置游标查询
在你的 Mapper 接口中,定义一个返回 Cursor 的方法。
import org.apache.ibatis.cursor.Cursor;
@Mapper
public interface HugeDataMapper {
// 方法返回类型必须是 Cursor
Cursor<YourDataModel> streamAllData();
}
对应的 XML 映射文件(如果使用XML):
<select id="streamAllData" resultType="YourDataModel">
SELECT * FROM your_huge_table
<!-- 可以添加 WHERE 条件等 -->
</select>
关键点: 仅仅是定义返回 Cursor,MyBatis 就会使用流式方式执行查询。
第二步:Service 层封装为 Flux
这是最关键的一步。我们需要在一个专门的、有边界的线程池(Schedulers.boundedElastic())中执行这个阻塞的游标遍历操作,并将每一行数据通过 Flux.create 的 sink 发射出去。
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class StreamService {
private final HugeDataMapper hugeDataMapper; // 你的 MyBatis Mapper
public Flux<YourDataModel> streamHugeData() {
return Flux.create(sink -> {
// 这段代码将在弹性线程池中执行,不会阻塞WebFlux的事件循环线程
try (Cursor<YourDataModel> cursor = hugeDataMapper.streamAllData()) {
cursor.forEach(sink::next); // 逐行遍历,并通过sink发射数据
sink.complete(); // 遍历完成,发出完成信号
} catch (Exception e) {
sink.error(e); // 发生异常,发出错误信号
}
}).subscribeOn(Schedulers.boundedElastic()); // 指定上述操作所在的线程池
}
}
代码解释:
(1) Flux.create: 创建一个 Flux,我们可以完全控制如何向其中发射数据。
(2) subscribeOn(Schedulers.boundedElastic()): 这是至关重要的一步。它将整个阻塞的游标遍历操作(包括数据库查询)转移到一个专门的、大小受限的弹性线程池中执行。这保护了 WebFlux 用于处理高并发请求的有限的事件循环线程(EventLoop Thread),防止它们被阻塞操作拖垮。在
(3) create 的回调中:
使用 forEach 逐行遍历,每拿到一行数据,就调用 sink.next(data) 将其发射出去。
遍历完成后调用 sink.complete()。
用 try-with-resources 语句确保 Cursor 和底层的 ResultSet、Connection 会被正确关闭。
第三步:Controller 层暴露流式端点
在 Controller 中,注入 Service 并返回这个 Flux。
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class StreamingController {
private final StreamService streamService;
@GetMapping(value = "/stream/data", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<YourDataModel> streamData() {
return streamService.streamHugeData();
}
}
关键点:
(1) produces = MediaType.TEXT_EVENT_STREAM_VALUE: 这声明了该端点使用 Server-Sent Events (SSE) 协议进行响应。这是最常用的流式传输协议,前端可以直接用 EventSource API 来消费。
(2) 你也可以使用 MediaType.APPLICATION_NDJSON_VALUE,它会以换行符分隔的 JSON 格式返回数据。
第四步:配置
1. MySQL JDBC 连接参数
为了确保 MySQL JDBC 驱动真正使用流式传输,必须在数据库连接 URL 中配置以下参数:properties
# application.properties
spring.datasource.url=jdbc:mysql://your-host:3306/your-db?
useCursorFetch=true& # 启用游标抓取
defaultFetchSize=-2147483648&# 设置默认抓取大小为 Integer.MIN_VALUE,这是驱动层流式传输的关键
characterEncoding=UTF-8&
serverTimezone=Asia/Shanghai
defaultFetchSize=-2147483648 是驱动识别为“流式模式”的魔法值。useCursorFetch=true 与之配合使用。
2. 连接池配置 (重要!)
必须确保连接池不会干扰流式查询。 常见的连接池(如 HikariCP)会在连接归还时关闭任何打开的 ResultSet,这会立即中断你的流。
解决方案:使用 Spring 事务管理, 在 @Transactional 注解的方法中执行游标查询,这样可以保证在整个流式读取完成之前,连接不会被归还。你可以将 @Transactional 注解加在 Mapper 方法上或 Service 方法上。
@Service
public class StreamService {
...
@Transactional // 添加事务注解,保持连接直到整个流处理完毕
public Flux<YourDataModel> streamHugeData() {
return Flux.create(sink -> {
try (Cursor<YourDataModel> cursor = hugeDataMapper.streamAllData()) {
cursor.forEach(sink::next);
sink.complete();
} catch (Exception e) {
sink.error(e);
}
}).subscribeOn(Schedulers.boundedElastic());
}
}
291

被折叠的 条评论
为什么被折叠?



