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

【Redis实战】投票功能

1. 前言

现在就来实践一下如何使用 Redis 来解决实际问题,市面上很多网站都提供了投票功能,比如 Stack OverFlow 以及 Reddit 网站都提供了根据文章的发布时间以及投票数计算出一个评分,然后根据这个评分进行文章的展示顺序。本文就简单演示了如何使用 Redis 构建一个网站投票后端逻辑。

2. 数据结构设计

要想完成这个后端系统我们就需要思考如何设计 Redis 的存储内容及其结构:

  • 文章信息(包含文章id、标题、内容、作者、投票数、发布时间):hash 结构
  • 评分排行榜(成员是文章id、分数是评分):zset 结构
  • 发布时间排行榜(成员是文章id、分数是发布时间):zset 结构
  • 文章投票用户集合(成员是用户id):set 结构

3. 接口设计

想要设计一个投票网站,我们就必须限定一些数值和规则条件:

  1. 用户只能给在发布时间一周内的文章投票
  2. 每个用户不得重复给一个文章投票

3.1 对文章进行投票

想要对文章进行投票,我们前提是需要设定一个评分函数(投票数越高评分越高、发布时间越久评分越低)我们假设使用以下函数:rate = 100 * vote_num + publish_time

其中:

  • rate:表示该文章的评分
  • vote_num: 表示该文章的得票数
  • publish_time: 表示发布时间的 unix 时间戳

详细步骤如下:

  1. 校验文章发布时间是否已经超过一周
  2. 校验该用户是否已经给该文章投过票
  3. 给评分加上 100 使用ZINCRBY命令重新放入 zset 中,使用HINCRBY命令修改文章信息将投票数+1
  4. 将投票用户 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))
}

💡 注意:

  1. 实际上我们应该用 redis 的事务保证修改操作的同步!但是由于还没有介绍 Lua 脚本之类的知识,所以暂不考虑!
  2. 我们常用 “:” 冒号分隔符分隔 key 中的多个标识符

3.2 发布文章

详细步骤如下:

  1. 构建一个 redis 当中的 hash 结构,使用HMSET命令保存到 redis 中,键格式为:“article:articleId”
  2. 将发布的用户id保存到文章对应已投票用户集合当中(并设置一周的过期时间)
  3. 保存 发布时间-文章id 使用ZADD命令添加到有序集合中
  4. 保存 评分-文章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 个文章了,详细流程如下(以评分为例):

  1. 使用zrevrange命令按照 score 从高到低获取score:有序集合中指定数量的成员
  2. 根据每个成员的文章 id 从article:articleId中使用HGETALL命令获取详细文章数据
  3. 构建结果返回
// 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),我们就需要提供一个往分组中添加或者删除指定文章的功能:

  1. 构建文章对应 key
  2. addGroups中将文章添加到每个分组中
  3. 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 表示得分为较大值),除此以外我们还可以缓存过期时间提高效率

  1. 检查分组有序集合 key 是否存在,若不存在则使用ZINTERSTORE命令构建分组有序集合
  2. 设定过期时间为 60s
  3. 复用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 命令总结如下:

  1. 对于hash结构
    • HMSET:批量向 hash 结构插入键值对
    • HGETALL:获取 key 对应的 hash 结构全部键值对
    • HINCRBY:向 key 对应的 hash 结构特定的键进行自增
  2. 对于set集合结构
    • SADD:向 set 结构插入成员
    • SREM:从 set 结构中删除成员
  3. 对于zset有序集合结构
    • ZADD:向 zset 结构插入成员-分数
    • ZREVRANGE:从 zset 结构中按照分数从大到小取出成员
    • ZINCRBY:向 zset 结构特定成员分数自增
    • ZINTERSTORE:将两个集合进行交集运算得到一个新的 zset 结构
  4. 通用命令
    • EXPIRE:对某个 key 设置过期时间(单位为 ms )
    • EXISTS:检查某个 key 是否存在

相关文章:

