迭代器与分批查询

1,482 阅读5分钟

迭代器与分批查询

迭代器模式

关于迭代器的模式的解释,网上已经有很多的文章和示例。

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。

可以明确的是迭代器和集合这两个概念是紧密的联系在一起的,JDK里面的各种集合类通常都会在其内部实现迭代器Iterator接口,并暴露给调用方使用。

Iterator接口的核心方法就是hasNextnext,前者判断迭代器里面是否还有未遍历到的元素,后者返回下一个遍历的元素。

public interface Iterator<E> {

    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

分批查询

分批查询在实际的应用开发中是十分常见的,通常的场景是调用方需要从数据库获取大量的数据。数据提供方一次性加载全部的数据提供给调用方的方式,对系统内存以及网络IO都是十分不友好的。因此数据提供方一般都是提供分批查询的方式,能够多批次不重叠的返回数据。为了保证批次之间的数据有序且不重叠,数据提供方在获取数据时必须使用统一的排序方式。

分批查询方式

分批查询的实现方式有很多中,下面介绍最常见的几类

分页查询

分页查询的核心参数就是提供当前的页数以及每页的数量,接口如下

public interface PageGet<T> {

    /**
     * 根据分页位置和每页数量获取数据
     */
    List<T> pageGetFunc(int page, int pageSize, QueryParam queryParam);
}

滚页查询

滚页查询的核心参数就是提供游标的位置,通常是从上一批次获取的最后一条数据中获取游标信息,接口如下

public interface ScrollGet<T> {

    /**
     * 根据游标位置获取数据
     */
    List<T> scrollGetFunc(T cursor, QueryParam queryParam);
}

特殊分页/滚页查询

不管是分页还是滚页查询,获取数据的时候都必须指定一种排序方式。有些时候大数据量的排序本身就是一件对数据库压力很大的事情。比如说在ES搜索场景下,百万级的命中数据参与排序是很耗时的操作。另外,大数据量分批查询导致的深度分页对数据库也可能会带来一定的压力。

解决这个问题的一个思路是减小查询条件命中的数据范围,将查询条件可能命中大量数据按照ID或者时间(通常和排序字段是同一个维度)进行分片。这种先分片再分批的方式可以减少每次参与排序的数据量,从而降低数据库的压力。分片的一个好处是给并行分批获取数据提供了一种手段,特别是对滚页查询的方式。

以分页接口为例,下面的分批获取方法增加了Range参数。Range本质上也是查询条件,从实现角度上完全可以塞进QueryParam里面去。这里单独作为一个参数是想体现,数据需求方最终需要的数据是QueryParam条件命中的数据,Range只是数据获取过程中的一种分片手段。

public interface SpecPageGet<T> {
    /**
     * 根据分页位置和每页数量获取数据,指定了数据范围
     */
    List<T> specPageGetFunc(int page, int pageSize, Range range, QueryParam queryParam);
}

迭代器包装

上文提到的场景是,调用方需要查询条件命中的所有数据,并通过分页接口多批次不重叠的获取该数据。我们可以将查询条件命中的所有数据看作一个集合,同时调用方获取到的数据是按照一定的顺序返回的。不难看出,这个和迭代器的思想是非常一致的。我们可以将上述分批接口包装为迭代器。

分页查询迭代器包装

工具类

/**
 * 将分页接口包装为迭代器
 * @param pageGetFunc 分页获取接口,入参为page,返回list数据
 * @param size 分页的大小,提供给外部指定
 * @param <T>
 * @return
 */
public static <T> Iterator<T> wrapPageGetApiToIterator(
        Function<Integer, List<T>> pageGetFunc,
        int size) {

    return new Iterator<T>() {
        private int page = 0;
        private boolean pageGetHasMore = true;
        private Iterator<T> currentIterator = Collections.emptyIterator();

        public void tryStorageGet() {
            if (currentIterator.hasNext()) {
                return;
            }
            if (!pageGetHasMore) {
                return;
            }
            List<T> list = pageGetFunc.apply(page);
            pageGetHasMore = list.size() >= size;
            currentIterator = list.iterator();
            page++;
        }

        @Override
        public boolean hasNext() {
            tryStorageGet();
            return currentIterator.hasNext();
        }

        @Override
        public T next() {
            tryStorageGet();
            return currentIterator.next();
        }
    };
}

接口包装

public interface PageGet<T> {
    
    int DEFAULT_PAGE_SIZE = 100;

    /**
     * 根据分页位置和每页数量获取数据
     */
    List<T> pageGetFunc(int page, int pageSize, QueryParam queryParam);

    default Iterator<T> getDataIterator(QueryParam queryParam) {
        return getDataIterator(queryParam, DEFAULT_PAGE_SIZE);
    }

    default Iterator<T> getDataIterator(QueryParam queryParam, int pageSize) {
        return IteratorWrapUtils.wrapGetApiToIterator(
                (page) -> pageGetFunc(page, pageSize, queryParam),
                pageSize
        );
    }
}

滚页查询迭代器包装

工具方法

/**
 * 将滚页接口包装为迭代器
 * @param scrollGetFunc 滚页获取接口,入参为游标,返回数据list
 * @param size  分页的大小,提供给外部指定
 * @param <T>
 * @return
 */
public static <T> Iterator<T> wrapScrollGetApiToIterator(
        Function<T, List<T>> scrollGetFunc,
        int size) {

    return new Iterator<T>() {
        private T lastScrollGetItem = null;
        private boolean pageGetHasMore = true;
        private Iterator<T> currentIterator = Collections.emptyIterator();

        public void tryStorageGet() {
            if (currentIterator.hasNext()) {
                return;
            }
            if (!pageGetHasMore) {
                return;
            }
            List<T> list = scrollGetFunc.apply(lastScrollGetItem);
            if (!list.isEmpty()) {
                lastScrollGetItem = list.get(list.size() - 1);
            }
            pageGetHasMore = list.size() >= size;
            currentIterator = list.iterator();
        }

        @Override
        public boolean hasNext() {
            tryStorageGet();
            return currentIterator.hasNext();
        }

        @Override
        public T next() {
            tryStorageGet();
            return currentIterator.next();
        }
    };
}

接口包装

public interface ScrollGet<T> {

    int DEFAULT_PAGE_SIZE = 100;

    /**
     * 根据游标位置获取数据
     */
    List<T> scrollGetFunc(T cursor, QueryParam queryParam);

    default Iterator<T> getDataIterator(QueryParam queryParam) {
        return getDataIterator(queryParam, DEFAULT_PAGE_SIZE);
    }

    default Iterator<T> getDataIterator(QueryParam queryParam, int pageSize) {
        return IteratorWrapUtils.wrapScrollGetApiToIterator(
                (lastItem) -> scrollGetFunc(lastItem, queryParam),
                pageSize
        );
    }
}

特殊分页/滚页查询

如何将既分片又分批的数据获取过程包装为迭代器?将分片过程归纳到迭代器的内部,对外提供无感知的分片处理是最关键的。分片的实现通常是和数据本身的特点有关系,数据在指定范围是均匀的分布、还是间密分布,数据库容许的最大排序数量,对于分片范围的确定有很大影响。大家可以结合具体的场景实现这一功能。