当前位置: 首页 > news >正文

如何使用Redis实现内容推送功能

导读

在日常使用中,我们经常能看见内容推送功能。

常见的场景有,比如你在bilibili关注了某个up主,当up主发布视频后,就会推送到你的收件箱或者是动态中,让粉丝能够及时得知所关注的人发布了内容。

又比如朋友圈,也是按照时间的顺序,将好友发布的动态推送给你,如果你下拉刷新,就可以获取到新的好友的动态。

想知道这些功能是如何实现的吗?接着往下看吧!

这个需求,其实我们又把他叫做Feed流,关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。

对于传统模式的内容解锁:我们是需要用户去通过搜索引擎或者是其他的方式去解锁想要看的内容

 而对于Feed模式,则是主动推送给用户内容

 Feed流的两种模式

Feed流的实现有两种模式:

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

  • 优点:信息全面,不会有缺失。并且实现也相对简单

  • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

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

  • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷

  • 缺点:如果算法不精准,可能起到反作用

我们本次关注-推送的功能,采用的就是Timeline的方式,只需要拿到我们关注用户的信息,然后按照时间排序即可。

该模式的实现方案有三种:拉模式,推模式,推拉结合

拉模式(读扩散)

该模式的核心含义就是:当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序

优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清除。

缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大。

推模式(写扩散)

推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了

优点:时效快,不用临时拉取

缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去

推拉结合模式

推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。

Redis实现思路

我们使用Redis来实现Feed流,就需要选择合适的数据结构。

Feed流有两种特点,一是需要进行时间排序,二是数据不断在更新变化,即角标不断变化。

可以进行排序的数据结构有list,sortedset,都满足第一点需求。

feed流推送过来的内容,我们通常不是一次性查询所有内容,而是需要分页查询。传统的分页查询使用page和size来圈定范围,但是feed流不断进行内容推送,数据不断进行更新(类似栈结构,时间戳越大的在上头),数据角标不断变化,使用传统的分页模式就会导致内容的重复查询。所以需要使用滚动查询,滚动查询指的是记录上一次所查询的位置,下次查询时接着往下查询。所以使用sortedset结构我们可以记录每次查询最小的时间戳,下次查询的时候再找比这个时间戳更小的,就实现了滚动查询的效果。而list结构只能通过角标查询。所以我们最终选择sortedset结构。

 

 

流程

1.保存笔记并推送

在用户发布笔记保存到数据库的同时,也需要将笔记id推送到粉丝的收件箱中,即sortedSet集合中。key是“feed:粉丝id”,value是blog的id,score是当前时间戳

savaBlog():

  1. 验证用户是否登录

  2. 验证blog是否完整(发布者id,内容,关联商户)

  3. 保存blog到数据库

  4. 推送blog给粉丝

    1. 获取登录用户粉丝集合

    2. 循环遍历每个粉丝,保存blogId到粉丝sortedset集合

  5. 返回blog的id

2.获取推送笔记

如何使用Redis的sortedSet结构来实现滚动分页,获取推送的笔记内容?

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

命令解释:根据score值进行降序排序,查询count条集合中score范围为max到min的值

  • key:键

  • max,min:查询max到min范围的值

  • [WITHSCORES ]:是否带上score值

  • offset:偏移量,表示对于max值的偏移量,0表示对max偏移量为0,即取max值

  • count:查询几条

实现滚动分页,我们将时间戳作为score值,对score值进行降序排序就可以根据发布时间对笔记进行排序。

滚动分页的基本思想是:记录上一次查询的最小时间戳,下次查询时从所记录的位置开始查询。如果是第一次查询,max值就是当前时间戳,offset值取0,min值取0(因为时间戳最小为0,不可能为负数);如果不是第一次查询,max值就是上一次查询的最小时间戳,offset值取1(即不包括上一次查询的位置,否则会重复查询),min值取0。

需要注意的是,offset的值第一次查询时为0,之后的查询不一定为1,因为有可能会出现时间戳相等的情况:

比如score值降序排序后为5 2 2 3 2 2,每次查询3条记录。

那么第一次所查到的score是5 2 2,此次记录的最小时间戳是2。下次查询时,redis会查找到score值为2的位置,即第一个2,然后根据偏移量进行便宜,如果偏移量为1,那么结果就会为 2 3 2 ,而不是 3 2 2。所以偏移量offset的值应该是当前查询最小时间戳的重复次数。

所以通过上面的分析,我们发现min和count值是不变的。max和offset都需要通过前端作为请求参数传递。

当第一次查询时,前端发送的请求参数max为当前时间戳,offset默认为0可以不传递,当后端查询出所需要的笔记集合,还需要记录本次查询的最小时间戳,以及计算偏移量,并返回给前端,这两个数据将作为下一次查询的参数。而前端每次上下拖动的时候,就会刷新会发起请求。