【Redis实战】投票功能

1. 前言 现在就来实践一下如何使用 Redis 来解决实际问题&#xff0c;市面上很多网站都提供了投票功能&#xff0c;比如 Stack OverFlow 以及 Reddit 网站都提供了根据文章的发布时间以及投票数计算出一个评分&#xff0c;然后根据这个评分进行文章的展示顺序。本文就简单演示…...

【开源AI】AI一页一页读PDF

【开源AI】AI一页一页读PDF 可以在这里看 : 让AI 处理 PDF 文件,提取其中的知识点,并生成总结。 只是无法修改,后续若有更新在csdn这里。 【OpenAI】 API 更新: JSON 结构化输出约束机制( JSON Schema) 的一次实战。知识库的JSON Schema形式 每一页都要总结,总结的知识…...

Selenium记录RPA初阶 - 基本输入元件

防止自己遗忘&#xff0c;故作此为记录。 爬取网页基本元件并修改后爬取。 包含元件&#xff1a; elements: dict[str, str] {"username": None,"password": None,"email": None,"website": None,"date": None,"ti…...

第三个Qt开发实例:利用之前已经开发好的LED驱动在Qt生成的界面中控制LED2的亮和灭

前言 上一篇博文 https://blog.csdn.net/wenhao_ir/article/details/145459006 中&#xff0c;我们是直接利用GPIO子系统控制了LED2的亮和灭&#xff0c;这篇博文中我们利用之前写好的LED驱动程序在Qt的生成的界面中控制LED2的亮和灭。 之前已经在下面两篇博文中实现了LED驱动…...

Android studio 创建aar包给Unity使用

1、aar 是什么&#xff1f; 和 Jar有什么区别 aar 和 jar包 都是压缩包&#xff0c;可以使用压缩软件打开 jar包 用于封装 Java 类及其相关资源 aar 文件是专门为 Android 平台设计的 &#xff0c;可以包含Android的专有内容&#xff0c;比如AndroidManifest.xml 文件 &#…...

BurpSuite抓包与HTTP基础

文章目录 前言一、BurpSuite1.BurpSuite简介2.BurpSuite安装教程(1)BurpSuite安装与激活(2)安装 https 证书 3.BurpSuite使用4.BurpSuite资料 二、图解HTTP1.HTTP基础知识2.HTTP客户端请求消息3.HTTP服务端响应消息4.HTTP部分请求方法理解5.HTTPS与HTTP 总结 前言 在网络安全和…...

把DeepSeek接入Word软件,给工作提质增效!

前几天给大家分享了 DeepSeek 的资源包&#xff0c;可能很多人并没有本地部署 DeepSeek 的需求&#xff0c;只想使用它来提高一下工作效率。那今天来分享一下怎么直接在 Word 软件调用 DeepSeek&#xff0c;避免在 Word 软件和网页版 DeepSeek 里来回切换。 ## 前置条件 1、有…...

Linux进阶——web服务器

一、相关名词解释及概念&#xff1a; www&#xff1a;(world wide web)全球信息广播&#xff0c;通常来说的上网就是使用www来查询用户所需的信息。使用http超文本传输协议。 过程&#xff1a;web浏览器向web服务&#xff08;Apache&#xff0c;Microsoft&#xff0c;nginx&…...

QT笔记——多语言翻译

文章目录 1、概要2、多语言切换2.1、结果展示2.2、创建项目2.2、绘制UI2.2、生成“.st”文件2.4、生成“.qm”文件2.5、工程demo 1、概要 借助QT自带的翻译功能&#xff0c;实现实际应用用进行 “多语言切换” 2、多语言切换 2.1、结果展示 多语言切换 2.2、创建项目 1、文件…...

oracle 基础语法复习记录

Oracle SQL基础 因工作需要sql能力&#xff0c;需要重新把sql这块知识重新盘活&#xff0c;特此记录学习过程。 希望有新的发现。加油&#xff01;20250205 学习范围 学习SQL基础语法 掌握SELECT、INSERT、UPDATE、DELETE等基本操作。 熟悉WHERE、GROUP BY、ORDER BY、HAVIN…...

