随机分页查询的实现方案

背景

这段时间在整理项目代码时,发现一个两年前实习时做的随机分页功能,效果如下图所示:

POPO-screenshot-20211014-161426

这个功能的业务需求如下:在前端分页展示数据时,为了让每个数据都有相同的几率被展示,每次分页展示数据时,都是随机展示的,并要求每一页之间数据不能重复。

下面简单介绍一下这个功能的实现过程。

思路

ORDER BY RAND()

要求随机展示数据,最直接的方式就是在每次查询时,从数据库中随机查询数据返回给前端。

可以使用 MySQL 命令 ORDER BY RAND() 实现随机查询功能,命令如下所示:

SELECT * 
FROM table_name
ORDER BY RAND()
LIMIT 10;
复制代码

这个方式可以实现随机查询,但是存在以下问题:

  • 性能比较差,EXPLAIN 该语句时,EXTRA 显示该语句的执行计划使用了 Using temporary; Using filesort
  • 每次查询的结果都是随机的,前后两页的数据可能存在重复。

随机序列

既要“随机”又要“不重复”,我们可以在服务端把要查询数据的 ID 列表打乱,在打乱后的 ID 列表上执行分页,把分页后的 ID 子列表作为一个随机序列,使用该随机序列从数据库查询数据。

实现步骤如下:

1、查询所有作品的 ID 列表 2、按相同规则打乱 ID 列表 3、从 ID 列表中分页取出子列表,作为随机序列 4、使用随机序列查询数据

伪代码如下:

idList <- 查询ID列表;
shuffle(idList); // 打乱ID列表 
subIdList <- page(idList); // 分页 ID 列表
result <- query(subIdList); // 查询子列表
复制代码

这种方式能够实现“随机、且不重复”的要求,但是并没有做到真正的随机,依然存在以下问题:

  • 所有用户在同一个页码下,看到的数据都是相同的;
  • 用户重新进入后,在同一个页码上看到的数据,与上次进入看到的数据相同;

随机种子

前面方案的问题在于:每次重新进入,使用的都是同一种方式打乱ID列表。我们可以在用户每次进入页面后,使用一个固定的乱序方式,在刷新前,每次切换页码时,保持该乱序规则,我们可以使用随机种子实现这个功能。

实现步骤如下:

  • 用户进入进入页面调用接口时,服务端生成一个随机种子 seed,使用 seed 来打乱 ID 列表,并把 seed 返回给前端;
  • 用户访问其他页码时,前端传入 seed,服务端继续使用 seed来打乱 ID 列表并分页,这样可以保证不同页码不会出现重复数据;
  • 用户重新进入页面、刷新、访问第一页时,服务端生成一个新 seed,重复上述过程,这样可以保证每次重新进入后,在同一个页码看到的数据不一样;

seed可以由服务端生成后返回给前端保存,也可以由前端生成后传给服务端。

伪代码如下:

if page = 1 or seed = null:
	then seed <- initSeed(); // 生成seed;
else:
	shuffle(seed,idList); // 使用 seed 打乱 ID 列表;

subIdList <- page(idList); // 分页 ID 列表
result <- [query(subIdList),seed]; // 查询子列表,返回数据和seed
复制代码

这种方案能满足我们前面的要求,但是依然存在一个问题:每次查询都要把所有 ID 查询到内存中,只适合数据量较小的业务场景,如果数据量很大该怎么做?

暂时想不出什么好办法,可能需要使用搜索引擎来实现。

代码实现

项目地址为:github.com/ShiMengjie/… APP 模块的 ActivityWorkController 中,下面介绍其中几个关键类。

ActivityWorkListQuery

ActivityWorkListQuery 对象用来封装查询条件,源码如下:

import lombok.Getter;

import java.util.Random;

/**
 * ActivityWork 列表查询条件
 **/
@Getter
public class ActivityWorkListQuery {

    /**
     * 查询条件:标题
     */
    private String title;

    /**
     * 随机种子
     */
    private Integer seed;

    /**
     * 当前页码
     */
    private Integer pageNum;

