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

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

相关推荐
笨鸟先飞,勤能补拙1 天前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
郭老二1 天前
【前端】vue3:编译步骤
前端
长和信泰光伏储能1 天前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂1 天前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
不在逃避q1 天前
Trae/Vs Code/Cursor命令行无法跑npm命令
前端·arcgis·npm
夏殇之殁1 天前
包中创建自定义列表项。 . 使用自定义列表项进行数据绑定 . 将天气预报数据保存到本地内存表,通过LiveBindings进行显示。 ...
服务器·前端·javascript
The Chosen One9851 天前
高进度算法模板速记(待完善)
java·前端·算法
Gu Gu Study1 天前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
陈随易1 天前
MoonBit抓包模块pcap,查看电脑的每一次联网通信
前端·后端·程序员
卷无止境1 天前
写代码这件事,到底该讲究点什么?
后端·python