php 接入扣子的 token获取

本身逻辑只是个api&#xff0c;但是官方不提供php的sdk 扎心了老铁&#xff0c;这下php 狗都不用了&#xff0c;主要麻烦的是如何获取access_token&#xff0c;代码如下 protected function get_jwt(): string{$header [alg > RS256,typ > JWT,kid > $this->kid];…...

网络工程师 (22)网络协议

前言 网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合&#xff0c;它规定了通信时信息必须采用的格式和这些格式的意义。 一、基本要素 语法&#xff1a;规定信息格式&#xff0c;包括数据及控制信息的格式、编码及信号电平等。这是协议的基础&#xff0c;确…...

【银河麒麟高级服务器操作系统】系统日志Call trace现象分析及处理全流程

了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer.kylinos.cn 文档中心&#xff1a;https://document.kylinos.cn 服务器环境以及配置 系统环境 物理机/虚拟机/云…...

Milvus 存储设计揭秘:从数据写入到 Segment 管理的全链路解析

作为一款云原生向量数据库&#xff0c;Milvus 的高效查询性能有赖于其独特的存储架构设计。然而&#xff0c;在实际使用过程中&#xff0c;许多社区用户常常会遇到以下问题&#xff1a; 为什么频繁调用 flush 后&#xff0c;查询速度会变慢&#xff1f; 数据删除后&#xff0c;…...

Redis双写一致性(数据库与redis数据一致性)

一 什么是双写一致性&#xff1f; 当修改了数据库&#xff08;MySQL&#xff09;中的数据&#xff0c;也要同时更新缓存&#xff08;redis&#xff09;中的数据&#xff0c;缓存中的数据要和数据库中的数据保持一致 双写一致性&#xff0c;根据业务对时间上的要求&#xff0c;…...

14.PPT:中国注册税务师协会宣传【26】

目录 NO12 NO3/4/5​ NO678​ 【文本框水平/垂直居中】【文本框内容水平/垂直居中】 NO12 坑&#xff1a;注意❗Word文档的PPt素材.docx的标题大纲是混乱的&#xff0c;虽然他设置了&#xff0c;所以我们需要重新设置 设计→主题视图→幻灯片母版→删除版式插入logo NO3/4…...

搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程

参考文章&#xff1a; 安装protoc、protoc-gen-go、protoc-gen-go-grpc-CSDN博客 一、简单介绍 本文开发环境&#xff0c;均为 windows 环境&#xff0c;mac 环境其实也类似 ~ ① 编译proto文件&#xff0c;相关插件 简单介绍&#xff1a; protoc 是编译器&#xff0c;用于将…...

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_write_stderr函数

定义 在ngx_log.h 中 static ngx_inline void ngx_write_stderr(char *text) {(void) ngx_write_fd(ngx_stderr, text, ngx_strlen(text)); } 在开发软件时&#xff0c;尤其是像 Nginx 这样复杂的服务器软件&#xff0c;开发者需要一种方法来输出调试信息或者错误信息。这段代码…...

使用requestAnimationFrame减少浏览器重绘

文章目录 介绍使用使用rAF前使用rAF后 介绍 在屏幕中&#xff0c;浏览器通常都以60FPS&#xff08;1/60 s&#xff09;每帧更新屏幕&#xff0c;但是当前端绑定了一些高频事件&#xff0c;如鼠标移动&#xff0c;屏幕滚动、触摸滑动等时&#xff0c;在一帧的周期内&#xff0c;…...

autMan奥特曼机器人-对接deepseek教程

一、安装插件ChatGPT 符合openai api协议的大模型均可使用此插件&#xff0c;包括chatgpt-4/chatgpt-3.5-turbo&#xff0c;可自定义服务地址和模型&#xff0c;指令&#xff1a;gpt&#xff0c;要求Python3.7以上&#xff0c;使用官方库https://github.com/openai/openai-pyt…...

