Redis实现好友关注 | 黑马点评

news2024/10/7 8:21:07

目录

一、关注和取关

二、共同关注                               

三、关注推送(feed流)

1、Timeline模式的方案

拉模式

推模式

推拉结合模式

总结

2、推模式实现关注推送

需求

feed流分页问题

feed流的滚动分页

实现推送到粉丝的收件箱

滚动分页接收思路

实现滚动分页查询


一、关注和取关

加载的时候会先发请求看是否关注了,来显示是关注按钮还是取关按钮

当我们点击关注或取关之后再发请求进行操作

数据库表结构

关注表(主键、用户id、关注用户id)

需求

  1. 关注和取关接口
  2. 判断是否关注接口
/**
  * 关注用户
  * @param id
  * @param isFollow
  * @return
  */
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long id, @PathVariable("isFollow") Boolean isFollow){
    return followService.follow(id,isFollow);
}

/**
  * 判断是否关注指定用户
  * @param id
  * @return
  */
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long id){
    return followService.isFollow(id);
}
/**
  * 关注用户
  * @param id 
  * @param isFollow
  * @return
  */
@Override
public Result follow(Long id, Boolean isFollow) {
    //获取当前用户id
    Long userId = UserHolder.getUser().getId();
    //判断是关注操作还是取关操作
    if(BooleanUtil.isTrue(isFollow)){
        //关注操作
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(id);
        save(follow);
    }else{
        //取关操作
        remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",id));
    }
    return Result.ok();
}

/**
  * 判断是否关注指定用户
  * @param id
  * @return
  */
@Override
public Result isFollow(Long id) {
    //获取当前用户id
    Long userId = UserHolder.getUser().getId();
    Integer count = query().eq("user_id", userId).eq("follow_user_id", id).count();
    if(count>0){
        return Result.ok(true);
    }
    return Result.ok(false);
}

二、共同关注                               

需求:利用redis中恰当的数据结构,实现共同关注功能,在博主个人页面展示当前用户和博主的共同好友

可以用redis中set结构的取交集实现

 先在关注和取关增加存入redis

/**
  * 关注用户
  * @param id
  * @param isFollow
  * @return
  */
@Override
public Result follow(Long id, Boolean isFollow) {
    //获取当前用户id
    Long userId = UserHolder.getUser().getId();
    String key = "follow:" + userId;
    //判断是关注操作还是取关操作
    if(BooleanUtil.isTrue(isFollow)){
        //关注操作
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(id);
        boolean success = save(follow);
        if(success){
            //插入set集合中
            stringRedisTemplate.opsForSet().add(key,id.toString());
        }
    }else{
        //取关操作
        boolean success = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", id));
        //从set集合中移除
        if(success){
            stringRedisTemplate.opsForSet().remove(key,id.toString());
        }
    }
    return Result.ok();
}

然后就可以开始写查看共同好友接口了

/**
  * 判断是否关注指定用户
  * @param id
  * @return
  */
@GetMapping("common/{id}")
public Result followCommons(@PathVariable("id") Long id){
    return followService.followCommons(id);
}
/**
  * 共同关注
  * @param id
  * @return
  */
@Override
public Result followCommons(Long id) {
    Long userId = UserHolder.getUser().getId();
    //当前用户的key
    String key1 = "follow:" + userId;
    //指定用户的key
    String key2 = "follow:" + id;
    //判断两个用户的交集
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
    if(intersect==null||intersect.isEmpty()){
        //说明没有共同关注
        return Result.ok();
    }
    //如果有共同关注,则获取这些用户的信息
    List<Long> userIds = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    List<UserDTO> userDTOS = userService.listByIds(userIds).stream().map(item -> (BeanUtil.copyProperties(item, UserDTO.class))).collect(Collectors.toList());
    return Result.ok(userDTOS);
}

三、关注推送(feed流)

关注推送也叫做fedd流,直译为投喂。为用户持续的提供"沉浸式"的体验,通过无限下拉刷新获取新的信息。feed模式,内容匹配用户。

Feed流产品有两种常见模式:

Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

  • 优点:信息全面,不会有缺失。并且实现也相对简单
  • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

  • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
  • 缺点:如果算法不精准,可能起到反作用

本例中是基于关注的好友来做Feed流的,因此采用Timeline的模式。

1、Timeline模式的方案

该模式的实现方案有

  • 拉模式
  • 推模式
  • 推拉结合 

拉模式

优点:节省内存消息,只用保存一份,保存发件人的发件箱,要读的时候去拉取就行了

缺点:每次读取都要去拉,耗时比较久

推模式

优点:延迟低

