需要

性能: P15
  • 公布文章
  • 获取文章
  • 文章分组
  • 投反对票
数值及限度条件 P15
  1. 如果一篇文章取得了至多 200 张反对票,那么这篇文章就是一篇乏味的文章
  2. 如果这个网站每天有 50 篇乏味的文章,那么网站要把这 50 篇文章放到文章列表页前 100 位至多一天
  3. 反对文章评分(投反对票会加评分),且评分随工夫递加

实现

投反对票 P15

如果要实现评分实时随工夫递加,且反对按评分排序,那么工作量很大而且不准确。能够想到只有工夫戳会随工夫实时变动,如果咱们把公布文章的工夫戳当作初始评分,那么后公布的文章初始评分肯定会更高,从另一个层面上实现了评分随工夫递加。依照每个乏味文章每天 200 张反对票计算,均匀到一天(86400 秒)中,每张票能够将分进步 432 分。

为了依照评分和工夫排序获取文章,须要文章 id 及相应信息存在两个有序汇合中,别离为:postTime 和 score 。

为了避免对立用户对对立文章屡次投票,须要记录每篇文章投票的用户id,存储在汇合中,为:votedUser:{articleId} 。

同时规定一篇文章公布期满一周后不能再进行投票,评分将被固定下来,同时记录文章曾经投票的用户名单汇合也会被删除。

// redis keytype RedisKey stringconst (    // 公布工夫 有序汇合    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 记录到该文章的投票用户汇合中(即发布者默认为本人投反对票),同时设置过期工夫为一周。

存储文章相干信息,并将初始评分和公布工夫记录下来。

// 公布文章(没有事务管制,第 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

分页获取反对四种排序,获取谬误时返回空数组。

留神:ZRANGEZREVRANGE 的范畴起止都是闭区间。

type ArticleOrder intconst (    TIME_ASC ArticleOrder = iota    TIME_DESC    SCORE_ASC    SCORE_DESC)// 依据 ArticleOrder 获取相应的 命令 和 RedisKeyfunc 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

反对将文章退出到分组汇合,也反对将文章从分组汇合中删除。

// 设置分组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,不实时生成。

// 缓存过期工夫 60sconst 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

减少投反对票性能,并反对反对票和反对票互转。

  • 看到这个练习和相应的提醒后,又分割素日里投票的场景,感觉题目中的形式并不合理。在投反对/反对票时解决相应的转换逻辑合乎用户习惯,也能又较好的扩展性。
  • 更改处

    • 文章 HASH,减少一个 downvoteNum 字段,用于记录投反对票人数
    • 文章投票用户汇合 SET 改为 HASH,用于存储用户投票的类型
    • UpvoteArticle 函数换为 VoteArticle,同时减少一个类型为 VoteType 的入参。函数性能不仅反对投反对/反对票,还反对勾销投票
// redis keytype RedisKey stringconst (    // 公布工夫 有序汇合    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 stringconst (    // 未投票    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)}

小结

  • Redis 个性

    • 内存存储:Redis 速度十分快
    • 近程:Redis 能够与多个客户端和服务器进行连贯
    • 长久化:服务器重启之后依然放弃重启之前的数据
    • 可扩大:主从复制和分片

所思所想

  • 代码不是一次成形的,会在写新性能的过程中不断完善以前的逻辑,并抽取公共办法以达到较高的可维护性和可扩展性。
  • 感觉思路还是没有转过来(不晓得还是这个 Redis 开源库的问题),始终使用 Java 的思维,很多中央写着不不便。
  • 尽管本人写的一些公有的办法保障不会呈现某些异样数据,然而还是有一些会进行相应的解决,以防当前没留神调用了出错。
本文首发于公众号:满赋诸机(点击查看原文) 开源在 GitHub :reading-notes/redis-in-action