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