demand
Function:P15
- Published articles
- Access to the article
- The article group
- voted
Values and constraintsP15
- An article is an interesting article if it gets at least 200 support votes
- If the site has 50 interesting articles a day, then the site should put those 50 articles in the top 100 of the article list page for at least one day
- Support article rating (voting in favor will give you a rating), and the rating decreases over time
implementation
votedP15
Implementing ratings that decrease over time in real time and support sorting by rating is a lot of work and imprecise. It can be thought that only the timestamp will change in real time. If we take the timestamp of the published article as the initial score, then the initial score of the later published article will be higher, which realizes the score decreasing with time from another level. Based on 200 votes per day for each interesting article, each vote adds up to 432 points over the course of an average day (86,400 seconds).
In order to obtain articles in order of score and time, the article ID and corresponding information need to be stored in two ordered sets, namely, postTime and Score.
To prevent unified users from voting on unified articles more than once, you need to record the user ID that voted for each article, stored in the collection as: votedUser:{articleId}.
It also stipulates that a post cannot be voted on after one week, the ratings will be fixed, and the set of user lists that record the post’s votes will be deleted.
// redis key
type RedisKey string
const (
// Release time ordered set
POST_TIME RedisKey = "postTime"
// article score ordered set
SCORE RedisKey = "score"
// Article voting user set prefix
VOTED_USER_PREFIX RedisKey = "votedUser:"
// Number of published articles string
ARTICLE_COUNT RedisKey = "articleCount"
// Publish the article hash prefix
ARTICLE_PREFIX RedisKey = "article:"
// Group prefix
GROUP_PREFIX RedisKey = "group:"
)
const ONE_WEEK_SECONDS = int64(7 * 24 * 60 * 60)
const UPVOTE_SCORE = 432
UserId votes for articleId (without transaction control, Redis transactions are covered in Chapter 4)
func UpvoteArticle(conn redis.Conn, userId int, articleId int) {
// Calculate the current time of the earliest post of the voting article
earliestPostTime := time.Now().Unix() - ONE_WEEK_SECONDS
// Get the publication time of the current article
postTime, err := redis.Int64(conn.Do("ZSCORE", POST_TIME, articleId))
Get an error or the voting deadline for articleId has passed
iferr ! =nil || postTime < earliestPostTime {
return
}
// If the current article can be voted, the vote operation is performed
votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
addedNum, err := redis.Int(conn.Do("SADD", votedUserKey, userId))
// Add an error or the current vote is returned
iferr ! =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 is increased
_, err = conn.Do("ZINCRBY", SCORE, UPVOTE_SCORE, articleId)
// Error increment, return
iferr ! =nil {
return
}
// Increase the number of votes for the current article
articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId))
_, err = conn.Do("HINCRBY", articleKey, 1)
// Error increment, return
iferr ! =nil {
return}}Copy the code
Published articlesP17
You can use the INCR command to generate a self-increment unique ID for each article.
Add the publisher’s userId to the set of voting users for this article (that is, the publisher votes for himself by default) and set the expiration time to one week.
Store information about articles and record initial ratings and publication dates.
// Publish articles (without transaction control, chapter 4 covers Redis transactions)
func PostArticle(conn redis.Conn, userId int, title string, link string) {
// Get the id of the current article
articleId, err := redis.Int(conn.Do("INCR", ARTICLE_COUNT))
iferr ! =nil {
return
}
// Add the author to the voting user collection
votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
_, err = conn.Do("SADD", votedUserKey, userId)
iferr ! =nil {
return
}
// Set the expiration time of the voting user set to one week
_, err = conn.Do("EXPIRE", votedUserKey, ONE_WEEK_SECONDS)
iferr ! =nil {
return
}
postTime := time.Now().Unix()
articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId))
// Set the information about the article
_, err = conn.Do("HMSET", articleKey,
"title", title,
"link", link,
"userId", userId,
"postTime", postTime,
"upvoteNum".1.)iferr ! =nil {
return
}
// Set the publishing time
_, err = conn.Do("ZADD", POST_TIME, postTime, articleId)
iferr ! =nil {
return
}
// Set the article rating
score := postTime + UPVOTE_SCORE
_, err = conn.Do("ZADD", SCORE, score, articleId)
iferr ! =nil {
return}}Copy the code
Paging for articlesP18
Paged fetching supports four sorts and returns an empty array on an error fetching.
Note: the range of ZRANGE and ZREVRANGE starts and ends in closed ranges.
type ArticleOrder int
const (
TIME_ASC ArticleOrder = iota
TIME_DESC
SCORE_ASC
SCORE_DESC
)
// Get the command and RedisKey according to ArticleOrder
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 "".""}}// Perform paging fetch article logic (ignore some simple parameter verification and other logic)
func doListArticles(conn redis.Conn, page int, pageSize int, command string, redisKey RedisKey) []map[string]string {
var articles []map[string]string
// ArticleOrder returns an empty list
if command == "" || redisKey == ""{
return nil
}
// Get the start and end indices (both 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))
iferr ! =nil {
return articles
}
// Get information for 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
}
// Paging to get the article
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)
// Perform paging to get the article logic and return the result
return doListArticles(conn, page, pageSize, command, redisKey)
}
Copy the code
The article groupP19
Support for adding articles to and removing articles from grouped collections.
// Set the 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
// []int converts to []interface{}
for i, articleId := range articleIds {
args[i + 1] = articleId
}
[]interface{} []interface{}
// groupKey, articleIds... Interface {},... Interface {})
_, _ = conn.Do("SADD", args...)
}
// Cancel grouping
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 converts to []interface{}
for i, articleId := range articleIds {
args[i + 1] = articleId
}
[]interface{} []interface{}
// groupKey, articleIds... Interface {},... Interface {})
_, _ = conn.Do("SREM", args...)
}
Copy the code
Group pages to get articlesP20
Grouping information and sorting information are in different (ordered) sets, so you need to take the intersection of the two (ordered) sets and page it out.
The intersection takes time, so 60 seconds is cached and not generated in real time.
// Cache expiration time 60s
const EXPIRE_SECONDS = 60
// Paging to get articles in a group (ignore simple logic such as parameter verification; Expiration setting not in 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)
// ArticleOrder returns an empty list to prevent multiple 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))
// The intersection does not exist or has expired
if err == nil|| exists ! =1 {
_, err := conn.Do("ZINTERSTORE", targetRedisKey, 2, redisKey, groupKey)
iferr ! =nil {
return nil}}// Set expiration time (failed to set expiration time, does not affect query)
_, _ = conn.Do("EXPIRE", targetRedisKey, EXPIRE_SECONDS)
// Perform paging to get the article logic and return the result
return doListArticles(conn, page, pageSize, command, targetRedisKey)
}
Copy the code
Exercise: Vote noP21
Add the function of voting against, and support the yas and nays to rotate.
- After seeing this exercise and the corresponding prompts, and considering the voting scenes in daily life, I felt that the method in the question was not reasonable. Processing the corresponding conversion logic in the vote for/against is consistent with user habits, but also good scalability.
- Change the place
- Article HASH, adding a downvoteNum field to record the number of dissenters
- The article voting user SET is changed to a HASH to store the type of user voting
- Change the UpvoteArticle function to VoteArticle and add an input parameter of type VoteType. This function not only supports voting for/against, but also supports canceling voting
// redis key
type RedisKey string
const (
// Release time ordered set
POST_TIME RedisKey = "postTime"
// article score ordered set
SCORE RedisKey = "score"
// Article voting user set prefix
VOTED_USER_PREFIX RedisKey = "votedUser:"
// Number of published articles string
ARTICLE_COUNT RedisKey = "articleCount"
// Publish the article hash prefix
ARTICLE_PREFIX RedisKey = "article:"
// Group prefix
GROUP_PREFIX RedisKey = "group:"
)
type VoteType string
const (
/ / not to vote
NONVOTE VoteType = ""
// Vote yes
UPVOTE VoteType = "1"
// Vote no
DOWNVOTE VoteType = "2"
)
const ONE_WEEK_SECONDS = int64(7 * 24 * 60 * 60)
const UPVOTE_SCORE = 432
// Get the increments of scores, votes for, and votes against according to the original voting type and the new voting type.
func getDelta(oldVoteType VoteType, newVoteType VoteType) (scoreDelta, upvoteNumDelta, downvoteNumDelta int) {
// The type 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 the vote (ignore some parameter verification; Without transaction control, Chapter 4 covers Redis transactions.)
func doVoteArticle(conn redis.Conn, userId int, articleId int, oldVoteType VoteType, voteType VoteType) {
// Get the score, the number of votes for, and the increment of votes against
scoreDelta, upvoteNumDelta, downvoteNumDelta := getDelta(oldVoteType, voteType)
// Update the current user voting type
votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
_, err := conn.Do("HSET", votedUserKey, userId, voteType)
// If the Settings are incorrect, return
iferr ! =nil {
return
}
// Update the current article score
_, err = conn.Do("ZINCRBY", SCORE, scoreDelta, articleId)
// Error increment, return
iferr ! =nil {
return
}
// Update the current post support count
articleKey := ARTICLE_PREFIX + RedisKey(strconv.Itoa(articleId))
_, err = conn.Do("HINCRBY", articleKey, "upvoteNum", upvoteNumDelta)
// Error increment, return
iferr ! =nil {
return
}
// Update the current post vote
_, err = conn.Do("HINCRBY", articleKey, "downvoteNum", downvoteNumDelta)
// Error increment, return
iferr ! =nil {
return}}// Execute the voting logic (ignore some parameter verification; Without transaction control, Chapter 4 covers Redis transactions.)
func VoteArticle(conn redis.Conn, userId int, articleId int, voteType VoteType) {
// Calculate the current time of the earliest post of the voting article
earliestPostTime := time.Now().Unix() - ONE_WEEK_SECONDS
// Get the publication time of the current article
postTime, err := redis.Int64(conn.Do("ZSCORE", POST_TIME, articleId))
Get an error or the voting deadline for articleId has passed
iferr ! =nil || postTime < earliestPostTime {
return
}
// Get the vote type in the collection
votedUserKey := VOTED_USER_PREFIX + RedisKey(strconv.Itoa(articleId))
result, err := conn.Do("HGET", votedUserKey, userId)
// Query error, return
iferr ! =nil {
return
}
// oldVoteType must be one of "", "" 1", "2"
oldVoteType, err := redis.String(result, err)
// If the vote type does not change, it is not processed
if VoteType(oldVoteType) == voteType {
return
}
// Perform the vote to modify the data logic
doVoteArticle(conn, userId, articleId, VoteType(oldVoteType), voteType)
}
Copy the code
summary
- Redis features
- Memory storage: Redis is very fast
- Remote: Redis can connect to multiple clients and servers
- Persistence: The data before the restart remains after the server restarts
- Extensible: master slave replication and sharding
thoughts
- Code does not take shape all at once, refining old logic as new features are written and extracting common methods to achieve high maintainability and extensibility.
- I still feel that I haven’t changed my mind (I don’t know if it is the problem of the Redis open source library). I have been using the idea of Java, and many places are not convenient.
- Although some of the proprietary methods you write are guaranteed not to have some abnormal data, some are handled accordingly, in case you don’t notice the error later.
This article is published on GitHub: Reading-notes/Redis-in-Action