一、什么是流式查询
流式查询是一种逐条或分批处理大量数据 的查询方式,避免一次性将所有数据加载到内存中,特别适合处理百万级甚至千万级的数据场景。
核心优势
-
✅ 内存友好:数据边读边处理,不会OOM
-
✅ 性能高效:减少GC压力,提升处理速度
-
✅ 实时处理:可边读取边进行业务处理
二、三种实现方式
方式1:使用 @Options 注解(推荐)
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE age > #{age}")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
@ResultType(User.class)
void streamQuery(@Param("age") Integer age, ResultHandler<User> handler);
}
使用示例:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void processUsers() {
userMapper.streamQuery(18, resultContext -> {
User user = resultContext.getResultObject();
// 处理每一条数据
System.out.println("Processing: " + user.getName());
// 可以控制停止查询
if (someCondition) {
resultContext.stop();
}
});
}
}
方式2:使用 Cursor(MyBatis原生)
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE age > #{age}")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
Cursor<User> streamQueryCursor(@Param("age") Integer age);
}
使用示例:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(readOnly = true)
public void processWithCursor() {
try (Cursor<User> cursor = userMapper.streamQueryCursor(18)) {
for (User user : cursor) {
// 处理每条数据
System.out.println(user.getName());
// 注意:在遍历过程中要保持事务开启
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
方式3:分页流式查询(自定义实现)
java
@Service
public class BatchStreamService {
@Autowired
private UserMapper userMapper;
public void streamByPage(int pageSize) {
int currentPage = 1;
boolean hasMore = true;
while (hasMore) {
Page<User> page = new Page<>(currentPage, pageSize);
Page<User> result = userMapper.selectPage(page, null);
// 处理当前页数据
result.getRecords().forEach(user -> {
// 业务处理
processUser(user);
});
hasMore = result.hasNext();
currentPage++;
// 清空EntityManager缓存(JPA场景)
if (entityManager != null) {
entityManager.clear();
}
}
}
}
三、完整实战示例
1. 数据导出场景
java
@Service
public class DataExportService {
@Autowired
private OrderMapper orderMapper;
public void exportLargeData(String filePath) {
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
// 写入CSV头部
writer.println("Order ID,Amount,Status,Create Time");
// 流式查询
orderMapper.streamLargeOrders(resultContext -> {
Order order = resultContext.getResultObject();
String line = String.format("%d,%.2f,%s,%s",
order.getId(),
order.getAmount(),
order.getStatus(),
order.getCreateTime()
);
writer.println(line);
// 每处理1000条,刷新缓冲区
if (resultContext.getResultCount() % 1000 == 0) {
writer.flush();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
@Select("SELECT * FROM orders WHERE amount > #{minAmount}")
@Options(fetchSize = 500, resultSetType = ResultSetType.FORWARD_ONLY)
@ResultType(Order.class)
void streamLargeOrders(@Param("minAmount") BigDecimal minAmount,
ResultHandler<Order> handler);
}
2. 数据同步场景
java
@Service
public class DataSyncService {
@Autowired
private SourceMapper sourceMapper;
@Autowired
private TargetMapper targetMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void syncData() {
AtomicLong processedCount = new AtomicLong(0);
sourceMapper.streamSyncData(resultContext -> {
SourceData source = resultContext.getResultObject();
// 转换并保存到目标表
TargetData target = convert(source);
targetMapper.insert(target);
long count = processedCount.incrementAndGet();
if (count % 10000 == 0) {
log.info("已同步 {} 条数据", count);
}
});
log.info("同步完成,共处理 {} 条数据", processedCount.get());
}
}
四、重要注意事项
⚠️ 1. 必须开启事务
java
@Transactional(readOnly = true) // 查询场景使用只读事务
public void streamQueryWithTx() {
// 流式查询必须在事务内执行
}
⚠️ 2. 合理设置 fetchSize
java
// fetchSize 建议值
// - 网络状况好:1000-5000
// - 数据行较大:100-500
// - 默认值:根据实际测试调整
@Options(fetchSize = 1000) // 每次从数据库获取1000条
⚠️ 3. ResultSetType 设置
java
@Options(resultSetType = ResultSetType.FORWARD_ONLY)
// FORWARD_ONLY: 只能向前滚动,性能最好
// SCROLL_INSENSITIVE: 可滚动,对其他事务修改不敏感
// SCROLL_SENSITIVE: 可滚动,对其他事务修改敏感
⚠️ 4. 资源释放
java
// Cursor 方式必须使用 try-with-resources
try (Cursor<User> cursor = mapper.streamCursor()) {
// 自动释放资源
}
// ResultHandler 方式无需手动释放
五、性能对比测试
java
@Test
public void performanceTest() {
// 测试1: 普通查询(500万数据会OOM)
long start1 = System.currentTimeMillis();
List<User> allUsers = userMapper.selectList(null);
allUsers.forEach(this::process);
System.out.println("普通查询耗时: " + (System.currentTimeMillis() - start1));
// 测试2: 流式查询
long start2 = System.currentTimeMillis();
userMapper.streamQuery(handler -> {
process(handler.getResultObject());
});
System.out.println("流式查询耗时: " + (System.currentTimeMillis() - start2));
// 测试3: 分页查询
long start3 = System.currentTimeMillis();
int pageNum = 1;
int pageSize = 1000;
while (true) {
Page<User> page = new Page<>(pageNum++, pageSize);
Page<User> result = userMapper.selectPage(page, null);
result.getRecords().forEach(this::process);
if (!result.hasNext()) break;
}
System.out.println("分页查询耗时: " + (System.currentTimeMillis() - start3));
}
六、常见问题解决
问题1:连接被重置/超时
bash
# application.yml 配置
spring:
datasource:
hikari:
connection-timeout: 30000
validation-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# MySQL 配置
# wait_timeout=28800
# interactive_timeout=28800
问题2:大数据量下性能下降
java
// 批量处理优化
userMapper.streamQuery(18, resultContext -> {
User user = resultContext.getResultObject();
buffer.add(user);
// 批量提交
if (buffer.size() >= BATCH_SIZE) {
batchProcess(buffer);
buffer.clear();
// 手动清理缓存
entityManager.clear();
}
});
问题3:事务过大导致锁问题
java
// 拆分为多个小事务
@Service
public class StreamWithBatchTx {
@Autowired
private TransactionTemplate transactionTemplate;
public void processWithSmallTx() {
userMapper.streamQuery(18, resultContext -> {
User user = resultContext.getResultObject();
// 每条数据独立事务
transactionTemplate.execute(status -> {
processInTransaction(user);
return null;
});
});
}
}
七、最佳实践总结
-
优先使用 ResultHandler 方式,代码简洁,资源管理自动
-
务必添加 @Transactional,否则流式查询无效
-
合理设置 fetchSize,根据实际数据大小测试
-
避免在流式查询内部调用其他数据库操作,可能导致连接耗尽
-
复杂查询建议使用分页流式,更可控
-
监控数据库连接和内存使用情况
通过以上方式,你可以轻松处理千万级数据的查询和导出任务,而不用担心内存溢出问题。