缺点:太占空间了,一个消息要保存好多遍

推拉结合模式

推拉结合分用户,比如大v很多粉丝就采用推模式,有自己的发件箱,让用户上线之后去拉取。普通人发的话就用推模式推给每个用户,因为粉丝数也不多直接推给每个人延迟低。粉丝也分活跃粉丝和普通粉丝,活跃粉丝用推模式有主机的收件箱,因为他天天都看必看,而普通粉丝用拉模式,主动上线再拉取,僵尸粉直接不会拉取,就节省空间。

总结

 由于我们这点评网站,用户量比较小,所以我们采用推模式(千万以下没问题)。

2、推模式实现关注推送

需求

(1)修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱

(2)收件箱满足可以根据时间排序,必须用redis的数据结构实现

(3)查询收件箱数据时,可以实现分页查询

要进行分页查询,那么我们存入redis采用什么数据类型呢,是list还是zset呢

feed流分页问题

假如我们在分页查询的时候,这个时候加了新的内容11, 再查询下一页的时候,6就重复出现了,为了解决这种问题,我们必须使用滚动分页

feed流的滚动分页

滚动分页就是每次都记住最后一个id,方便下一次进行查询,用这种lastid的方式来记住,不依赖于角标,所以我们不会收到角标的影响。所以我们不能用list来存数据,因为他依赖于角标,zset可以根据分数值范围查询。我们按时间排序,每次都记住上次最小的,然后从比这小的开始。

实现推送到粉丝的收件箱

修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱

@Override
public Result saveBlog(Blog blog) {
    // 1.获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 2.保存探店笔记
    boolean isSuccess = save(blog);
    if(!isSuccess){
        return Result.fail("新增笔记失败!");
    }
    // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
    List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
    // 4.推送笔记id给所有粉丝
    for (Follow follow : follows) {
        // 4.1.获取粉丝id
        Long userId = follow.getUserId();
        // 4.2.推送
        String key = FEED_KEY + userId;
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
    }
    // 5.返回id
    return Result.ok(blog.getId());
}

滚动分页接收思路

第一次查询是分数(时间)从1000(很大的数)开始到0(最小)这个范围,然后限制查3个(一页数量),偏移量是0,然后记录结尾(上一次的最小值)

以后每次都是从上一次的最小值到0,限定查3个,偏移量是1(因为记录的那个值不算),再记录结尾的值。

但是有一种情况,如果有相同的时间,分数一样的话,比如两个6分,而且上一页都显示完,我们下一页是按照第一个6分当结尾的,第二个6分可能会出现的,所以我们这个偏移量不能固定是1,要看有几个和结尾相同的数,如果是两个就得是2,3个就是3。

滚动分页查询参数:

  • 最大值:当前时间戳 | 上一次查询的最小时间戳
  • 最小值:0
  • 偏移量:0 | 最后一个值的重复数
  • 限制数:一页显示的数

实现滚动分页查询

前端需要传来两条数据,分别是lastId和offset,如果是第一次查询,那么这两个值是固定的,会由前端来指定,lastId是发起查询时的时间戳,而offset就是零,当后端查询完分页信息后需要返回三条数据,第一条自然就是分页信息,第二条是此次分页查询数据中最后一条数据的时间戳,第三条信息是偏移量,我们需要在分页查询后计算有多少条信息的时间戳与最后一条是相同的,作为偏移量来返回。而前端拿到这后两个参数之后就会分别保存在前端的lastId和offset中,下一次分页查询时就会将这两条数据作为请求参数来访问,然后不断循环上述过程,这样也就实现了分页查询。

定义返回值实体类

@Data
public class ScrollResult {
    private List<?> list;
    private Long minTime;
    private Integer offset;
}

Controller

@GetMapping("/of/follow")
public Result queryBlogOfFollow(
    @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
    return blogService.queryBlogOfFollow(max, offset);
}

BlogServiceImpl

