You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Pageable查询获取全部行?Driver Referral Lead分页漏处理问题

解决Pageable分页查询无法获取全部符合条件记录的问题

问题场景

现有需求是批量更新source_channel为SourceChannels.DRIVER_REFERRAL_CHANNEL的Lead记录,但当前实现仅能处理4条符合条件的记录(实际存在6条)。分页查询的第二次迭代无法获取到数据,日志显示空结果。

原实现代码

int batchSize = 2;
Page<Lead> driverReferralLeads = repository.findByChannelAndCreatedAtIsLessThanAndDriverIdIsNullAndStatusIn(SourceChannels.DRIVER_REFERRAL_CHANNEL, expiryDate, statusForExpiring, PageRequest.of(0, batchSize));
List<Lead> driverReferralLeadsList = driverReferralLeads.getContent();
LOGGER.info("{} phoneNumbers: {}", SourceChannels.DRIVER_REFERRAL_CHANNEL, driverReferralLeadsList.stream().map(Lead::getPhone).toArray()); // todo to be removed
service.expireLeads(SourceChannels.DRIVER_REFERRAL_CHANNEL, driverReferralLeadsList, expiryDate);
int driverReferralLeadsIteration = 0; // todo to be removed
while (driverReferralLeads.hasNext()) {
    LOGGER.info("{} iteration [{}].", SourceChannels.DRIVER_REFERRAL_CHANNEL, driverReferralLeadsIteration); // todo to be removed
    driverReferralLeads = repository.findByChannelAndCreatedAtIsLessThanAndDriverIdIsNullAndStatusIn(SourceChannels.DRIVER_REFERRAL_CHANNEL, expiryDate, statusForExpiring, driverReferralLeads.nextPageable());
    List<Lead> driverReferralLeadsNextList = driverReferralLeads.getContent();
    LOGGER.info("{} iteration [{}]. phoneNumbers: {}", SourceChannels.DRIVER_REFERRAL_CHANNEL, driverReferralLeadsIteration, driverReferralLeadsNextList.stream().map(Lead::getPhone).toArray()); // todo to be removed
    cartHeroService.expireLeads(SourceChannels.DRIVER_REFERRAL_CHANNEL, driverReferralLeadsNextList, expiryDate);
    driverReferralLeadsIteration++; // todo to be removed
}
LOGGER.info("{} Expired [{}] Leads.", LEAD_EXPIRY_LOGGER, SourceChannels.DRIVER_REFERRAL_CHANNEL);

日志信息

Driver Referral iteration [0]. phoneNumbers: [1122334455, 1123455551]
Driver Referral iteration [1]. phoneNumbers: []

问题根源

核心原因是分页查询的条件与数据更新操作冲突:

  1. 第一次查询获取前2条符合条件的记录后,调用expireLeads修改了这些记录的状态(使其不再满足statusForExpiring的条件)。
  2. 第二次分页查询时,数据库中符合原条件的记录只剩下4条,但分页请求的是第2页(页码从0开始,nextPageable()返回页码1),此时数据库中第2页没有符合条件的数据。
  3. 第一次查询的Page对象判断hasNext()为true(因为初始总记录数是6,总页数3),导致进入循环,但实际查询不到数据,后续第三页的查询也不会触发,最终漏处理了原本属于第3页的2条记录。

修复方案

方案一:先查询所有符合条件的ID,再分批处理(推荐)

先一次性获取所有符合条件的记录ID,再分批根据ID查询并更新,避免分页过程中数据状态变化导致的漏查。

步骤1:在Repository中添加查询ID的方法

// LeadRepository.java
List<Long> findIdsByChannelAndCreatedAtIsLessThanAndDriverIdIsNullAndStatusIn(
    SourceChannels channel, Date expiryDate, List<LeadStatus> statuses);

步骤2:修改业务代码

// 获取所有符合条件的Lead ID
List<Long> leadIds = repository.findIdsByChannelAndCreatedAtIsLessThanAndDriverIdIsNullAndStatusIn(
    SourceChannels.DRIVER_REFERRAL_CHANNEL, expiryDate, statusForExpiring);

int batchSize = 2;
int totalProcessed = 0;

// 分批处理ID列表
for (int i = 0; i < leadIds.size(); i += batchSize) {
    int endIndex = Math.min(i + batchSize, leadIds.size());
    List<Long> batchIds = leadIds.subList(i, endIndex);
    
    // 根据ID查询批次记录
    List<Lead> batchLeads = repository.findAllById(batchIds);
    // 执行过期操作
    service.expireLeads(SourceChannels.DRIVER_REFERRAL_CHANNEL, batchLeads, expiryDate);
    
    totalProcessed += batchLeads.size();
    LOGGER.info("处理批次 {},手机号:{}", i/batchSize, batchLeads.stream().map(Lead::getPhone).toArray());
}

LOGGER.info("{} 已过期 {} 条Leads。", LEAD_EXPIRY_LOGGER, SourceChannels.DRIVER_REFERRAL_CHANNEL, totalProcessed);

方案二:使用数据库快照隔离级别(可选)

如果必须保留分页查询的方式,可以配置数据库使用读快照隔离级别(如MySQL的REPEATABLE READ),让分页查询基于初始查询时的数据快照,不受后续更新操作的影响。但这种方式依赖数据库支持,且如果数据量较大,可能会占用更多数据库资源。

关键注意点

  • 避免在分页查询过程中修改查询条件依赖的字段,否则会导致分页逻辑失效。
  • 优先采用“先查ID再分批处理”的方式,逻辑更稳定,不受数据实时变化影响。

内容的提问来源于stack exchange,提问作者Dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 21:34:52