日常工作中,有一些功能如状态更新等需要遍历表中数据,如果数据量比较少的情况下,我们可以正常的使用数据库如mysql提供的limit和offset来实现分页的功能,但是如果数据量比较大,这时候就会有深分页的问题,产生慢SQL, 为了解决这个问题,一种实现方式就是通过主键ID+查询条件来过滤数据,使用类似如下语句
1
| SELECT * FROM task WHERE id > $minId AND status = 1 ORDER BY id LIMIT 200
|
如果有其他条件导致需要扫描很大的行数才能扫描到的话,可能还需要限制id上限
1
| SELECT * FROM task WHERE id > $minId AND id < $maxId AND status = 1 ORDER BY id LIMIT 200
|
之后每次使用查询的最大值更新变量minId,直到查询不出数据为止,具体对应到Java代码中大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class TaskStatusUpdater { public void execute() { int limit = 200; int minId = 0; while (true) { List<Task> tasks = taskDao.listByMinId(minId, limit); if (CollectionUtils.isEmpty(tasks)) { break; } minId = tasks.stream() .max(Comparator.comparing(Task::getId)) .map(Task::getId) .get(); for (Task task : tasks) { operateTask(task); } } } private void operateTask(Task task) { } }
|
这里可以看到,执行方法中大部分都是一些业务无关的控制代码,如果有不同的处理逻辑需要遍历,那么都要复制一下这一大坨的控制代码,是否有更好的写法呢?
Consumer
首先想到的就是使用java8提供的函数接口-Consumer, 这时候我们可以将代码修改如下,将处理逻辑作为参数传递进方法,这样不同的处理逻辑,都可以复用这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public void consumeAllData(Consumer<Task> taskConsumer) { int limit = 200; int minId = 0;
while (true) { List<Task> tasks = taskDao.listByMinId(minId, limit); if (CollectionUtils.isEmpty(tasks)) { break; } minId = tasks.stream() .max(Comparator.comparing(Task::getId)) .map(Task::getId) .get();
for (Task task : tasks) { taskConsumer.accept(task); } } }
consumeAllData(task -> { });
consumeAllData(task -> { });
|
Iterator/Iterable
那么除了使用Consumer这个函数接口,是否还有其他的方式呢?那就是使用Iterable接口,这里我们将代码修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @Component public class TaskIterable implements Iterable<Task> { @Resource private TaskDao taskDao; @NotNull @Override public Iterator<Task> iterator() { return new TaskIterator(taskDao); }
static class TaskIterator implements Iterator<Task> { private TaskDao taskDao; private int minId = 0; private int limit = 200; private List<Task> currentDataList = new ArrayList<>(limit); private int currentIndex = 0;
public TaskIterator(TaskDao taskDao) { this.taskDao = taskDao; fetchNextPage(); } private void fetchNextPage() { currentDataList = this.taskDao.listByMinId(minId, limit); if (CollectionUtils.isNotEmpty(currentDataList)) { minId = maxId(currentDataList); } currentIndex = 0; }
@Override public boolean hasNext() { if (currentIndex >= CollectionUtils.size(currentDataList)) { fetchNextPage(); } return currentIndex < CollectionUtils.size(currentDataList); }
@Override public Task next() { if (!hasNext()) { throw new NoSuchElementException(); } return currentDataList.get(currentIndex++); }
private int maxId(List<Task> taskList) { return currentDataList.stream() .max(Comparator.comparing(Task::getId)) .map(Task::getId) .get(); } } }
|
这个相比之前的方式代码量有所上升,但是我们来看一下使用的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Service public class TaskService {
@Resource private TaskIterable taskIterable; public void execute() { for (Task task : taskIterable) { } }
public void otherExecute() { for (Task task : taskIterable) {
} } }
|
这里可以看到,使用起来非常的简单,直接通过spring注入后,使用foreach语法直接进行遍历即可,不需要关注具体的数据量,而且不同的业务逻辑也可以直接使用
Stream
如果喜欢Stream,也可以将上面的Iterator换成Stream的方式
1 2 3 4 5 6 7
| public Stream<Task> taskStream() { final Iterator<Task> iterator = taskIterable.iterator(); return StreamSupport.stream(Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED | Spliterator.IMMUTABLE), false); }
|
以上就是实现的大致思路,抛砖引玉~