使用Redis的zset集合实现小程序的滚动分页

一、 Redis中,使用有序集合(sorted set)实现滚动分页的原理如下:

  1. 将每个文档的 score 值设置为时间戳(或根据其他规则计算的分数),将文档的 ID 作为 value,然后将其添加到有序集合中。
  2. 获取当前时间戳,作为查询时间点。
  3. 使用 ZRANGEBYSCORE 命令根据 score 值范围查询出 score 值在当前时间戳之前的所有文档 ID。
  4. 返回查询结果作为当前页的结果集。
  5. 将当前页的最后一个文档 ID 作为新的查询起点,重复以上步骤,直到遍历所有文档。

二、Redis中,(sorted set)命令详细说明

Redis中的sorted set(有序集合)是一个数据结构,它允许你存储一组有序的元素(成员),每个元素可以有一个分数(score),分数可以用于排序、限制范围或聚合操作等。sorted set是自动排序的,并且所有的成员都是唯一的,不允许重复。

以下是在Redis中操作sorted set的命令:

1 ZADD key score member:

向有序集合中添加一个元素。

ZADD myset 7.5 "apple"  
ZADD myset 9.0 "orange"  
ZADD myset 8.2 "banana"

2 ZRANGE key start stop [WITHSCORES]:

返回有序集合中指定范围内的元素。

ZRANGE myset 0 -1 WITHSCORES  
ZRANGE myset 1 2 WITHSCORES

结果:

127.0.0.1:6379> ZRANGE myset 0 -1 WITHSCORES  
1) "apple"
2) "7.5"
3) "banna"
4) "8.1999999999999993"
5) "orange"
6) "9"
127.0.0.1:6379> ZRANGE myset 1 2 WITHSCORES  
1) "banna"
2) "8.1999999999999993"
3) "orange"
4) "9"

3 ZREVRANGE key start stop [WITHSCORES]:

返回有序集合中指定范围内的元素,按照分数从大到小排序,与 ZRANGE 返回的结果相反。

ZREVRANGE myset 0 -1 WITHSCORES  
ZREVRANGE myset 1 2 WITHSCORES

结果:

127.0.0.1:6379> ZREVRANGE myset 0 -1 withscores
1) "orange"
2) "9"
3) "banna"
4) "8.1999999999999993"
5) "apple"
6) "7.5"
127.0.0.1:6379> ZREVRANGE myset 1 2 withscores
1) "banna"
2) "8.1999999999999993"
3) "apple"
4) "7.5"

4 ZREM key member:

从有序集合中删除指定元素。

ZREM myset "apple"  
ZREM myset "banana"

5 ZCARD key:

返回有序集合中元素的数量。

ZCARD myset
127.0.0.1:6379> ZCARD myset 
(integer) 2

6 ZSCORE key member:

返回指定元素在有序集合中的分数。

ZSCORE myset "banana"
127.0.0.1:6379> ZSCORE myset "banana"
"7.9000000000000004"

7 ZREMRANGEBYSCORE key min max:

删除有序集合中分数在指定范围内的所有元素

127.0.0.1:6379> ZREMRANGEBYSCORE myset 6.5 8.5
(integer) 2

8 ZREMRANGE BY PRIORITY key min max:

删除有序集合中优先级在指定范围内的所有元素。与ZREMRANGE BY SCORE命令类似。

三、具体实现步骤如下:

1 向有序集合中添加文档:

在文档添加时,为每个文档添加一个时间戳作为score值,并将其文档ID作为value值。例如,使用以下Java代码向有序集合中添加文档:

ZAddArgs zAddArgs = new ZAddArgs(score, value);  
redis.zAdd("docs", zAddArgs);

2 获取当前时间戳:

使用Java的System.currentTimeMillis()方法获取当前时间戳。

3 查询score值在当前时间戳之前的文档ID

使用以下Java代码查询有序集合中score值在当前时间戳之前的文档ID:

ZRangeArgs zRangeArgs = new ZRangeArgs(0, -1, score -> score < System.currentTimeMillis());  
List<String> docIds = redis.zRangeByScore("docs", zRangeArgs);

其中,ZRangeArgs构造函数中的参数0表示起始位置为0,-1表示结束位置为集合末尾,score -> score < System.currentTimeMillis()表示只返回score值小于当前时间戳的文档ID。

4 返回查询结果作为当前页的结果集:

将查到的文档ID作为当前页的结果集返回。

5 将当前页的最后一个文档ID作为新的查询起点,重复以上步骤,直到遍历所有文档。

例如,使用以下Java代码将当前页的最后一个文档ID作为新的查询起点:

String lastDocId = docIds.get(docIds.size() - 1);  
docIds = redis.zRangeByScore("docs", new ZRangeArgs(0, -1, score -> score < System.currentTimeMillis() - 86400000L), lastDocId, "LIMIT");

其中,ZRangeArgs构造函数中的参数表示从集合的起始位置开始返回文档ID,lastDocId表示只返回大于lastDocId且score值小于当前时间戳的文档ID,86400000L表示一天的毫秒数,表示向前滚动一页。

四、实例

1 回顾传统分页

传统的分页 前端参数一般传入当前页数curpage和页面长度paegsize 最终通过数据库limit curpage*(pageszie-1),pageszie 实现分页 假设两参数分别为1,5 即 limit 0,5 也就是查询序号0到4的5条数据 这时如果数据库新增了一条数据其序号为1。

如果查询下一页即limit 5,5 查询序号为5 到9的数据
在这里插入图片描述
如图所示,很显然值为四的数据被重复查了。 查了下比较流行的做法就是新增一个字段,记录数据插入的时间。 然后查寻第一页的时候记录当前时间,之后每次分页查询都需要带上这个时间 把比这个时间大的数据排除。该方案挺不错,但是要修改数据库,费事.

