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

【Redis实战】Chapter01-投票后端

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.kler.cn/a/532892.html

相关文章:

  • 33.Word:国家中长期人才发展规划纲要【33】
  • DeepSeek最新图像模型Janus-Pro论文阅读
  • 使用Express.js和SQLite3构建简单TODO应用的后端API
  • 排序算法--快速排序
  • 4 前置技术(下):git使用
  • 如何安全地管理Spring Boot项目中的敏感配置信息
  • 『 C++ 』中理解回调类型在 C++ 中的使用方式。
  • Android学习20 -- 手搓App2(Gradle)
  • leetcode 1482. 制作 m 束花所需的最少天数
  • git error: invalid path
  • Redis - String相关命令
  • UE编辑器工具
  • 【自学笔记】Git的重点知识点-持续更新
  • LeetCode:392.判断子序列
  • 接口游标分页
  • 本系统旨在为用户提供一个灵活且可扩展的信息安全管理解决方案,通过插件化的开发模式,使得信息安全的维护更加高效、便捷。
  • 云原生详解:构建未来应用的架构革命
  • 996引擎-怪物:Lua 刷怪+清怪+自动拾取
  • 2025_2_4 C语言中关于free函数及悬空指针,链表的一级指针和二级指指针
  • 【Block总结】CoT,上下文Transformer注意力|即插即用
  • IIC重难点-2
  • 【JavaScript】《JavaScript高级程序设计 (第4版) 》笔记-Chapter2-HTML 中的 JavaScript
  • mysql 学习7 DCL语句,用来管理数据库用户,控制数据库的访问权限
  • k8s二进制集群之各节点部署
  • 【华为OD-E卷 - 跳格子2 100分(python、java、c++、js、c)】
  • Git 的安装与基本配置