一个精简完整的全栈电商平台采用现代化技术栈和安全架构

以下是一个精简但完整的全栈电商平台实现,采用现代化技术栈和安全架构: 架构设计 前端:SvelteKit + TypeScript (SSR/SSG) 后端:Go 1.21 + Fiber框架 数据库:MySQL 8.0 + Redis 7.0 安全:Cloudflare WAF + JWT+HTTPOnly+Secure Cookie 部署:Docker Swarm + Traefik (自…...

数据分析:企业数字化转型的金钥匙

引言&#xff1a;数字化浪潮下的数据金矿 在数字化浪潮席卷全球的背景下&#xff0c;有研究表明&#xff0c;只有不到30%的企业能够充分利用手中掌握的数据&#xff0c;这是否让人深思&#xff1f;数据已然成为企业最为宝贵的资产之一。然而&#xff0c;企业是否真正准备好从数…...

MySQL中datetime类型23:59:59变成下一天的00:00:00

1. 现象 公司项目中有一个统计的业务需求&#xff0c;需要记录数据的开始和结束时间点&#xff0c;对应的是MySQL表中的datetime类型。结束时间是当天的23:59:59&#xff0c;但是java程序中是对的时间&#xff0c;但是Mybatis-plus入库了之后查看数据库却变成第二天的00:00:00&…...

玩转Docker | 使用Docker部署httpd服务

玩转Docker | 使用Docker部署httpd服务 前言一、准备工作环境确认检查操作系统准备网站目录和配置文件二、拉取httpd镜像三、运行httpd容器运行容器命令检查容器状态四、验证httpd服务浏览器访问测试错误排查五、容器管理与维护查看容器状态停止和启动容器更新网站内容和配置六…...

Vue 双向数据绑定的原理

Vue 的双向数据绑定是其核心特性之一&#xff0c;它可以让视图与数据保持同步&#xff0c;简化了开发者在 DOM 操作上的工作。Vue 的双向数据绑定通过 响应式系统 和 DOM 事件监听 来实现&#xff0c;当数据发生变化时&#xff0c;视图会自动更新&#xff1b;当视图中的元素&am…...

Spring Web MVC项目的创建及使用

一、什么是Spring Web MVC&#xff1f; Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架&#xff0c;从⼀开始就包含在 Spring 框架中&#xff0c;通常被称为Spring MVC。 1.1 MVC的定义 MVC 是 Model View Controller 的缩写&#xff0c;它是软件工程中的一种软件架构…...

32.日常算法

1.最大子数组和 题目来源 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。子数组是数组中的一个连续部分。 示例 1&#xff1a; 输入&#xff1a;nums [-2,1,-3,4,-1,2,1,…...

MySQL的底层原理与架构

前言 了解MySQL的架构和原理对于很多的后续很多的操作会有很大的帮助与理解。并且很多知识都与底层架构相关联。 了解MySQL架构 通过上面的架构图可以得知&#xff0c;Server层中主要由 连接器、查询缓存、解析器/分析器、优化器、执行器 几部分组成的&#xff0c;下面将主要…...

python Excel 表读取合并单元格以及清除空格符

读取合并单元格并保留合并信息 读取合并单元格并保留合并信息清除各单元格的空格和换行符&#xff0c;并去除列名中的空格和换行符 读取合并单元格并保留合并信息 当我们只是使用 pandas 的 read_excel 方法读取 Excel 文件时&#xff0c;我们可能会遇到一个很棘手的问题&…...

Node.js 实现简单爬虫

介绍 爬虫是一种按照一定的规则&#xff0c;自动地抓取万维网信息的程序或者脚本。 本文将使用 Nodejs 编写一个简单的爬虫脚本&#xff0c;爬取一个美食网站&#xff0c;获取菜品的标题和图片链接&#xff0c;并以表格的形式输出。 准备工作 1、初始化项目 首先&#xff0…...