Redis:原理速成+项目实战——Redis实战12(好友关注、Feed流(关注推送)、滚动分页查询)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战11(达人探店(Redis实现点赞、热榜))
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

上一节用Redis实现了对笔记的点赞,现实生活中当访问其他用户的笔记的时候,一般也会有个关注该用户的按钮,比如你可以写这篇博客的作者点点关注,嘻嘻嘻。

除了简单的关注和取关,还可以在用户之间查看到共同关注的用户,并实现关注推送,最后粉丝查看消息推送的时候要能够实现滚动分页查询,这样看起来更符合现实的社交类APP。

这一部分有些内容就是单纯的CRUD,不得不说做CRUD真是爽,简单且不耗时间,后面的滚动分页查询细节还是有点多的。

好友关注、推送、滚动分页查询

  • 关注和取关
  • 用户主页
  • Redis实现共同关注
    • 改造关注功能
    • 实现共同关注
  • Feed流(关注推送)
    • 方案分析
    • 推送到粉丝收件箱
    • 滚动分页查询收件箱
      • 思路
      • 实现

关注和取关

当我们点击关注/取关按钮的时候,将会发起请求,请求包含被关注用户的id,以及true或false来分别表示你的动作是关注还是取关。
因此,当我们打开笔记的详情页的时候,就应该要主动发起一个GET请求,携带信息,来表示你有没有关注过当前用户。
基于此,我们要实现2个接口:

1、关注和取关
2、判断是否关注

用户之间是多对多之间的关系,需要一个中间表tb_follow:
在这里插入图片描述
我以前做项目的时候都是喜欢在这里加一个boolean字段,关注了就true否则就false,但是这边没有这个字段,我也不做修改了,就这么做下去,当取关的时候就直接删除这条数据就好了。
这个业务其实就是增删改查,非常简单。

FollowController:

@RestController
@RequestMapping("/follow")
public class FollowController {@Resourceprivate IFollowService followService;@PutMapping("/{id}/{isFollow}")public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){return followService.follow(followUserId, isFollow);}@GetMapping("/or/not/{id}")public Result isFollow(@PathVariable("id") Long followUserId){return followService.isFollow(followUserId);}
}

FollowServiceImlp:

@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {@Overridepublic Result follow(Long followUserId, Boolean isFollow) {Long userId = UserHolder.getUser().getId();//判断当前状态是关注还是取关if(isFollow){//关注,新增数据Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);save(follow);}else{//取关,删除数据remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));}return Result.ok();}@Overridepublic Result isFollow(Long followUserId) {Long userId = UserHolder.getUser().getId();//查询是否关注Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();return Result.ok(count > 0);}
}

用户主页

当在笔记中点击该用户头像的时候会跳转到其主页:
在这里插入图片描述
在这里我们应该要发起2个GET请求:
1、根据该用户的id查询该用户
2、查询该用户的所有笔记
其实还是一通增删改查。

UserController:

	@GetMapping("/{id}")public Result queryUserById(@PathVariable("id") Long userId){return userService.queryUserById(userId);}

UserServiceImpl:

	@Overridepublic Result queryUserById(Long userId) {User user = getById(userId);if(user == null){return Result.ok();}UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);return Result.ok(userDTO);}

BlogController:

	@GetMapping("of/user")public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current,@RequestParam("id") Long id){return blogService.queryBlogByUserId(current, id);}

