【Redis实战】投票功能
1. 前言
现在就来实践一下如何使用 Redis 来解决实际问题,市面上很多网站都提供了投票功能,比如 Stack OverFlow 以及 Reddit 网站都提供了根据文章的发布时间以及投票数计算出一个评分,然后根据这个评分进行文章的展示顺序。本文就简单演示了如何使用 Redis 构建一个网站投票后端逻辑。
2. 数据结构设计
要想完成这个后端系统我们就需要思考如何设计 Redis 的存储内容及其结构:
- 文章信息(包含文章id、标题、内容、作者、投票数、发布时间):hash 结构
- 评分排行榜(成员是文章id、分数是评分):zset 结构
- 发布时间排行榜(成员是文章id、分数是发布时间):zset 结构
- 文章投票用户集合(成员是用户id):set 结构
3. 接口设计
想要设计一个投票网站,我们就必须限定一些数值和规则条件:
- 用户只能给在发布时间一周内的文章投票
- 每个用户不得重复给一个文章投票
3.1 对文章进行投票
想要对文章进行投票,我们前提是需要设定一个评分函数(投票数越高评分越高、发布时间越久评分越低)我们假设使用以下函数:rate = 100 * vote_num + publish_time
其中:
- rate:表示该文章的评分
- vote_num: 表示该文章的得票数
- publish_time: 表示发布时间的 unix 时间戳
详细步骤如下:
- 校验文章发布时间是否已经超过一周
- 校验该用户是否已经给该文章投过票
- 给评分加上 100 使用
ZINCRBY
命令重新放入 zset 中,使用HINCRBY
命令修改文章信息将投票数+1 - 将投票用户 id 使用
SADD
命令加入到该文章对应已投票用户集合当中
示例代码如下:
const ONE_WEEK_SECONDS = 7 * 86400
const ARTICLE_PREFIX = "article:"
const VOTED_USERS_PREFIX = "voted:"
const RATE_SCORE_KEY = "rate:"
const TIME_SCORE_KEY = "time:"
const USER_PREFIX = "user:"
const BASE_SCORE = 100 // 基准分// ArticleVote 给文章投票函数
func ArticleVote(articleId string, userId string, client *redis.Client, ctx context.Context) {// 1. 校验文章发布时间是否超过一周var articleKey = ARTICLE_PREFIX + articleIdresult, _ := client.HGet(ctx, articleKey, "publish_time").Result()publishTime, _ := strconv.Atoi(result)if int64(publishTime) < time.Now().Unix()-ONE_WEEK_SECONDS {panic("发布时间已经超过一周!")}// 2. 校验用户是否已经投过票了var votedKey = VOTED_USERS_PREFIX + articleKeyvar userKey = USER_PREFIX + userIdi, _ := client.SAdd(ctx, votedKey, userKey).Result()if i == 0 {// 已经投过票了panic("用户已经投过票!")}// 3. 重新计算文章评分client.ZIncrBy(ctx, RATE_SCORE_KEY, float64(BASE_SCORE), articleKey)// 4. 重置文章得票数client.HIncrBy(ctx, articleKey, "vote_num", int64(1))
}
💡 注意:
- 实际上我们应该用 redis 的事务保证修改操作的同步!但是由于还没有介绍 Lua 脚本之类的知识,所以暂不考虑!
- 我们常用 “:” 冒号分隔符分隔 key 中的多个标识符
3.2 发布文章
详细步骤如下:
- 构建一个 redis 当中的 hash 结构,使用
HMSET
命令保存到 redis 中,键格式为:“article:articleId” - 将发布的用户id保存到文章对应已投票用户集合当中(并设置一周的过期时间)
- 保存 发布时间-文章id 使用
ZADD
命令添加到有序集合中 - 保存 评分-文章id 使用
ZADD
命令添加到有序集合中
// PublishArticle 发布文章
func PublishArticle(articleId string, userId string, article article.Article, client *redis.Client, ctx context.Context) {// 1. 保存文章信息var articleKey = ARTICLE_PREFIX + articleIdvar publishTime = time.Now().Unix()article.PublishTime = publishTimearticle.VoteNum = 0client.HMSet(ctx, articleKey, article)// 2. 保存发布人到已发布用户集合中并设置过期时间var votedKey = VOTED_USERS_PREFIX + articleKeyvar voteUser = USER_PREFIX + userIdclient.SAdd(ctx, votedKey, voteUser)client.Expire(ctx, votedKey, ONE_WEEK_SECONDS*time.Second)// 3.设置初始评分到有序集合中client.ZAdd(ctx, RATE_SCORE_KEY, redis.Z{Member: articleKey,Score: float64(publishTime),})// 4. 设置初始发布时间到有序集合中client.ZAdd(ctx, TIME_SCORE_KEY, redis.Z{Member: articleKey,Score: float64(publishTime),})
}
3.3 获取文章
我们已经实现了给文章投票以及发布文章的功能,那么写下来就要考虑如何获取评分最高的前 n 个文章以及获取发布时间最新的前 n 个文章了,详细流程如下(以评分为例):
- 使用
zrevrange
命令按照 score 从高到低获取score:
有序集合中指定数量的成员 - 根据每个成员的文章 id 从
article:articleId
中使用HGETALL
命令获取详细文章数据 - 构建结果返回
// GetArticlesByCondition 根据条件获取特定页文章列表
func GetArticlesByCondition(pageNo int64, scoreCondition string, client *redis.Client, ctx context.Context) []article.Article {// 1. 计算起始和结束索引下标var start = (pageNo - 1) * ARTICLES_PER_PAGEvar end = start + ARTICLES_PER_PAGE - 1// 2. 使用ZREVRANGE命令按照score倒序获取数据// 2.1 先判断是否存在该keyresult, _ := client.Exists(ctx, scoreCondition).Result()if result == 0 {// 没有这个有序集合键panic("不存在该有序集合键!")}articleIds, _ := client.ZRevRange(ctx, scoreCondition, start, end).Result()// 3. 根据id获取文章具体内容// 4. 构建响应var articles = make([]article.Article, 0, len(articleIds))for _, articleId := range articleIds {articleMap, _ := client.HGetAll(ctx, articleId).Result()var article article.Articlearticle.Id = articleMap["id"]article.Title = articleMap["title"]article.Content = articleMap["content"]publishTime, _ := strconv.ParseInt(articleMap["publish_time"], 10, 64)article.PublishTime = publishTimevoteNum, _ := strconv.ParseInt(articleMap["vote_num"], 10, 64)article.VoteNum = voteNumarticles = append(articles, article)}return articles
}
3.4 给文章分组
3.4.1 添加或删除分组
我们有些时候希望网站能够提供一个分组展示的功能,比如"Java"分组、"Go"分组等等,在 redis 中就可以设计为set
集合类型(对应 key 为group:group_name
),我们就需要提供一个往分组中添加或者删除指定文章的功能:
- 构建文章对应 key
- 从
addGroups
中将文章添加到每个分组中 - 从
removeGroups
每个分组中删除文章
const GROUP_PREFIX = "group:" // 分组前缀// AddOrRemoveGroups 添加或删除文章到分组中
func AddOrRemoveGroups(articleId string, addGroups []string, removeGroups []string, client *redis.Client, ctx context.Context) {var articleKey = ARTICLE_PREFIX + articleIdfor _, group := range addGroups {// 添加到分组中client.SAdd(ctx, GROUP_PREFIX+group, articleKey)}for _, group := range removeGroups {// 从分组中删除client.SRem(ctx, GROUP_PREFIX+group, articleKey)}
}
3.4.2 获取分组文章
我们已经有了对应的分组比如group:test
分组成员为article:1
,现在我们希望能够对某个特定分组当中的文章按照指定 score 进行排序,即构建一个新的有序集合,我们可以借助ZINTERSTORE
命令,将rate:
有序集合或者time:
有序集合中的元素与group:test
当中的元素取交集(设定 aggregate 为 max 表示得分为较大值),除此以外我们还可以缓存过期时间提高效率
- 检查分组有序集合 key 是否存在,若不存在则使用
ZINTERSTORE
命令构建分组有序集合 - 设定过期时间为 60s
- 复用
GetArticlesByCondition
方法获取文章列表
const SCORE_GROUP_EXPIRATION = 60 // 分组有序集合过期时间// GetGroupArticlesByCondition 根据条件获取分组特定页文章列表
func GetGroupArticlesByCondition(pageNo int64, group string, scoreCondition string, client *redis.Client, ctx context.Context) []article.Article {// 2. 判断是否已经存在该分组下的有序集合var scoreGroupKey = scoreCondition + groupresult, _ := client.Exists(ctx, scoreGroupKey).Result()if result == 0 {// 创建分组评分集合client.ZInterStore(ctx, scoreGroupKey, &redis.ZStore{Keys: []string{GROUP_PREFIX + group, scoreCondition},Aggregate: "max",})// 设置过期时间client.Expire(ctx, scoreGroupKey, SCORE_GROUP_EXPIRATION*time.Second)}// 3. 返回响应return GetArticlesByCondition(pageNo, scoreGroupKey, client, ctx)
}
4. 总结
我们可以把上述功能中提到的 redis 命令总结如下:
- 对于
hash
结构- HMSET:批量向 hash 结构插入键值对
- HGETALL:获取 key 对应的 hash 结构全部键值对
- HINCRBY:向 key 对应的 hash 结构特定的键进行自增
- 对于
set
集合结构- SADD:向 set 结构插入成员
- SREM:从 set 结构中删除成员
- 对于
zset
有序集合结构- ZADD:向 zset 结构插入成员-分数
- ZREVRANGE:从 zset 结构中按照分数从大到小取出成员
- ZINCRBY:向 zset 结构特定成员分数自增
- ZINTERSTORE:将两个集合进行交集运算得到一个新的 zset 结构
- 通用命令
- EXPIRE:对某个 key 设置过期时间(单位为 ms )
- EXISTS:检查某个 key 是否存在
相关文章:
【Redis实战】投票功能
1. 前言 现在就来实践一下如何使用 Redis 来解决实际问题,市面上很多网站都提供了投票功能,比如 Stack OverFlow 以及 Reddit 网站都提供了根据文章的发布时间以及投票数计算出一个评分,然后根据这个评分进行文章的展示顺序。本文就简单演示…...
linux常用基础命令 最新1
常用命令 查看当前目录下个各个文件大小查看当前系统储存使用情况查看当前路径删除当前目录下所有包含".log"的文件linux开机启动jar更改自动配置文件后操作关闭自启动linux静默启动java服务查询端口被占用查看软件版本重启关机开机启动取别名清空当前行创建文件touc…...

UnityShader学习笔记——多种光源
——内容源自唐老狮的shader课程 目录 1.光源类型 2.判断光源类型 2.1.在哪判断 2.2.如何判断 3.光照衰减 3.1.基本概念 3.2.unity中的光照衰减 3.3.光源空间变换矩阵 4.点光源衰减计算 5.聚光灯衰减计算 5.1.聚光灯的cookie(灯光遮罩) 5.2.聚…...

深入浅出谈VR(虚拟现实、VR镜头)
1、VR是什么鬼? 近两年VR这次词火遍网上网下,到底什么是VR?VR是“Virtual Reality”,中文名字是虚拟现实,是指采用计算机技术为核心的现代高科技手段生成一种虚拟环境,用户借助特殊的输入/输出设备&#x…...

项目2 车牌检测
检测车牌 1. 基本思想2. 基础知识2.1 YOLOV5(参考鱼苗检测)2.1.1 模型 省略2.1.2 输入输出 省略2.1.3 损失函数 省略2.2 LPRNet2.2.1 模型2.2.2 输入输出2.2.3 损失函数3. 流程3.1 数据处理3.1.1 YOLOV5数据处理3.2.2 LPRNet数据处理3.2 训练3.2.1 YOLOV5训练 省略3.2.2 LPRN…...

Linux: 网络基础
1.协议 为什么要有协议:减少通信成本。所有的网络问题,本质是传输距离变长了。 什么是协议:用计算机语言表达的约定。 2.分层 软件设计方面的优势—低耦合。 一般我们的分层依据:功能比较集中,耦合度比较高的模块层…...
【实战篇】巧用 DeepSeek,让 Excel 数据处理更高效
一、为何选择用 DeepSeek 处理 Excel 在日常工作与生活里,Excel 是我们频繁使用的工具。不管是统计公司销售数据、分析学生成绩,还是梳理个人财务状况,Excel 凭借其强大的功能,如数据排序、筛选和简单公式计算,为我们提供了诸多便利。但当面对复杂的数据处理任务,比如从…...

Flink CDC YAML:面向数据集成的 API 设计
摘要:本文整理自阿里云智能集团 、Flink PMC Member & Committer 徐榜江(雪尽)老师在 Flink Forward Asia 2024 数据集成(一)专场中的分享。主要分为以下四个方面: Flink CDC YAML API Transform A…...
RabbitMQ技术深度解析:打造高效消息传递系统
引言 在当前的分布式系统架构中,消息队列作为一种高效的消息传递机制,扮演着越来越重要的角色。RabbitMQ,作为广泛使用的开源消息代理,以其高可用性、扩展性和灵活性赢得了众多开发者的青睐。本文将深入探讨RabbitMQ的核心概念、…...
DeepSeek与人工智能的结合:探索搜索技术的未来
云边有个稻草人-CSDN博客 目录 引言 一、DeepSeek的技术背景 1.1 传统搜索引擎的局限性 1.2 深度学习在搜索中的优势 二、DeepSeek与人工智能的结合 2.1 自然语言处理(NLP) 示例代码:基于BERT的语义搜索 2.2 多模态搜索 示例代码&…...

TAPEX:通过神经SQL执行器学习的表格预训练
摘要 近年来,语言模型预训练的进展通过利用大规模非结构化文本数据取得了巨大成功。然而,由于缺乏大规模高质量的表格数据,在结构化表格数据上应用预训练仍然是一个挑战。本文提出了TAPEX,通过在一个合成语料库上学习神经SQL执行…...

Qt:Qt基础介绍
目录 Qt背景介绍 什么是Qt Qt的发展史 Qt支持的平台 Qt版本 Qt的优点 Qt的应用场景 Qt的成功案例 Qt的发展前景及就业分析 Qt背景介绍 什么是Qt Qt是⼀个跨平台的C图形用户界面应用程序框架。它为应用程序开发者提供了建立艺术级图形界面所需的所有功能。它是完全面向…...
加速度计信号处理
【使用 DSP 滤波器加速速度和位移】使用信号处理算法过滤加速度数据并将其转换为速度和位移研究(Matlab代码实现)_加速度计滤波器-CSDN博客 https://wenku.baidu.com/view/622d38b90f22590102020740be1e650e52eacff9.html?_wkts_1738906719916&bdQ…...

基于SpringBoot养老院平台系统功能实现六
一、前言介绍: 1.1 项目摘要 随着全球人口老龄化的不断加剧,养老服务需求日益增长。特别是在中国,随着经济的快速发展和人民生活水平的提高,老年人口数量不断增加,对养老服务的质量和效率提出了更高的要求。传统的养…...

Conmi的正确答案——Rider中添加icon作为exe的图标
C#版本:.net 8.0 Rider版本:#RD-243.22562.250(非商业使用版) 1、添加图标到解决方案下: 2、打开“App.xaml”配置文件,添加配置: <Applicationx:Class"ComTransmit.App"xmlns&q…...
机试题——DNS本地缓存
题目描述 正在开发一个DNS本地缓存系统。在互联网中,DNS(Domain Name System)用于将域名(例如www.example.com)解析为IP地址,以便将请求发送到正确的服务器上。通常情况下,DNS请求会发送到互联…...
Day38【AI思考】-彻底打通线性数据结构间的血脉联系
文章目录 **彻底打通线性数据结构间的血脉联系****数据结构家族谱系图****一、线性表(老祖宗的规矩)****核心特征** **二、嫡系血脉解析**1. **数组(规矩森严的长子)**2. **链表(灵活变通的次子)** **三、庶…...

【LeetCode】152、乘积最大子数组
【LeetCode】152、乘积最大子数组 文章目录 一、dp1.1 dp1.2 简化代码 二、多语言解法 一、dp 1.1 dp 从前向后遍历, 当遍历到 nums[i] 时, 有如下三种情况 能得到最大值: 只使用 nums[i], 例如 [0.1, 0.3, 0.2, 100] 则 [100] 是最大值使用 max(nums[0…i-1]) * nums[i], 例…...

[MRCTF2020]Ez_bypass1(md5绕过)
[MRCTF2020]Ez_bypass1(md5绕过) 这道题就是要绕过md5强类型比较,但是本身又不相等: md5无法处理数组,如果传入的是数组进行md5加密,会直接放回NULL,两个NuLL相比较会等于true; 所以?id[]1&gg…...

MySQL 缓存机制与架构解析
目录 一、MySQL缓存机制概述 二、MySQL整体架构 三、SQL查询执行全流程 四、MySQL 8.0为何移除查询缓存? 五、MySQL 8.0前的查询缓存配置 六、替代方案:应用层缓存与优化建议 总结 一、MySQL缓存机制概述 MySQL的缓存机制旨在提升数据访问效率&am…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
k8s从入门到放弃之HPA控制器
k8s从入门到放弃之HPA控制器 Kubernetes中的Horizontal Pod Autoscaler (HPA)控制器是一种用于自动扩展部署、副本集或复制控制器中Pod数量的机制。它可以根据观察到的CPU利用率(或其他自定义指标)来调整这些对象的规模,从而帮助应用程序在负…...

密码学基础——SM4算法
博客主页:christine-rr-CSDN博客 专栏主页:密码学 📌 【今日更新】📌 对称密码算法——SM4 目录 一、国密SM系列算法概述 二、SM4算法 2.1算法背景 2.2算法特点 2.3 基本部件 2.3.1 S盒 2.3.2 非线性变换 编辑…...

GraphRAG优化新思路-开源的ROGRAG框架
目前的如微软开源的GraphRAG的工作流程都较为复杂,难以孤立地评估各个组件的贡献,传统的检索方法在处理复杂推理任务时可能不够有效,特别是在需要理解实体间关系或多跳知识的情况下。先说结论,看完后感觉这个框架性能上不会比Grap…...

未授权访问事件频发,我们应当如何应对?
在当下,数据已成为企业和组织的核心资产,是推动业务发展、决策制定以及创新的关键驱动力。然而,未授权访问这一隐匿的安全威胁,正如同高悬的达摩克利斯之剑,时刻威胁着数据的安全,一旦触发,便可…...