2 使用Redis的zset分页

1 分页存在的问题

每次插入数据库成功时,额外保存一份<数据id,插入时间>到redis的有序集合里 。这时就可以通过插入时间分页了。第一次查询返回按时间排序前1到5的数据, 然后记录当前的时间6 。 之后的查询带上这个时间。 返回从 比这个时间小的第一个数据(即为5)和其后的四条数据。
在这里插入图片描述
但是这也有个问题 假如同一时间新增很多条相同数据怎么办。
在这里插入图片描述
引入个新的变量offset 记录返回的数据中有几个和 他们最后一个数据时间相同 上图
第一次查询返回 10 9 8 7 6 6即为2。 然后下一次查询的参数即为最后一个数据的时间戳6 ,偏移量2 ,就能确定是从第三个6开始了

2 分页方法说明

ZREVRANGEBYSCORE key Max Min LIMIT offset count
ZREVRANGEBYSCORE z1 5 0 withscores limit 1 3

分页参数:
max: 当前时间戳 | 上一次查询人最小时间戳
min:0 最小值,不变
offset: 0 | 取决于上一次的结果,与最小元素的个数
count: 3 分页的页面大小,相当于pageSize

3 在点评小程序的应用

在本小程序中,我们要将粉丝关注我的博客数排序

  • 保存博客时,将我的博客发送到粉丝的信箱,也就是按粉丝的id与博客id对保存到redis中
 @PostMapping
    public Result saveBlog(@RequestBody Blog blog) {
    
    
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        Long userId = user.getId();
        blog.setUserId(userId);
        // 保存探店博客
        if (blogService.save(blog)) {
    
    
            Long currTime = System.currentTimeMillis();
//        博客推送给关注作者的人
//        1获得关注该作者的用户列表
            List<Follow> follows = followService.query().select("user_id").eq("follow_user_id", userId).list();
            for (Follow follow : follows
            ) {
    
    
                Long followId = follow.getUserId();
//
                String key = RedisConstants.FEED_KEY + followId;
                stringRedisTemplate.opsForZSet().add(key,String.valueOf(blog.getId()),currTime);
            }
            // 返回id
            return Result.ok(blog.getId());
        }
        return Result.fail("发布笔记失败");
    }
  • 读取信箱,按评分进行分页
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    
    
     //1 获取当前用户
     Long userId = UserHolder.getUser().getId();
     //2 查询收件箱
     String key = RedisKey.FEED_KEY + userId;
     //3 解析数据 blogId、minTime时间戳、offset
     Set<ZSetOperations.TypedTuple<String>> tuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
     //4 非空判断
     if (tuples == null || tuples.isEmpty()) {
    
    
         return Result.ok();
     }
     //5 遍历
     long minTime = 0; //获取时间错最小值,遍历完最后一个值
     int offsetResutl = 1; //偏移量,为最小值的个数
     List<Long> ids = new ArrayList<>(tuples.size());
     for (ZSetOperations.TypedTuple<String> tuple : tuples) {
    
    
         long time = tuple.getScore().longValue();
         if(time == minTime){
    
    
             offsetResutl ++ ;
         }else {
    
    
             //如果与最小时间不同,则最小时间重新赋值,将 offsetFirst重新赋值为1
             minTime = time;
             offsetResutl = 1;
         }
         String blogId = tuple.getValue();
         ids.add(Long.valueOf(blogId));
     }
     //6 根据id 查询blog
     String idsStr = StrUtil.join(",", ids);
     List<Blog> blogs = this.lambdaQuery().eq(Blog::getId, ids).last("order by field(id," + idsStr + ")").list();
     //7 封装结果
     ScrollResult scrollResult = new ScrollResult();
     scrollResult.setList(blogs);
     scrollResult.setOffset(offsetResutl);
     scrollResult.setMinTime(minTime);
     return Result.ok(scrollResult);
 }

UserHolder 实体类

public class UserHolder {
    
    
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
    
    
        tl.set(user);
    }

    public static UserDTO getUser(){
    
    
        return tl.get();
    }

    public static void removeUser(){
    
    
        tl.remove();
    }
}

UserDTO 实体类

@Data
public class UserDTO {
    
    
    private Long id;
    private String nickName;
    private String icon;
}

Blog 实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_blog")
public class Blog implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 商户id
     */
    private Long shopId;
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 用户图标
     */
    @TableField(exist = false)
    private String icon;
    /**
     * 用户姓名
     */
    @TableField(exist = false)
    private String name;
    /**
     * 是否点赞过了
     */
    @TableField(exist = false)
    private Boolean isLike;

    /**
     * 标题
     */
    private String title;

    /**
     * 探店的照片,最多9张,多张以","隔开
     */
    private String images;

    /**
     * 探店的文字描述
     */
    private String content;

    /**
     * 点赞数量
     */
    private Integer liked;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


}

Result实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    
    
    private Boolean success;
    private String errorMsg;
    private Object data;
    private Long total;

    public static Result ok(){
    
    
        return new Result(true, null, null, null);
    }
    public static Result ok(Object data){
    
    
        return new Result(true, null, data, null);
    }
    public static Result ok(List<?> data, Long total){
    
    
        return new Result(true, null, data, total);
    }
    public static Result fail(String errorMsg){
    
    
        return new Result(false, errorMsg, null, null);
    }
}

Follow实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_follow")
public class Follow implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 关联的用户id
     */
    private Long followUserId;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;


}

五、源码下载

https://gitee.com/charlinchenlin/koo-erp

猜你喜欢

转载自blog.csdn.net/lovoo/article/details/131015069