说起分页查询,大家再熟悉不过了,但是如果如果分页查询使用方式不正确也会带来很大的麻烦,并且这个潜在的问题潜伏期会存在很久很久,并且不时地以其他的表现方式给开发人员制造麻烦。
接下来我会依据一个典型的例子来暴露传统的基于分页的业务操作所存在的问题,以及比较合理的解决方案。
背景描述
很多业务场景都会用到任务分配,所谓任务分配,就是在线场景无法及时处理的用例以离线数据的形式记录下来然后分配到某个人身上,等到这个人上线了之后看到分配给自己的工单然后及时处理掉。那么此处最核心的一个点就是任务分配,也就是任务状态的变更,简单的模型如下:
如模型中所表述,调度负责定时执行,每次执行期间循环分页从DB中拉取需要分配的任务,然后再循环将任务的状态变更(分配)。
方案&代码实现
简单使用了jdk自带的调度线程池来代替定时任务,在spring容器将bean初始化完毕,触发任务分配调度,调度的业务逻辑交给另外一个线程封装后实现,任务分配在应用启动5秒后开始触发,每20秒执行一次。看一下任务分配的具体逻辑实现:
class AssignThread implements Runnable {
private TaskService taskService;
public AssignThread(TaskService taskService) {
this.taskService = taskService;
}
@Override
public void run() {
System.out.println("任务分配开始···");
int total = 0;
PageQuery pageQuery = new PageQuery(20,1);
Map<String,Object> params = new HashMap<>();
params.put("status",2);
PageResult<Task> pageResult = this.taskService.page(pageQuery,params);
if(null == pageResult || CollectionUtils.isEmpty(pageResult.getList())) {
System.out.println("没有需要分配的任务");
return;
}
PageInfo pageInfo = pageResult.getPage();
int totalPage = pageInfo.getTotalPages();
for(int i = 1;i < totalPage 1; i ) {
pageQuery.setPageIndex(i);
pageResult = this.taskService.page(pageQuery,params);
List<Task> list = pageResult.getList();
for(Task task : list) {
this.taskService.doAssign(task);
total ;
}
}
System.out.println("任务分配结束···,分配了" total "条");
}
}
主要关注run方法,先触发一次待分配任务的分页查询,然后记录总页数,接着循环分页去查询待分配的任务,每次循环分页中会将查到的任务分配掉。为了记录调度执行的周期和每次调度分配掉的任务,在每次调度执行开始和结束都打印了日志。
相信屏幕前的你脑海中也大概过了一下代码,好像看起来没有什么问题。那究竟有没有问题呢?
测试&问题发现
基于上述的代码我们先进行一下测试。首先在Task表造了100条状态为2(需要分配)的数据:
运行程序:
AbstractApplicationContext context = new ClassPathXmlApplicationContext("spring-root.xml");
context.start();
运行一段时间后观察到:
一共执行了三次调度才把100条待分配的任务给分配掉,为什么?100条数据,分页大小是20,理论上一次调度,查询五次就把所有任务分配掉了。在看下图之前自己可以先思考一番到底为什么会产生这种情况,用图来直观的分析一下出现上述状况的原因:
从图中我们很轻易的看出问题的所在,由于每一次循环查询到的任务都会分配掉(状态从2变成3),导致整个待分配的任务池中的数据值减少的(直接导致每次分页查询到的总页数也是减少的),再看一下循环的关键代码:
第一个标记是从循环外拿到的5是不变的,而第二个标记分页查询得到的结果是越来越小的,所以导致上一张图中的第三四五次循环查询的pageIndex其实是超过实际totalPage的,因此就直接终止了分配。那这种情况如何解决呢?
解决方案
方案一:每次循环查询只查询第一页
将出问题的循环代码替换如下:
while(true) {
pageResult = this.taskService.page(pageQuery,params);
List<Task> list = pageResult.getList();
if(CollectionUtils.isEmpty(list)) {
System.out.println("分配完成");
break;
}
for(Task task : list) {
this.taskService.doAssign(task);
total ;
}
}
再次启动应用,可以看到:
只经过一次调度就将100条任务全部分配了,解决了上边我们遇到的分页查询遗漏问题。
方案二:不使用总页数做循环查询,使用自增id做增量查询
这个方案需要改造底层逻辑,使用id升序排序,每次循环查询拿上次的最大id做条件,新查询id要大于上一次查询的最大id,此处不再做赘述,有兴趣可以自己实现或者找我私聊。
总结
经过上述一系列的描述,我们通过一个典型的任务分配状态机流转案例来剖析了传统分页查询遇到的问题,以及合理的解决方案,希望能够给各位看官带来一些参考价值。