P15
P15
P15
若是要实现评分实时随时间递减,且支持按评分排序,那么工做量很大并且不精确。能够想到只有时间戳会随时间实时变化,若是咱们把发布文章的时间戳看成初始评分,那么后发布的文章初始评分必定会更高,从另外一个层面上实现了评分随时间递减。按照每一个有趣文章天天 200 张支持票计算,平均到一天(86400 秒)中,每张票能够将分提升 432 分。git
为了按照评分和时间排序获取文章,须要文章 id 及相应信息存在两个有序集合中,分别为:postTime 和 score 。github
为了防止统一用户对统一文章屡次投票,须要记录每篇文章投票的用户id,存储在集合中,为:votedUser:{articleId} 。redis
同时规定一篇文章发布期满一周后不能再进行投票,评分将被固定下来,同时记录文章已经投票的用户名单集合也会被删除。数组
// redis key type RedisKey string const ( // 发布时间 有序集合 POST_TIME RedisKey = "postTime" // 文章评分 有序集合 SCORE RedisKey = "score" // 文章投票用户集合前缀 VOTED_USER_PREFIX RedisKey = "votedUser:" // 发布文章数 字符串 ARTICLE_COUNT RedisKey = "articleCount" // 发布文章哈希表前缀 ARTICLE_PREFIX RedisKey = "article:" // 分组前缀 GROUP_PREFIX RedisKey = "group:" ) const ONE_WEEK_SECONDS = int64(7 * 24 * 60 * 60) const UPVOTE_SCORE = 432 // 用户 userId 给文章 articleId 投同意票(没有事务控制,第 4 章会介绍 Redis 事务) func UpvoteArticle(conn redis.Conn, userId int, articleId int) { // 计算当前时间能投票的文章的最先发布时间 earliestPostTime := time.Now().Unix() - ONE_WEEK_SECONDS // 获取 当前文章 的发布时间 postTime, err := redis.Int64(conn.Do("ZSCORE", POST_TIME, articleId)) // 获取错误 或 文章 articleId 的投票截止时间已过,则返回 if err != nil || postTime < earliestPostTime { return } // 当前文章能够投票,则进行投票操做 votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId)) addedNum, err := redis.Int(conn.Do("SADD", votedUserKey, userId)) // 添加错误 或 当前已投过票,则返回 if err != nil || addedNum == 0 { return } // 用户已成功添加到当前文章的投票集合中,则增长 当前文章 得分 _, err = conn.Do("ZINCRBY", SCORE, UPVOTE_SCORE, articleId) // 自增错误,则返回 if err != nil { return } // 增长 当前文章 支持票数 articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId)) _, err = conn.Do("HINCRBY", articleKey, 1) // 自增错误,则返回 if err != nil { return } }
P17
可使用 INCR
命令为每一个文章生成一个自增惟一 id 。缓存
将发布者的 userId 记录到该文章的投票用户集合中(即发布者默认为本身投支持票),同时设置过时时间为一周。服务器
存储文章相关信息,并将初始评分和发布时间记录下来。app
// 发布文章(没有事务控制,第 4 章会介绍 Redis 事务) func PostArticle(conn redis.Conn, userId int, title string, link string) { // 获取当前文章自增 id articleId, err := redis.Int(conn.Do("INCR", ARTICLE_COUNT)) if err != nil { return } // 将做者加入到投票用户集合中 votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId)) _, err = conn.Do("SADD", votedUserKey, userId) if err != nil { return } // 设置 投票用户集合 过时时间为一周 _, err = conn.Do("EXPIRE", votedUserKey, ONE_WEEK_SECONDS) if err != nil { return } postTime := time.Now().Unix() articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId)) // 设置文章相关信息 _, err = conn.Do("HMSET", articleKey, "title", title, "link", link, "userId", userId, "postTime", postTime, "upvoteNum", 1, ) if err != nil { return } // 设置 发布时间 _, err = conn.Do("ZADD", POST_TIME, postTime, articleId) if err != nil { return } // 设置 文章评分 score := postTime + UPVOTE_SCORE _, err = conn.Do("ZADD", SCORE, score, articleId) if err != nil { return } }
P18
分页获取支持四种排序,获取错误时返回空数组。ide
注意:ZRANGE
和 ZREVRANGE
的范围起止都是闭区间。函数
type ArticleOrder int const ( TIME_ASC ArticleOrder = iota TIME_DESC SCORE_ASC SCORE_DESC ) // 根据 ArticleOrder 获取相应的 命令 和 RedisKey func getCommandAndRedisKey(articleOrder ArticleOrder) (string, RedisKey) { switch articleOrder { case TIME_ASC: return "ZRANGE", POST_TIME case TIME_DESC: return "ZREVRANGE", POST_TIME case SCORE_ASC: return "ZRANGE", SCORE case SCORE_DESC: return "ZREVRANGE", SCORE default: return "", "" } } // 执行分页获取文章逻辑(忽略部分简单的参数校验等逻辑) func doListArticles(conn redis.Conn, page int, pageSize int, command string, redisKey RedisKey) []map[string]string { var articles []map[string]string // ArticleOrder 不对,返回空列表 if command == "" || redisKey == ""{ return nil } // 获取 起止下标(都是闭区间) start := (page - 1) * pageSize end := start + pageSize - 1 // 获取 文章id 列表 ids, err := redis.Ints(conn.Do(command, redisKey, start, end)) if err != nil { return articles } // 获取每篇文章信息 for _, id := range ids { articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(id)) article, err := redis.StringMap(conn.Do("HGETALL", articleKey)) if err == nil { articles = append(articles, article) } } return articles } // 分页获取文章 func ListArticles(conn redis.Conn, page int, pageSize int, articleOrder ArticleOrder) []map[string]string { // 获取 ArticleOrder 对应的 命令 和 RedisKey command, redisKey := getCommandAndRedisKey(articleOrder) // 执行分页获取文章逻辑,并返回结果 return doListArticles(conn, page, pageSize, command, redisKey) }
P19
支持将文章加入到分组集合,也支持将文章从分组集合中删除。post
// 设置分组 func AddToGroup(conn redis.Conn, groupId int, articleIds ...int) { groupKey := GROUP_PREFIX + RedisKey(strconv.Itoa(groupId)) args := make([]interface{}, 1 + len(articleIds)) args[0] = groupKey // []int 转换成 []interface{} for i, articleId := range articleIds { args[i + 1] = articleId } // 不支持 []int 直接转 []interface{} // 也不支持 groupKey, articleIds... 这样传参(这样匹配的参数是 interface{}, ...interface{}) _, _ = conn.Do("SADD", args...) } // 取消分组 func RemoveFromGroup(conn redis.Conn, groupId int, articleIds ...int) { groupKey := GROUP_PREFIX + RedisKey(strconv.Itoa(groupId)) args := make([]interface{}, 1 + len(articleIds)) args[0] = groupKey // []int 转换成 []interface{} for i, articleId := range articleIds { args[i + 1] = articleId } // 不支持 []int 直接转 []interface{} // 也不支持 groupKey, articleIds... 这样传参(这样匹配的参数是 interface{}, ...interface{}) _, _ = conn.Do("SREM", args...) }
P20
分组信息和排序信息在不一样的(有序)集合中,因此须要取两个(有序)集合的交集,再进行分页获取。
取交集比较耗时,因此缓存 60s,不实时生成。
// 缓存过时时间 60s const EXPIRE_SECONDS = 60 // 分页获取某分组下的文章(忽略简单的参数校验等逻辑;过时设置没有在事务里) func ListArticlesFromGroup(conn redis.Conn, groupId int, page int, pageSize int, articleOrder ArticleOrder) []map[string]string { // 获取 ArticleOrder 对应的 命令 和 RedisKey command, redisKey := getCommandAndRedisKey(articleOrder) // ArticleOrder 不对,返回空列表,防止多作取交集操做 if command == "" || redisKey == ""{ return nil } groupKey := GROUP_PREFIX + RedisKey(strconv.Itoa(groupId)) targetRedisKey := redisKey + RedisKey("-inter-") + groupKey exists, err := redis.Int(conn.Do("EXISTS", targetRedisKey)) // 交集不存在或已过时,则取交集 if err == nil || exists != 1 { _, err := conn.Do("ZINTERSTORE", targetRedisKey, 2, redisKey, groupKey) if err != nil { return nil } } // 设置过时时间(过时设置失败,不影响查询) _, _ = conn.Do("EXPIRE", targetRedisKey, EXPIRE_SECONDS) // 执行分页获取文章逻辑,并返回结果 return doListArticles(conn, page, pageSize, command, targetRedisKey) }
P21
增长投反对票功能,并支持支持票和反对票互转。
// redis key type RedisKey string const ( // 发布时间 有序集合 POST_TIME RedisKey = "postTime" // 文章评分 有序集合 SCORE RedisKey = "score" // 文章投票用户集合前缀 VOTED_USER_PREFIX RedisKey = "votedUser:" // 发布文章数 字符串 ARTICLE_COUNT RedisKey = "articleCount" // 发布文章哈希表前缀 ARTICLE_PREFIX RedisKey = "article:" // 分组前缀 GROUP_PREFIX RedisKey = "group:" ) type VoteType string const ( // 未投票 NONVOTE VoteType = "" // 投支持票 UPVOTE VoteType = "1" // 投反对票 DOWNVOTE VoteType = "2" ) const ONE_WEEK_SECONDS = int64(7 * 24 * 60 * 60) const UPVOTE_SCORE = 432 // 根据 原有投票类型 和 新投票类型,获取 分数、支持票数、反对票数 的增量(暂未处理“枚举”不对的状况,直接全返回 0) func getDelta(oldVoteType VoteType, newVoteType VoteType) (scoreDelta, upvoteNumDelta, downvoteNumDelta int) { // 类型不变,相关数值不用改变 if oldVoteType == newVoteType { return 0, 0, 0 } switch oldVoteType { case NONVOTE: if newVoteType == UPVOTE { return UPVOTE_SCORE, 1, 0 } if newVoteType == DOWNVOTE { return -UPVOTE_SCORE, 0, 1 } case UPVOTE: if newVoteType == NONVOTE { return -UPVOTE_SCORE, -1, 0 } if newVoteType == DOWNVOTE { return -(UPVOTE_SCORE << 1), -1, 1 } case DOWNVOTE: if newVoteType == NONVOTE { return UPVOTE_SCORE, 0, -1 } if newVoteType == UPVOTE { return UPVOTE_SCORE << 1, 1, -1 } default: return 0, 0, 0 } return 0, 0, 0 } // 为 投票 更新数据(忽略部分参数校验;没有事务控制,第 4 章会介绍 Redis 事务) func doVoteArticle(conn redis.Conn, userId int, articleId int, oldVoteType VoteType, voteType VoteType) { // 获取 分数、支持票数、反对票数 增量 scoreDelta, upvoteNumDelta, downvoteNumDelta := getDelta(oldVoteType, voteType) // 更新当前用户投票类型 votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId)) _, err := conn.Do("HSET", votedUserKey, userId, voteType) // 设置错误,则返回 if err != nil { return } // 更新 当前文章 得分 _, err = conn.Do("ZINCRBY", SCORE, scoreDelta, articleId) // 自增错误,则返回 if err != nil { return } // 更新 当前文章 支持票数 articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId)) _, err = conn.Do("HINCRBY", articleKey, "upvoteNum", upvoteNumDelta) // 自增错误,则返回 if err != nil { return } // 更新 当前文章 反对票数 _, err = conn.Do("HINCRBY", articleKey, "downvoteNum", downvoteNumDelta) // 自增错误,则返回 if err != nil { return } } // 执行投票逻辑(忽略部分参数校验;没有事务控制,第 4 章会介绍 Redis 事务) func VoteArticle(conn redis.Conn, userId int, articleId int, voteType VoteType) { // 计算当前时间能投票的文章的最先发布时间 earliestPostTime := time.Now().Unix() - ONE_WEEK_SECONDS // 获取 当前文章 的发布时间 postTime, err := redis.Int64(conn.Do("ZSCORE", POST_TIME, articleId)) // 获取错误 或 文章 articleId 的投票截止时间已过,则返回 if err != nil || postTime < earliestPostTime { return } // 获取集合中投票类型 votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId)) result, err := conn.Do("HGET", votedUserKey, userId) // 查询错误,则返回 if err != nil { return } // 转换后 oldVoteType 必为 "", "1", "2" 其中之一 oldVoteType, err := redis.String(result, err) // 若是投票类型不变,则不进行处理 if VoteType(oldVoteType) == voteType { return } // 执行投票修改数据逻辑 doVoteArticle(conn, userId, articleId, VoteType(oldVoteType), voteType) }
本文首发于公众号:满赋诸机(点击查看原文) 开源在 GitHub :reading-notes/redis-in-action