Redis practice – 02. Redis simple practice – article voting

Time:2021-4-1

demand

Function:P15
  • Post articles
  • Get articles
  • Article grouping
  • Vote for it
Values and restrictionsP15
  1. If an article gets at least 200 votes, it’s an interesting article
  2. If the website has 50 interesting articles every day, the website should put these 50 articles in the top 100 of the article list page for at least one day
  3. Support article score (vote for support will add score), and the score decreases with time

realization

Vote for itP15

If we want to realize the real-time score decreasing with time, and support sorting by score, then the workload is huge and inaccurate. It is conceivable that only the timestamp will change with time in real time. If we take the timestamp of published articles as the initial score, the initial score of later published articles will be higher, and the score will decrease with time from another level. According to 200 support tickets per interesting article per day, each ticket can increase the score by 432 points in an average day (86400 seconds).

In order to get the articles according to the score and time order, the article ID and the corresponding information need to exist in two ordered sets: posttime and score.

In order to prevent unified users from voting for unified articles more than once, the user ID of each article voting should be recorded and stored in the collection as: voteduser: {articleid}.

At the same time, it is stipulated that an article can not be voted one week after its release period, the score will be fixed, and the user list collection that records the voting of the article will also be deleted.

// redis key
type RedisKey string
const (
    //Release time ordered collection
    POST_TIME RedisKey = "postTime"
    //Orderly collection of article scores
    SCORE RedisKey = "score"
    //Article voting user set prefix
    VOTED_USER_PREFIX RedisKey = "votedUser:"
    //Publish article number string
    ARTICLE_COUNT RedisKey = "articleCount"
    //Publish article hash table prefix
    ARTICLE_PREFIX RedisKey = "article:"
    //Group prefix
    GROUP_PREFIX RedisKey = "group:"
)

const ONE_WEEK_SECONDS = int64(7 * 24 * 60 * 60)
const UPVOTE_SCORE = 432

//User userid votes for article articleid (no transaction control, Chapter 4 will introduce redis transaction)
func UpvoteArticle(conn redis.Conn, userId int, articleId int) {
    //Calculate the earliest publishing time of articles that can vote at the current time
    earliestPostTime := time.Now().Unix() - ONE_WEEK_SECONDS

    //Get the publishing time of the current article
    postTime, err := redis.Int64(conn.Do("ZSCORE", POST_TIME, articleId))
    //Get error or the voting deadline of article articleid has passed
    if err != nil || postTime < earliestPostTime {
        return
    }

    //If the current article can vote, vote
    votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
    addedNum, err := redis.Int(conn.Do("SADD", votedUserKey, userId))
    //Add error or vote has been cast, return
    if err != nil || addedNum == 0 {
        return
    }

    //If the user has been successfully added to the voting set of the current article, the score of the current article will be increased
    _, err = conn.Do("ZINCRBY", SCORE, UPVOTE_SCORE, articleId)
    //Autoincrement error, return
    if err != nil {
        return
    }
    //Increase the number of support votes for the current article
    articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId))
    _, err = conn.Do("HINCRBY", articleKey, 1)
    //Autoincrement error, return
    if err != nil {
        return
    }
}
Post articlesP17

have access toINCRThe command generates a self increasing unique ID for each article.

The user ID of the publisher is recorded in the voting user set of the article (that is, the publisher votes for himself by default), and the expiration time is set to one week.

Store the information about the article, and record the initial score and publishing time.

//Publish articles (no transaction control, Chapter 4 will introduce redis transactions)
func PostArticle(conn redis.Conn, userId int, title string, link string) {
    //Get the self increasing ID of the current article
    articleId, err := redis.Int(conn.Do("INCR", ARTICLE_COUNT))
    if err != nil {
        return
    }

    //Add authors to the voting user collection
    votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
    _, err = conn.Do("SADD", votedUserKey, userId)
    if err != nil {
        return
    }

    //Set the expiration time of voting user collection to one week
    _, err = conn.Do("EXPIRE", votedUserKey, ONE_WEEK_SECONDS)
    if err != nil {
        return
    }

    postTime := time.Now().Unix()
    articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId))
    //Set information about articles
    _, err = conn.Do("HMSET", articleKey,
        "title", title,
        "link", link,
        "userId", userId,
        "postTime", postTime,
        "upvoteNum", 1,
    )
    if err != nil {
        return
    }

    //Set publishing time
    _, err = conn.Do("ZADD", POST_TIME, postTime, articleId)
    if err != nil {
        return
    }
    //Set article rating
    score := postTime + UPVOTE_SCORE
    _, err = conn.Do("ZADD", SCORE, score, articleId)
    if err != nil {
        return
    }
}
Get articles by pageP18