BlogServiceImpl:

	@Overridepublic Result queryBlogByUserId(Integer current, Long id) {//根据用户查询笔记Page<Blog> page = query().eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));//获取当前页信息List<Blog> records = page.getRecords();return Result.ok(records);}

在这里插入图片描述

Redis实现共同关注

这个功能的实现思路也很容易,因为我们已经拥有了当前用户以及写文章用户的id,都根据id查询关注以后,求个交集即可。
为了提高性能,在Redis中选用set来做,set也拥有求交集的命令。
但是,这就有一个问题,要实现交集,我们首先要找到这两个用户对应的set集合,所以每当用户点击关注功能的时候,都应该将自己关注的这个用户id保存到Redis中去,这样会方便我们后序实现共同关注功能的查询。

改造关注功能

每次关注的时候,关注对象除了要放在数据库中,还要放到Redis中:

	@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result follow(Long followUserId, Boolean isFollow) {Long userId = UserHolder.getUser().getId();String key = "follows:" + userId;//判断当前状态是关注还是取关if(isFollow){//关注,新增数据Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);boolean isSuccess = save(follow);if(isSuccess){//放入集合stringRedisTemplate.opsForSet().add(key, followUserId.toString());}}else{//取关boolean isSuccess = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));if(isSuccess){//从Redis中移出关注用户的idstringRedisTemplate.opsForSet().remove(key, followUserId.toString());}}return Result.ok();}

实现共同关注

FollowServiceImpl:

	@Overridepublic Result followCommons(Long id) {Long userId = UserHolder.getUser().getId();String key1 = "follows:" + userId;String key2 = "follows:" + id;Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);if(intersect == null || intersect.isEmpty()){return Result.ok(Collections.emptyList());}//将id解析成Long型List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());//查询用户,需要注入IuserService类,查询出来再转成DTOList<UserDTO> users = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(users);}

在这里插入图片描述

Feed流(关注推送)

方案分析

关注推送也叫Feed流,也叫投喂,为用户持续提供体验,通过无线下拉刷新获取新信息。
Feed不再是用户自行去查询内容,而是内容自动推送匹配给用户。
Feed流产品有2种常见模式:

TimeLine:不做内容筛选,简单按照发布时间排序,常用于好友或关注
(1)优点:信息全面,不会有缺失,实现简单
(2)缺点:信息噪音多,用户不一定感兴趣
智能排序:利用智能算法屏蔽违规的、用户不感兴趣的内容。推送用户感兴趣信息
(1)优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
(2)缺点:若算法不精准,可能起反作用

这里是基于好友的Feed流,所以使用TimeLine模式,要做智能排序的话要自行去研究智能算法,这个还是要花不少时间看看论文啥的。TimeLine模式的实现方案有3种:
(1)拉模式(读扩散):每个用户发完就放在发件箱,要读的时候就把所有关注的人的消息都读出来
(2)推模式(写扩散):发完笔记直接就推给每个粉丝的收件箱,收件箱做排序
(3)推拉结合:读写混合,结合两种模式的优点(活跃粉丝的用推模式,普通粉丝用拉模式)

因此,对于有大V的那类社交软件(例如围脖),最好的方式是,当一个用户发笔记的时候,要能直接发到其活跃粉丝的收件箱中,同时自己的邮件也发送到自己的发件箱中,当普通的用户要读取自己的笔记的时候再拿出来。
在这个项目里,将会使用推模式,当每个人发表探店笔记的时候,都需要推送笔记id到其粉丝的收件箱中,粉丝需要的时候就根据这个id即可查询到笔记。
我们可以利用Redis的数据结构来实现需求:

收件箱要根据时间戳排序,需要用Redis的数据结构,容易想到SortedSet。
查收件箱时,要能够分页查询,需要数据集合有角标,容易想到List。

虽然SortedSet没有分页查询需要的角标,但SortedSet却具有排名性质。又因为Feed流的数据是会不断更新的,数据角标也会变化,因此不能用传统的分页模式。
传统分页会出现重复读取的情况:
在这里插入图片描述
因此需要使用滚动分页查询:
在这里插入图片描述
因此,我们应该使用SortedSet,并且根据score来查询。

推送到粉丝收件箱

修改saveBlog的业务,使其能够推送到收件箱:

	public Result saveBlog(Blog blog) {UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());boolean isSuccess = save(blog);if(!isSuccess){return Result.fail("新增笔记失败");}//查询粉丝List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();//推送id给粉丝for (Follow follow : follows) {//获取粉丝IdLong userId = follow.getUserId();//推送到粉丝的收件箱(SortedSet)String key = "feed:" + userId;stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());}return Result.ok(blog.getId());}

在这里插入图片描述
再发一条,粉丝可以在收件箱中看到:
在这里插入图片描述

滚动分页查询收件箱

已经推送,现在需要粉丝来查询。需要再熟悉一下SortedSet的一些相关命令,可以看我之前的文章:
Redis常见命令

思路

如果我们根据角标来查询(ZREVRANGE),就会出现重复查询的情况,因此应该要以分数来查询(ZRANGEBYSCORE),下一页的查询直接根据上一页查询中分数的最小值的角标位置即可。

ZREVRANGEBYSCORE key MAX_SOCRE MIN_SOCRE WITHSCORES LIMIT L R
表示查询分数区间在(MAX_SCORE, MIN_SOCRE]的元素中的[L, R)之间的元素

由于时间戳可能是会有一样的,所以查询出来的结果,要查询起来还得用(L,R]来跳过,根据业务,可以将滚动分页查询设置为:

MAX_SOCRE:上一次查询的最小时间戳 | 当前时间戳(若是第一页的话,当前时间戳一定会大于容器内所有score)
MIN_SCORE:0
offset:在上一次的结果中,与最小值一样的元素的个数 | 0(第一页第一次查询)
count:固定的指定查几条

在个人主页的“关注”中,携带上一次查询的最小时间戳与偏移量,使用GET查询推送的Blog,并返回List<Blog>、本次查询的最小时间戳以及偏移量。

实现

1、封装一下返回值:

@Data
public class ScrollResult {private List<?> list;//定义成泛型private Long minTime;private Integer offset;
}

BlogController:

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

BlogServiceImpl:

@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {//获取当前用户,查询其收件箱Long userId = UserHolder.getUser().getId();String key = FEED_KEY + userId;//FEED_KEY = "feed:"Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);if(typedTuples == null || typedTuples.isEmpty()){return Result.ok();}//解析数据,blogId、minTime、offsetList<Long> ids = new ArrayList<>(typedTuples.size());long minTime = 0;int os = 1;//数一下最小值的个数for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {//获取BlogIdString idStr = tuple.getValue();ids.add(Long.valueOf(idStr));//获取分数(时间戳)long time = tuple.getScore().longValue();if(time == minTime) os++;else{minTime = time;os = 1;}}//查询blog,因为mysql会用IN来查询,所以需要指定OrderBYString idStr = StrUtil.join(",", ids);List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();//blog要带上自己是否点赞过的信息for (Blog blog : blogs) {//查询blog相关用户queryBlogUser(blog);//查询blog是否被点赞isBlogLiked(blog);}//封装返回ScrollResult r = new ScrollResult();r.setList(blogs);r.setOffset(os);r.setMinTime(minTime);return Result.ok(r);}

在这里插入图片描述
这时候关注的人再发一条笔记,往下滚动看不见,但是网上滚动即可刷新,然后发现这一条:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年全国职业院校技能大赛软件测试赛题—单元测试卷⑨

单元测试 一、任务要求 题目1&#xff1a;根据下列流程图编写程序实现相应分析处理并显示结果。返回文字“xa*a*b的值&#xff1a;”和x的值&#xff1b;返回文字“xa-b的值&#xff1a;”和x的值&#xff1b;返回文字“xab的值&#xff1a;”和x的值。其中变量a、b均须为整型…

【Linux实用篇】项目部署 基于Shell脚本自动部署

目录 1. 项目部署 1.1 手动部署项目 1.2 基于Shell脚本自动部署 1.2.1 介绍 1.2.2 推送代码到远程 1.2.3 Git操作 1.2.4 Maven安装 1.2.5 Shell脚本准备 1.2.6 Linux权限 1.2.7 授权并执行脚本 1.2.8 设置静态IP 1. 项目部署 之前我们讲解Linux操作系统时&#xff0…

【Mybatis系列】Mybatis空值关联

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

操作系统-操作系统的概念和功能

文章目录 大家熟悉的操作系统总览操作系统的概念&#xff08;定义&#xff09;操作系统的功能和目标-作为系统资源的管理者操作系统的功能和目标-向上层提供方便易用的服务图形化用户接口联机命令接口脱机命令接口程序接口小结 操作系统的功能和目标-作为最解决硬件的层次小结 …

6. 逻辑删除

逻辑删除对应的是物理删除&#xff0c;分别介绍一下这两个概念&#xff1a; 物理删除 &#xff1a;指的是真正的删除&#xff0c;即&#xff1a;当执行删除操作时&#xff0c;将数据表中的数据进行删除&#xff0c;之后将无法再查询到该数据逻辑删除 &#xff1a;并不是真正意…

数据结构排序——详细讲解归并排序(c语言实现递归及非递归)

上次是快排和冒泡&#xff1a;数据结构排序——详解快排及其优化和冒泡排序(c语言实现、附有图片与动图示意&#xff09; 今天为大家带来归并排序 文章目录 1.基本思想2.递归实现3.非递归实现 1.基本思想 归并排序是一种分治算法&#xff0c;它将序列分成两个子序列&#xff0…

virtualbox Ubuntu 网络连接

一、网络连接需求1—— 上网&#xff1a; 虚拟机默认的NAT连接方式&#xff0c;几乎不需要怎么配置&#xff0c;即可实现上网。 enp0s17以太网必须要开启&#xff0c;才能上网&#xff1b; 但是主机ping不通虚拟机&#xff0c;貌似可以ping 127.0.0.1; 二、主机和虚拟机相互p…

Whisper: openAI开源准确率最高的通用语言语音识别

简介 我们研究了仅通过预测大量互联网音频录音的语音处理系统的能力。当扩大到68万小时的多语言和多任务监督时&#xff0c;生成的模型可以很好地泛化到标准基准&#xff0c;而且通常可以与之前的全监督结果相竞争&#xff0c;但在zero-shot识别设置中&#xff0c;无需进行任何…

5.MapReduce之Combiner-预聚合

目录 概述本地预计算 Combiner 意义实践前提代码日志观察 结束 概述 在 MR、Spark、Flink 中&#xff0c;常用的减少网络传输的手段。 通常在 Reducer 端合并&#xff0c;shuffle 的数据量比在 Mapper 端要大&#xff0c;根据业务情况及数据量极大时&#xff0c;将大幅度降低效…

JVM基础(11)——G1垃圾回收器

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 学习必须往深处挖&…

使用Nginx作为反向代理服务器在Linux中的最佳实践

在Linux环境下&#xff0c;Nginx因其高效性能、稳定性以及丰富的功能集而广泛用于作为反向代理服务器。以下是在Linux中使用Nginx作为反向代理服务器的最佳实践&#xff1a; 1. 安装与配置 首先&#xff0c;确保你的Linux发行版已经安装了Nginx。大多数Linux发行版都提供了Ng…

OpenCV——图像按位运算

目录 一、算法概述1、逻辑运算2、函数解析3、用途 二、代码实现三、结果展示 OpenCV——图像按位运算由CSDN点云侠原创&#xff0c;爬虫自重。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫。 一、算法概述 1、逻辑运算 OpenCV4 针对两个图像之…