queryBlogOfFollow(max,offset):

  • 检验是否登录,获取当前登录用户

  • 查看收件箱

  • 滚动查询收件箱,获取笔记id

  • 根据笔记id查询笔记

  • 封装返回

 @Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {//获取当前用户Long userId = UserHolder.getUser().getId();//查看收件箱String key = FEED_KEY + userId;Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);//非空校验if(typedTuples == null || typedTuples.isEmpty()){return Result.ok();}//取出收件箱的笔记id放入集合List<Long> idList = new ArrayList<>(typedTuples.size());long minTime = 0;int os = 1;for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {idList.add(Long.valueOf(typedTuple.getValue()));//获取最小的时间戳,记录偏移量long time = typedTuple.getScore().longValue();if(time == minTime){os++;}else{minTime = time;os = 1;}}//根据id集合查询blogString idStr = StrUtil.join(",",idList);List<Blog> blogs = query().in("id", idList).last("ORDER BY FIELD(id," + idStr + ")").list();//查询blog点赞和相关用户for(Blog blog : blogs){queryBlogUser(blog);isBlogLiked(blog);}//封装并返回ScrollResult result = new ScrollResult();result.setList(blogs);result.setOffset(os);result.setMinTime(minTime);return Result.ok(result);}

  

相关文章:

如何使用Redis实现内容推送功能

导读 在日常使用中&#xff0c;我们经常能看见内容推送功能。 常见的场景有&#xff0c;比如你在bilibili关注了某个up主&#xff0c;当up主发布视频后&#xff0c;就会推送到你的收件箱或者是动态中&#xff0c;让粉丝能够及时得知所关注的人发布了内容。 又比如朋友圈&…...

怎么对视频进行压缩?

怎么对视频进行压缩&#xff1f;视频压缩&#xff0c;我们都知道是将视频文件进行压缩变小的过程&#xff0c;是我们日常办公中较为常用的手段。现如今&#xff0c;在视频技术不断发展与创新的基础上&#xff0c;视频分辨率也在不断提高&#xff0c;进而导致文件占有量也非常大…...

redisson配置类---SpringBoot集成、redis单机和集群模式配置

1项目配置文件&#xff1a; 1.1&#xff1a;pom.xml <dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.17.7</version></dependency> 1.2 application.yml配置…...

瓴羊发布All in One 产品,零售SaaS的尽头是DaaS?

“打破烟囱、化繁为简&#xff0c;让丰富的能力、数据和智能All in One”&#xff0c;这是瓴羊新发布的产品瓴羊One承担的使命&#xff0c;也意味着瓴羊DaaS事业迈入了一个新阶段。 成立伊始&#xff0c;瓴羊就打出了“Not SaaS&#xff0c;But DaaS”旗号&#xff0c;将自己的…...

win10中Docker安装、构建镜像、创建容器、Vscode连接实例

Docker方便一键构建项目所需的运行环境&#xff1a;首先构建镜像(Image)。然后镜像实例化成为容器(Container)&#xff0c;构成项目的运行环境。最后Vscode连接容器&#xff0c;方便我们在本地进行开发。下面以一个简单的例子介绍在win10中实现&#xff1a;Docker安装、构建镜像…...

贝锐蒲公英:快速搭建连锁门店监控体系,赋能企业高效管理

随着国民生活水平的提高和零售场景的变革&#xff0c;消费者对于餐饮类目的消费支出不断增加&#xff0c;线下社区生鲜商超作为下沉市场最主要的消费场景之一&#xff0c;蕴藏着巨大价值机会。 对于线下连锁生鲜超市而言&#xff0c;连锁门店多、员工多&#xff0c;门店管理时会…...

c++ WinInet InternetOpenUrl下载中文文件

windows自带的WinInet,几个函数就可以实现http文件下载, 且可获取文件大小,进度条等。 在用WinInet下载文件时,遇到个问题, 如果是中文,下载下来的文件大小为0 英文文件正常,为什么呢? bool WWWFileBuffer(const char* host, const char* path, char* outBuffer, in…...

算法通过村第三关-数组青铜笔记|单调数组

文章目录 前言单调数组问题搜索插入位置&#xff1a;数组合并问题&#xff1a;总结 前言 提示&#xff1a;本份真诚面对自己、坦然无碍面对他人&#xff0c;就是优雅。 数组中的比较经典性问题: 单调数组问题数组合并问题 单调数组问题 参考例子&#xff1a;896. 单调数列…...

Springboot MultipartFile文件上传与下载

yml文件配置是否可以上传及上传附件大小 servlet:multipart:# 允许文件上传enabled: true# 单个文件大小max-file-size: 20MB# 设置总上传的文件大小max-request-size: 50MB /*** param files* param request* Description 上传文件* Throws* Return java.util.List* Date 202…...

js this变量

js this变量 有个比较特殊的箭头函数没有自己的this&#xff0c;而是继承了外部作用域的this...

Ubuntu ip冲突,修改静态IP方法

虚拟机克隆Ubuntu造成的IP地址相同冲突的问题_虚拟机ip冲突怎么解决_昌哥不爱晚睡的博客-CSDN博客...

windows下dll文件的创建详细教程

1、前言 dll文件是啥&#xff0c;就不作过多赘述了。现在直接教大家如何创建与使用dll文件。 本文基于windows系统&#xff0c;使用的编译相关工具为visual studio 2019。 2、创建dll 2.1 创建dll工程 首先打开visual studio&#xff0c;然后选择创建新项目&#xff0c;在搜…...

一些Git Repo

文章目录 Fake-TcpWow Fishing Script模拟券商柜台 Fake-Tcp Fake-Tcp 自己写的一个伪装包测试。 尝试把UDP的包伪装成TCP包&#xff0c;再发送到Internet Wow Fishing Script 魔兽世界钓鱼脚本 自己写的魔兽世界钓鱼脚本&#xff0c;10.0初期钓鱼成功率90%以上。现在关服了…...

【Unity脚本开源】记录鼠标按下的位置和移动的距离来进行物体的旋转,并在鼠标释放后将物体恢复到初始旋转位置

♥️作者&#xff1a;白日参商 &#x1f935;‍♂️个人主页&#xff1a;白日参商主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识&#xff0c;和大家一起努力呀&#xff01;&#xff01;&#xff01; &#x1f388;&#x1f388;加油&#xff01; 加油&#xff01…...

金蝶软件实现导入Excel数据分录行信息到单据体分录行中

>>>适合KIS云专业版V16.0|KIS云旗舰版V7.0|K/3 WISE 14.0等版本<<< 金蝶软件中实现[导入Excel数据业务分录行]信息到[金蝶单据体分录]中,在采购订单|采购入库单|销售订单|销售出库单等类型单据中,以少量的必要字段在excel表格中按模板填列好,很方便快捷地从…...

C# 11 中的新增功能

本文内容 泛型属性泛型数学支持数值 IntPtr 和 UIntPtr字符串内插中的换行符 显示另外 11 个 C# 11 中增加了以下功能&#xff1a; 原始字符串字面量泛型数学支持泛型属性UTF-8 字符串字面量字符串内插表达式中的换行符列表模式文件本地类型必需的成员自动默认结构常量 str…...

Postman通用接口加密解决方案

前言&#xff1a; 很对小伙伴对于psotman接口加密不知道如何解决&#xff0c;这里给大家出了一个全网最详细的解决方案&#xff0c;希望能帮助到大家 问题 postman内置加密Api&#xff0c;但不支持RSA加解密码。如何用postman进入rsa加解密&#xff1f;postman中request对象…...

java,钉钉小程序免密登录

一、开发者后台统一登录 - 钉钉统一身份认证 登录钉钉开放平台 二、教程介绍 如何实现用户免登。免登是指用户进入应用后&#xff0c;无需输入钉钉用户名和密码&#xff0c;应用程序可自动获取当前用户身份&#xff0c;进而登录系统的流程。 三、准备工作 注册了钉钉管理员…...

基于docker部署的Selenium Grid分布式自动化测试

01、什么是Selenium Grid Selenium Grid是Selenium套件的一部分&#xff0c;它专门用于并行运行多个测试用例在不同的浏览器、操作系统和机器上。 Selenium Grid有两个版本——老版本Grid 1和新版本Grid 2。我们只对新版本做介绍&#xff0c;因为Selenium团队已经逐渐遗弃老版…...

目标和——力扣494

文章目录 题目描述解法:动态规划题目描述 解法:动态规划 nt findTargetSumWays(vector<int>& nums, int target){int sum...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验

系列回顾&#xff1a; 在上一篇中&#xff0c;我们成功地为应用集成了数据库&#xff0c;并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了&#xff01;但是&#xff0c;如果你仔细审视那些 API&#xff0c;会发现它们还很“粗糙”&#xff1a;有…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)

骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术&#xff0c;它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton)&#xff1a;由层级结构的骨头组成&#xff0c;类似于人体骨骼蒙皮 (Mesh Skinning)&#xff1a;将模型网格顶点绑定到骨骼上&#xff0c;使骨骼移动…...

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型&#xff08;LLM&#xff09;参数规模的增长&#xff0c;推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长&#xff0c;而KV缓存的内存消耗可能高达数十GB&#xff08;例如Llama2-7B处理100K token时需50GB内存&a…...

【JVM面试篇】高频八股汇总——类加载和类加载器

目录 1. 讲一下类加载过程&#xff1f; 2. Java创建对象的过程&#xff1f; 3. 对象的生命周期&#xff1f; 4. 类加载器有哪些&#xff1f; 5. 双亲委派模型的作用&#xff08;好处&#xff09;&#xff1f; 6. 讲一下类的加载和双亲委派原则&#xff1f; 7. 双亲委派模…...

c++第七天 继承与派生2

这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分&#xff1a;派生类构造函数与析构函数 当创建一个派生类对象时&#xff0c;基类成员是如何初始化的&#xff1f; 1.当派生类对象创建的时候&#xff0c;基类成员的初始化顺序 …...

go 里面的指针

指针 在 Go 中&#xff0c;指针&#xff08;pointer&#xff09;是一个变量的内存地址&#xff0c;就像 C 语言那样&#xff1a; a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10&#xff0c;通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...