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

news2024/11/18 3:27:57

一、 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/604530.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

delphi 开发虚拟摄像头

用directshow创建一个虚拟摄像头 然后注册到系统&#xff0c; 在用一个main app 调用摄像头&#xff0c;往里面写流&#xff0c; 可以是屏幕录制&#xff0c;可以是播放的多媒体文件 然后具体是要实现录屏&#xff0c;或者播放多媒体文件&#xff0c;在虚拟摄像头插件中fil…

Godot 4 源码分析 - 初探

准备研究GoDot 4源码。 源码下载 获取源代码 在进入 SCons 构建系统并编译 Godot 之前&#xff0c;你需要将 Godot 的源代码下载到本地。 源代码位于 GitHub 上, 虽然你可以通过网站手动下载它, 但是通常你希望通过 git 版本控制系统来下载. 如果你是为了做贡献或拉动请求…

国产新秀---XS5018A,芯昇,图像信号处理芯片

国产视频处理芯片&#xff0c;大崛起。 XS5018A 是一款针对 CMOS 图像传感器的高性价比图像信号处理芯片&#xff0c;支持 1M/2M 像素 图像传感器&#xff0c;一组 10-bit DVP 输入接口&#xff0c; ISP 具备优异的 3D 降噪功能&#xff0c;标清模拟输出支持 960…

Java官方笔记6继承

继承 Java只有单继承&#xff0c;最顶级的父类是Object。 子类会继承父类的fields和methods&#xff0c;而不会继承constructors&#xff0c;因为constructors不属于methods&#xff0c;但是子类可以通过super调用父类的constructor。 子类继承父类的范围是&#xff1a;public、…

记录--让URL地址都变成了ooooooooo

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 发现一个很有创意的小工具网站&#xff0c;如封面图所示功能很简单&#xff0c;就是将一个URL地址转换为都是 ooooooooo 的样子&#xff0c;通过转换后的地址访问可以转换回到原始地址&#xff0c;简单…

chatgpt赋能python:Python创建Word文档指南

Python创建Word文档指南 在今天的数字时代&#xff0c;Word文档仍然是最常见和使用的文档类型之一。Python是一个强大的编程语言&#xff0c;可以用于自动化创建各种类型的文档&#xff0c;包括Word文档。在本篇文章中&#xff0c;我们将介绍如何使用Python创建Word文档&#…

Rust每日一练(Leetday0019) 跳跃游戏、合并区间、插入区间

目录 55. 跳跃游戏 Jump Game &#x1f31f;&#x1f31f; 56. 合并区间 Mmerge Intervals &#x1f31f;&#x1f31f; 57. 插入区间 Insert Interval &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专…

KP3114电源IC介绍--【其利天下】

KP3114 是一款高性能低成本 PWM 控制功率开关&#xff0c;适用于离线式小功率降压型应用场合&#xff0c;外围电路简单、器件个数少。同时产品内置高耐压 MOSFET 可提高系统浪涌耐受能力。 与传统的 PWM 控制器不同&#xff0c;KP3114 内部无固定时钟驱动 MOSFET&#xff0c;系…

为什么陶瓷板视觉技术会越来越受到人们的青睐?

随着科技的不断进步和应用&#xff0c;陶瓷板视觉技术越来越受到人们的青睐。陶瓷板视觉技术是一种新型的非接触式检测技术&#xff0c;它可以在生产过程中对产品进行高效、准确的检测和质量控制。下面我们来详细了解一下陶瓷板视觉技术的优势和应用。 一、优势 1.高效性 陶…

10款抓包工具优缺点对比

1. Wireshark 介绍&#xff1a;Wireshark是一个免费的网络协议分析工具&#xff0c;支持Windows、macOS和Linux等多个平台。它可以捕获和分析网络数据包&#xff0c;支持多种网络协议解码。优点&#xff1a;功能强大、支持多种协议解码、支持多平台、过滤器功能强大、免费开源。…

QT+OpenGL实例化和抗锯齿

