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. 监控数据库连接和内存使用情况

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

相关推荐
才兄说2 小时前
机器人二次开发机器狗巡检?定位精度±2cm
python
2301_782659182 小时前
SQL视图能否用于数据仓库模型_雪花模型与视图构建
jvm·数据库·python
m0_377618232 小时前
CSS如何让文字超出两行显示省略号_使用line-clamp属性限制
jvm·数据库·python
m0_743623922 小时前
HTML5中LocalStorage存储用户自定义快捷键配置
jvm·数据库·python
用户412467508372 小时前
用 Locust 压测一个网站,记录一下学习过程
前端
2301_773553622 小时前
HTML5中SharedWorker生命周期与浏览器进程关闭的关系
jvm·数据库·python
m0_640309302 小时前
mysql flush privileges有什么作用_mysql权限生效机制解析
jvm·数据库·python
2401_897190552 小时前
mysql备份期间如何监控系统负载_使用iostat与top命令
jvm·数据库·python
2301_796588502 小时前
SQL批量删除不同条件的记录_使用IN子句简化删除逻辑
jvm·数据库·python