Paging access supports four sorts, and returns an empty array when getting an error.

be careful:ZRANGEandZREVRANGEThe range of is closed from beginning to end.

type ArticleOrder int
const (
    TIME_ASC ArticleOrder = iota
    TIME_DESC
    SCORE_ASC
    SCORE_DESC
)

//Obtain the corresponding command and rediskey according to article order
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 "", ""
    }
}

//Simple access to logic parameters (ignoring the article's pagination)
func doListArticles(conn redis.Conn, page int, pageSize int, command string, redisKey RedisKey) []map[string]string {
    var articles []map[string]string

    //Article order is wrong, return empty list
    if command == "" || redisKey == ""{
        return nil
    }

    //Get the starting and ending subscripts (all closed intervals)
    start := (page - 1) * pageSize
    end := start + pageSize - 1
    //Get the list of article IDS
    ids, err := redis.Ints(conn.Do(command, redisKey, start, end))
    if err != nil {
        return articles
    }
    //Get information about each article
    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
}

//Get articles by page
func ListArticles(conn redis.Conn, page int, pageSize int, articleOrder ArticleOrder) []map[string]string {
    //Get the command and rediskey corresponding to articleorder
    command, redisKey := getCommandAndRedisKey(articleOrder)
    //Execute paging to get the article logic and return the result
    return doListArticles(conn, page, pageSize, command, redisKey)
}
Article groupingP19

It supports adding articles to the grouping set and deleting articles from the grouping set.

//Set grouping
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
    //Convert [] int to [] interface {}
    for i, articleId := range articleIds {
        args[i + 1] = articleId
    }

    //Direct conversion of [] int to [] interface {} is not supported
    //It also doesn't support parameter passing like groupkey, articleids... (the matching parameters are interface {},... Interface {})
    _, _ = conn.Do("SADD", args...)
}

//Ungroup
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
    //Convert [] int to [] interface {}
    for i, articleId := range articleIds {
        args[i + 1] = articleId
    }

    //Direct conversion of [] int to [] interface {} is not supported
    //It also doesn't support parameter passing like groupkey, articleids... (the matching parameters are interface {},... Interface {})
    _, _ = conn.Do("SREM", args...)
}
Get articles in groups by pageP20

Grouping information and sorting information are in different (ordered) sets, so we need to take the intersection of two (ordered) sets, and then get them by pagination.

It takes a lot of time to get the intersection, so the cache is 60s, and it is not generated in real time.

//Cache expiration time 60s
const EXPIRE_SECONDS = 60

//Get the articles in a group by paging (ignore the simple logic such as parameter verification; the expiration setting is not in the transaction)
func ListArticlesFromGroup(conn redis.Conn, groupId int, page int, pageSize int, articleOrder ArticleOrder) []map[string]string {
    //Get the command and rediskey corresponding to articleorder
    command, redisKey := getCommandAndRedisKey(articleOrder)
    //The article order is not correct. It returns an empty list to prevent more intersection operations
    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 the intersection does not exist or has expired, the intersection is taken
    if err == nil || exists != 1 {
        _, err := conn.Do("ZINTERSTORE", targetRedisKey, 2, redisKey, groupKey)
        if err != nil {
            return nil
        }
    }

    //Set the expiration time (the expiration setting fails and does not affect the query)
    _, _ = conn.Do("EXPIRE", targetRedisKey, EXPIRE_SECONDS)

    //Execute paging to get the article logic and return the result
    return doListArticles(conn, page, pageSize, command, targetRedisKey)
}
Exercise: vote noP21

Add the function of voting against, and support the mutual transfer of support vote and negative vote.

  • After seeing this exercise and the corresponding tips, I contacted the usual voting scene and felt that the way in the title was not reasonable. When voting for or against, the corresponding conversion logic is in line with the user’s habits and has good scalability.
  • Changes

    • The article hash, add a downvote num field, used to record the number of votes against
    • In this paper, the voting user set set is changed to hash to store the voting types
    • The upvotearticle function is replaced with votearticle, and an input parameter of votetype is added. The function supports not only yes / no voting, but also cancel voting