QTOpenGL实例化和抗锯齿 本篇完整工程见gitee:QtOpenGL 对应点的tag&#xff0c;由turbolove提供技术支持&#xff0c;您可以关注博主或者私信博主 实例化 如果我们需要渲染大量物体时&#xff0c; 代码看起来会像这样&#xff1a; for(int i 0; i < amount; i) {DoSom…

Mybatis 别名的配置 + Mybatis配置类的调查

参考资料 Mybatis配置类别名mybatis-spring-boot-autoconfigure类型别名&#xff08;typeAliases&#xff09;SpringBoot五步配置Mybatis超简教程 目录 一. 使用场景二. 前期准备2.1 实体类2.2 查询接口 三. 配置方式1 配置文件的方式3.1 application.yml文件3.2 SQL的XMl文件…

做完这些lab,国内外大厂横着走

hi&#xff0c;大家好&#xff0c;这里是极客重生&#xff0c;坚实的计算机基础&#xff0c;对我们发展都是至关重要的&#xff0c;不管是校招还是社招&#xff0c;因此我和小伙伴一起收集了一些国内外硬核lib和开源项目来帮助大家学习和巩固基础&#xff08;动手实践&#xff…

计算机组成原理 第一章_概述

typora-copy-images-to: images 文章目录 typora-copy-images-to: images1.现代计算机的结构2.各硬件的工作原理2.1 主存储器的基本组成2.2 运算器的基本组成2.3 控制器的基本组成2.4 计算机的工作过程 3.计算机系统的层次结构4. 计算机的性能指标4.1存储器的性能指标4.2 CPU的…

14-Vue3快速上手

目录 1.Vue3简介2. Vue3带来了什么2.1 性能的提升2.2 源码的升级2.3 拥抱TypeScript2.4 新的特性 1、海贼王&#xff0c;我当定了&#xff01;——路飞 2、人&#xff0c;最重要的是“心”啊&#xff01;——山治 3、如果放弃&#xff0c;我将终身遗憾。——路飞 4、人的梦想是…

chatgpt赋能python:Python如何创建一个DataFrame

Python如何创建一个DataFrame 在数据科学和分析领域中&#xff0c;DataFrame是一种非常常见的数据结构。它类似于电子表格&#xff0c;可以存储和处理包含多个列和行的数据。在Python中&#xff0c;pandas库提供了DataFrame数据结构的支持。 什么是DataFrame&#xff1f; Da…

Hugging News #0602: Transformers Agents 介绍、大语言模型排行榜发布!

每一周&#xff0c;我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新&#xff0c;包括我们的产品和平台更新、社区活动、学习资源和内容更新、开源库和模型更新等&#xff0c;我们将其称之为「Hugging News」&#xff0c;本期 Hugging News 有哪些有趣的消息…

聚观早报 | B站第一季度净亏损同比下降72%;​必应聊天放宽限制

今日要闻&#xff1a;B站第一季度净亏损同比下降72%&#xff1b;必应聊天放宽限制&#xff1b;马斯克再次成为世界首富&#xff1b;英伟达CEO黄仁勋计划访华&#xff1b;联想moto razr 40系列新品发布 B站第一季度净亏损同比下降72% 6 月 1 日消息&#xff0c;哔哩哔哩&#x…

哈希表理论基础

目录 哈希表 哈希函数 哈希碰撞 一般哈希碰撞有两种解决方法&#xff0c; 拉链法和线性探测法。 拉链法 线性探测法 常见的三种哈希结构 set map 总结 哈希表 哈希表是根据关键码的值而直接进行访问的数据结构。 哈希表中关键码就是数组的索引下标&#xff0c;然后通…

Linux NGINX 优化与防盗链

----------------隐藏版本号------------------- 可以使用 Fiddler 工具抓取数据包&#xff0c;查看 Nginx版本&#xff0c; 也可以在 CentOS 中使用命令 curl -I http://192.168.80.101 显示响应报文首部信息。 curl -I http://192.168.80.101 方法一&#xff1a;修改配置文件…