@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    //获取当前用户
    Long userId = UserHolder.getUser().getId();
    //组装key
    String key = RedisConstants.FEED_KEY + userId;
    //分页查询收件箱,一次查询两条 ZREVRANGEBYSCORE key Max Min LIMIT offset count
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
    //若收件箱为空则直接返回
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }
    //通过上述数据获取笔记id,偏移量和最小时间
    ArrayList<Long> ids = new ArrayList<>();
    long minTime = 0;
    //因为这里的偏移量是下一次要传给前端的偏移量,所以初始值定为1
    int os = 1;
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
        //添加博客id
        ids.add(Long.valueOf(typedTuple.getValue()));
        //获取时间戳
        long score = typedTuple.getScore().longValue();
        //由于数据是按时间戳倒序排列的,因此最后被赋值的就是最小时间
        if (minTime == score) {
            //如果有两个数据时间戳相等,那么偏移量开始计数
            os++;
        } else {
            //如果当前数据的时间戳与已经记录的最小时间戳不相等,则说明当前时间小于已记录的最小时间戳,将其赋给minTime
            minTime = score;
            //偏移量重置
            os = 1;
        }
    }
    //需要考虑到时间戳相等的消息数量大于2的情况,这时候偏移量就需要加上上一页查询时的偏移量
    os = minTime == max ? os : os + offset;

    //根据id查询blog
    String idStr = StrUtil.join(",", ids);
    //查询时需要手动指定顺序
    List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
    //这里还需要查询博客作者的相关信息,这里对比视频中,用一次查询代替了多次查询,提高效率
    List<Long> blogUserIds = blogs.stream().map(blog -> blog.getUserId()).collect(Collectors.toList());
    String blogUserIdStr = StrUtil.join(",", blogUserIds);
    HashMap<Long, User> userHashMap = new HashMap<>();
    userService.query().in("id", blogUserIds).last("ORDER BY FIELD(id," + blogUserIdStr + ")").list().
        stream().forEach(user -> {
        userHashMap.put(user.getId(), user);
    });
    //为blog封装数据
    Iterator<Blog> blogIterator = blogs.iterator();
    while (blogIterator.hasNext()) {
        Blog blog = blogIterator.next();
        User user = userHashMap.get(blog.getUserId());
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
        blog.setIsLike(isLikeBlog(blog.getId()));
    }
    //返回封装数据
    ScrollResult scrollResult = new ScrollResult();
    scrollResult.setList(blogs);
    scrollResult.setMinTime(minTime);
    scrollResult.setOffset(os);
    return Result.ok(scrollResult);
}

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

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

相关文章

(学习笔记)opencv和dlib的基础操作

来源&#xff1a;投稿 作者&#xff1a;LSC 编辑&#xff1a;学姐 本篇文章将讲述作者对opencv和dlib基础操作的学习笔记。 首先来看opencv的11种基础操作 (1)imread 读取图片 (2)resize 图片缩放 (3)cvtColor 灰度化 (4)threshold 阈值化 (5)bitwise_not 图像取反 (6)a…

JavaEE-初识网络

目录一、局域网二、广域网三、网络通信基础3.1 IP地址3.2 端口号3.3 协议四、协议分层五、封装和分用一、局域网 局域网&#xff0c;网络种类&#xff0c;覆盖范围一般是方圆几千米之内&#xff0c;其具备的安装便捷、成本节约、扩展方便等特点使其在各类办公室内运用广泛。局…

有序列表标签与无序列表标签

<!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title></title> </head> <body bgcolor"bisque"> <!-- 在ol有序列表标签和ul无序列表标签…

【初识数据库】进入数据库的大门+数据库基本操作

前言&#xff1a; 大家好&#xff0c;我是良辰丫&#x1f353;&#x1f353;&#x1f353;&#xff0c;这个专栏我将带领大家去探索数据库的汪洋大海&#xff0c;我主要使用的数据库软件是MySQL&#xff0c;数据库软件大同小异&#xff0c;嘿嘿嘿&#xff0c;废话不多说&#x…

分布式事务Seata学习笔记

目录 1.Seata 是什么 2. Seata快速开始 2.1 Seata Server&#xff08;TC&#xff09;环境搭建 db存储模式Nacos(注册&配置中心)部署 3. Seata Client快速开始 4.demo源码:springcloudAlibaba: Alibaba微服务学习demo 1.Seata 是什么 Seata 是一款开源的分布式事务…

神级程序员上网都在看什么?

同样都是上网&#xff0c;为什么大神总能在最短时间内汲取最有效的信息呢&#xff1f; 不怕大神不上网&#xff0c;就怕大神上网比你精&#xff01;其实&#xff0c;你与大神的区别都藏在细节中。 这些神级网站都是我偷看他们电脑抄来的&#xff08;误&#xff09;&#xff0c;…

超链接(还称热连接)标签

<!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>超链接(还称热连接)</title> </head> <body> <!-- 超链接的特点&#xff1a; 1、有…

Python内置函数

5.6 Python内置函数 Python自带的所有内置函数如下&#xff1a; Python函数 以上为Python3.10中所有的内置函数&#xff0c;其中绝大部分在前面的学习当中都已经接触过了。这里在补充介绍下没有提到或学习到的函数。 5.6.1 callable(object) 描述 如果参数 object 是可调…

