【Redis实战】投票功能

news2025/2/8 16:52:53

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 + articleId
	result, _ := 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 + articleKey
	var userKey = USER_PREFIX + userId
	i, _ := 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 + articleId
	var publishTime = time.Now().Unix()
	article.PublishTime = publishTime
	article.VoteNum = 0
	client.HMSet(ctx, articleKey, article)
	// 2. 保存发布人到已发布用户集合中并设置过期时间
	var votedKey = VOTED_USERS_PREFIX + articleKey
	var voteUser = USER_PREFIX + userId
	client.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_PAGE
	var end = start + ARTICLES_PER_PAGE - 1
	// 2. 使用ZREVRANGE命令按照score倒序获取数据
	// 2.1 先判断是否存在该key
	result, _ := 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.Article
		article.Id = articleMap["id"]
		article.Title = articleMap["title"]
		article.Content = articleMap["content"]
		publishTime, _ := strconv.ParseInt(articleMap["publish_time"], 10, 64)
		article.PublishTime = publishTime
		voteNum, _ := strconv.ParseInt(articleMap["vote_num"], 10, 64)
		article.VoteNum = voteNum
		articles = 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 + articleId
	for _, 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 + group
	result, _ := 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 是否存在

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

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

相关文章

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…

网络工程师 (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;用于将…

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…

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

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

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;它是软件工程中的一种软件架构…

MySQL的底层原理与架构

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

Node.js 实现简单爬虫

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

2024.1版android studio创建Java语言项目+上传gitee

1.在gitee上创建仓库 Gitee 创建仓库并邀请成员指南_gitee创建仓库邀请成员-CSDN博客 见1 2.新建android studio项目 3.在Android studio配置gitee Android Studio提交代码到gitee仓库_android log in to gitee-CSDN博客 其中的一二步 p.s.添加gitee账户选择password时&a…