MyBatis-Plus 流式查询教程

一、什么是流式查询

流式查询是一种逐条或分批处理大量数据 的查询方式,避免一次性将所有数据加载到内存中,特别适合处理百万级甚至千万级的数据场景。

核心优势

  • 内存友好:数据边读边处理,不会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;
            });
        });
    }
}

七、最佳实践总结

  1. 优先使用 ResultHandler 方式,代码简洁,资源管理自动

  2. 务必添加 @Transactional,否则流式查询无效

  3. 合理设置 fetchSize,根据实际数据大小测试

  4. 避免在流式查询内部调用其他数据库操作,可能导致连接耗尽

  5. 复杂查询建议使用分页流式,更可控

  6. 监控数据库连接和内存使用情况

通过以上方式,你可以轻松处理千万级数据的查询和导出任务,而不用担心内存溢出问题。

相关推荐
anOnion2 小时前
构建无障碍组件之Menu Button pattern
前端·html·交互设计
用户47949283569152 小时前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端
zhangxingchao5 小时前
Kotlin常用的Flow 操作符整理
前端
IT_陈寒6 小时前
React的useState居然还有这种坑?我差点删库跑路
前端·人工智能·后端
Pedantic7 小时前
SwiftUI 手势笔记
前端·后端
金銀銅鐵7 小时前
[Python] 从《千字文》中随机挑选汉字
后端·python
橙子家8 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user20585561518138 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州8 小时前
CSS aspect-ratio 属性完全指南
前端