    /**
     * 每页数量
     */
    private Integer pageSize;

    public ActivityWorkListQuery() {
    }

    public ActivityWorkListQuery(String title, Integer seed, Integer pageNum, Integer pageSize) {
        this.title = title;
        // 如果是第一页或 seed 为 null,就生成一个新的 seed
        if (seed == null || seed == 0 || pageNum == 1) {
            this.seed = new Random().nextInt();
        } else {
            this.seed = seed;
        }

        this.pageNum = pageNum;
        this.pageSize = pageSize;
    }
}
复制代码

ActivityWorkListEntity

ActivityWorkListEntity 作为“查询活动作品列表”的实体,持有查询对象、结果列表、结果总数,并打乱ID列表和对ID列表分页,源码如下:

import com.shimengjie.wpm.common.utils.CollectionUtils;
import com.shimengjie.wpm.work.domain.model.activity.query.ActivityWorkListQuery;

import java.util.Collections;
import java.util.List;
import java.util.Random;

public class ActivityWorkListEntity {

    private ActivityWorkListQuery activityWorkListQuery;

    private int total;

    private List<ActivityWork> activityWorkList;

    public ActivityWorkListEntity() {
    }

    public ActivityWorkListEntity(ActivityWorkListQuery activityWorkListQuery) {
        this.activityWorkListQuery = activityWorkListQuery;
    }

    public int getTotal() {
        return this.total;
    }

    public void setTotal(int total) {
        this.total = total;
    }

    public List<ActivityWork> getActivityWorkList() {
        return this.activityWorkList;
    }

    public void setActivityWorkList(List<ActivityWork> activityWorkList) {
        this.activityWorkList = activityWorkList;
    }

    /**
     * 打乱 id 列表
     */
    public void shuffleIdList(List<Long> idList) {
        if (CollectionUtils.isEmpty(idList)) {
            return;
        }
        Collections.shuffle(idList, new Random(activityWorkListQuery.getSeed()));
    }

    /**
     * 分页 idList
     */
    public List<Long> pagingIdList(List<Long> idList) {
        return CollectionUtils.pagingList(idList, activityWorkListQuery.getPageNum(), activityWorkListQuery.getPageSize());
    }
}
复制代码

ActivityWorkApplicationService

ActivityWorkApplicationService 作为服务层代码,执行接口查询逻辑,源码如下:

import com.shimengjie.wpm.work.domain.model.activity.ActivityWork;
import com.shimengjie.wpm.work.domain.model.activity.ActivityWorkListEntity;
import com.shimengjie.wpm.work.domain.model.activity.query.ActivityWorkListQuery;
import com.shimengjie.wpm.work.port.adapter.persistence.repository.MybatisActivityWorkRepository;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class ActivityWorkApplicationService {

    @Resource
    private MybatisActivityWorkRepository mybatisActivityWorkRepository;

    /**
     * 查询活动作品列表
     *
     * @param activityWorkListQuery 查询条件
     * @return List<ActivityWork>
     */
    public ActivityWorkListEntity queryActivityWorkList(ActivityWorkListQuery activityWorkListQuery) {
        ActivityWorkListEntity entity = new ActivityWorkListEntity(activityWorkListQuery);
        // 查询满足条件的作品ID
        List<Long> idList = mybatisActivityWorkRepository.queryActivityWorkIdListByTitle(activityWorkListQuery.getTitle());
        entity.setTotal(idList.size());
        entity.shuffleIdList(idList);
        // 分页后查询
        List<Long> subList = entity.pagingIdList(idList);
        List<ActivityWork> list = mybatisActivityWorkRepository.queryActivityWorkListByIds(subList);
        entity.setActivityWorkList(list);
        return entity;
    }
}
复制代码

总结

介绍了一种实现随机分页的方案,关键在于服务端和前端要维护一个随机种子,通过随机种子打乱数据的ID列表,但不适合大数量的业务场景。

参考阅读

1、聊聊order by rand()

关注微信公众号“CodeGo编程笔记”,每周发布编程文章,一起学习共同进步。

猜你喜欢

转载自juejin.im/post/7019290348308398087