// redis key
type RedisKey string
const (
    //Release time ordered collection
    POST_TIME RedisKey = "postTime"
    //Orderly collection of article scores
    SCORE RedisKey = "score"
    //Article voting user set prefix
    VOTED_USER_PREFIX RedisKey = "votedUser:"
    //Publish article number string
    ARTICLE_COUNT RedisKey = "articleCount"
    //Publish article hash table prefix
    ARTICLE_PREFIX RedisKey = "article:"
    //Group prefix
    GROUP_PREFIX RedisKey = "group:"
)

type VoteType string
const (
    //No vote
    NONVOTE VoteType = ""
    //Vote for it
    UPVOTE VoteType = "1"
    //Vote No
    DOWNVOTE VoteType = "2"
)

const ONE_WEEK_SECONDS = int64(7 * 24 * 60 * 60)
const UPVOTE_SCORE = 432

//According to the original voting type and the new voting type, obtain the increment of the score, the number of supporting votes and the number of opposing votes (if the "enumeration" is not correct, return 0 directly)
func getDelta(oldVoteType VoteType, newVoteType VoteType) (scoreDelta, upvoteNumDelta, downvoteNumDelta int) {
    //The type does not change and the relevant value does not change
    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
}

//Update data for voting (ignore some parameter validation; no transaction control, Chapter 4 will introduce redis transaction)
func doVoteArticle(conn redis.Conn, userId int, articleId int, oldVoteType VoteType, voteType VoteType) {
    //Gain points, support votes and negative votes
    scoreDelta, upvoteNumDelta, downvoteNumDelta := getDelta(oldVoteType, voteType)
    //Update current user voting type
    votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
    _, err := conn.Do("HSET", votedUserKey, userId, voteType)
    //Set error, return
    if err != nil {
        return
    }

    //Update current article score
    _, err = conn.Do("ZINCRBY", SCORE, scoreDelta, articleId)
    //Autoincrement error, return
    if err != nil {
        return
    }
    //Update the number of support votes for the current article
    articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId))
    _, err = conn.Do("HINCRBY", articleKey, "upvoteNum", upvoteNumDelta)
    //Autoincrement error, return
    if err != nil {
        return
    }
    //Update the number of negative votes in the current article
    _, err = conn.Do("HINCRBY", articleKey, "downvoteNum", downvoteNumDelta)
    //Autoincrement error, return
    if err != nil {
        return
    }
}

//Execute voting logic (ignore part of parameter verification; no transaction control, Chapter 4 will introduce redis transaction)
func VoteArticle(conn redis.Conn, userId int, articleId int, voteType VoteType) {
    //Calculate the earliest publishing time of articles that can vote at the current time
    earliestPostTime := time.Now().Unix() - ONE_WEEK_SECONDS

    //Get the publishing time of the current article
    postTime, err := redis.Int64(conn.Do("ZSCORE", POST_TIME, articleId))
    //Get error or the voting deadline of article articleid has passed
    if err != nil || postTime < earliestPostTime {
        return
    }
    //Gets the voting type in the collection
    votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
    result, err := conn.Do("HGET", votedUserKey, userId)
    //Query error, return
    if err != nil {
        return
    }
    //Oldvotetype must be one of "" 1 "" 2 "after conversion
    oldVoteType, err := redis.String(result, err)
    //If the voting type remains unchanged, it will not be processed
    if VoteType(oldVoteType) == voteType {
        return
    }

    //Execute voting to modify data logic
    doVoteArticle(conn, userId, articleId, VoteType(oldVoteType), voteType)
}

Summary

  • Redis features

    • Memory storage: redis is very fast
    • Remote: redis can connect with multiple clients and servers
    • Persistence: after the server restarts, it still keeps the data before the restart
    • Scalable: master slave replication and fragmentation

What you think

  • The code is not formed at one time, it will constantly improve the previous logic in the process of writing new functions, and extract public methods to achieve high maintainability and scalability.
  • I feel that the idea has not changed (I don’t know it’s still the problem of redis open source library). I have been using the idea of Java all the time, and it’s inconvenient in many places.
  • Although some private methods written by ourselves guarantee that some abnormal data will not appear, some will be handled accordingly in case of errors caused by careless calls in the future.

This article starts with the official account: full Fu machine (click to view the original), open source in GitHub:reading-notes/redis-in-action
Redis practice - 02. Redis simple practice - article voting