Java面试题,mysql相关基础问题(全是自己的总结,如有不对,敬请斧正)

mysql相关基础问题一、mysql中int类型单引号问题二、静态SQL与动态SQL1. 静态SQL2. 动态SQL3. 动态SQL与静态SQL的区别与联系一、mysql中int类型单引号问题 今天写mysql的时候发现了一个问题&#xff0c;在查询的时候不小心把int类型的id加上引号查询了&#xff0c;但是也能查…

Day08 C++STL入门基础知识五——vector容器(下) 插入删除-数据存取-交换容器-预留空间【全面深度剖析+例题代码展示】

More haste, less speed. 欲速则不达 文章目录1. 承接上文2. 插入操作2.1 函数原型(总括)2.2 尾插尾删2.2.1 操作2.2.2 代码展示2.2.3 测试结果2.3 迭代器插入2.3.1 操作2.3.2 代码展示2.3.3 测试结果2.4 think小思考2.4.1 小疑问2.4.2 思路2.4.3 代码展示2.4.4 测试结果3. 删除…

数据结构---堆

堆 定义 基本操作 建堆 堆排序 优先队列 一、堆的定义&#xff1a; 堆必须是一个完全二叉树 还得满足堆序性 什么是完全二叉树呢&#xff1f; 完全二叉树只允许最后一行不为满 且最后一行必须从左到右排序 最后一行元素之间不可有间隔&#xff0c;中间不可有空缺 如下几棵树…

CSS基础学习

HTMLCSSJavaScript 结构表现交互 学习内容&#xff1a; CSS是什么CSS怎么用&#xff08;快速入门&#xff09;CSS选择器&#xff08;重点难点&#xff09;美化网页&#xff08;文字&#xff0c;阴影&#xff0c;超链接&#xff0c;列表&#xff0c;渐变…&#xff09;盒子模型…

基于电压型磁链观测器的异步电机矢量控制学习

导读&#xff1a;本期主要介绍电压型磁链观测器&#xff08;voltage flux observer&#xff09;在静止坐标系下的表现形式&#xff0c;应用在异步电机矢量控制系统中。如需要文中的仿真模型&#xff0c;关注微信公众号&#xff1a;浅谈电机控制&#xff0c;获取。一、引言磁链估…

万字长文--详解Node.js(快速入门)

Node.js基础与扩展Node.js1、初识Node.js与内置模块1.1 Node.js初识1.2 fs文件系统模块1.3 path路径模块1.4 http模块2、模块化2.1 模块化的基本概念2.2 Node.js中模块化2.3 npm与包2.4 模块的加载机制3、Express3.1 初识Express3.2 Express路由3.3 Express中间件3.4 使用Expre…

LeetCode刷题复盘笔记—一文搞懂贪心算法之45. 跳跃游戏 II问题(贪心算法系列第五篇)

今日主要总结一下可以使用贪心算法解决的一道题目&#xff0c;45. 跳跃游戏 II 题目&#xff1a;45. 跳跃游戏 II Leetcode题目地址 题目描述&#xff1a; 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度…

论文投稿指南——中文核心期刊推荐(铁路运输)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

Windows卸载与清除工具“ Geek 与 CCleaner ” (带你快速了解)

&#x1f4dc; “作者 久绊A” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴。 &#x1f341; 操作系统【带你快速了解】对于电脑来说&#xff0c;如果说…

什么是闭包,Python闭包

闭包&#xff0c;又称闭包函数或者闭合函数&#xff0c;其实和前面讲的嵌套函数类似&#xff0c;不同之处在于&#xff0c;闭包中外部函数返回的不是一个具体的值&#xff0c;而是一个函数。一般情况下&#xff0c;返回的函数会赋值给一个变量&#xff0c;这个变量可以在后面被…

剑指 Offer 第5天、第6天

目录 剑指 Offer 04. 二维数组中的查找 剑指 Offer 50. 第一个只出现一次的字符 剑指 Offer 11. 旋转数组的最小数字 剑指 Offer 32 - I. 从上到下打印二叉树 剑指 Offer 32 - II. 从上到下打印二叉树 II 剑指 Offer 32 - III. 从上到下打印二叉树 III 剑指 Offer 04. 二…

Java线程池应用实例

线程池的学习基本概念好处应用场景ThreadPoolExecutor实例理解&#xff1a;执行流程自定义线程池4大核心参数测试demo结论&#xff1a;ExecutorService常用方法思考获取ExecutorService代码示例ScheduleExecutorService常用获取方式如下ScheduledExecutorService常用方法如下:代…