背景
这段时间在整理项目代码时,发现一个两年前实习时做的随机分页功能,效果如下图所示:
这个功能的业务需求如下:在前端分页展示数据时,为了让每个数据都有相同的几率被展示,每次分页展示数据时,都是随机展示的,并要求每一页之间数据不能重复。
下面简单介绍一下这个功能的实现过程。
思路
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列表,但不适合大数量的业务场景。
参考阅读
关注微信公众号“CodeGo编程笔记”,每周发布编程文章,一起学习共同进步。