The source address

Redis database design

Suppose the business logic is “upvotes to upvotes +1 upvotes to upvotes -1”

Use zset ordered collections to design the database

Zset A:

Valus is the number of likes on a post by post ID score

In addition, we should consider that each user can only give a post once, you can like or tap, but you can not repeatedly like or tap

Zset post ID:

Values are the ID of the user who voted for the post, and score is either 1 or -1, indicating that the user liked or clicked on the post

The specific implementation

Register the route first, which obviously requires verification of login

v1.POST("/like/", middleware.JWTAuthMiddleWare(), controllers.PostLikeHandler)
Copy the code

Define two types of errors and parameters

Type ParamLikeData struct {// thread id PostId int64 'json:"post_id,string" binding:"required"' // Direction int64 'json:" Direction,string" Binding :" Required,oneof= 1-1 "'} const (DirectionLike =1) DirectionUnLike = -1 )Copy the code
Var ErrAleadyUnLike = errors.New() var ErrAleadyUnLike = errors.New()Copy the code

The controller to achieve

// func PostLikeHandler(c *gin.Context) {p := new(models.paramLikeData) if err := c.houldbindjson (p); err ! = nil { zap.L().Error("PostLikeHandler with invalid param", Zap. Error(err)) // The json format Error is not a validator Error and cannot be translated. So here to do type judgment is incremented, ok: = err. (the validator. ValidationErrors) if! ok { ResponseError(c, CodeInvalidParam) } else { ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans))) } return } id, err := getCurrentUserId(c) if err ! Err = logic.PostLike(p, id) if err! = nil {/ / can take if errors Is (err, logic. ErrAleadyLike) | | errors. Is (err, logic.ErrAleadyUnLike) { ResponseErrorWithMsg(c, CodeInvalidParam, err.Error()) } else { ResponseError(c, CodeServerBusy) } return } ResponseSuccess(c, "success") }Copy the code

Look at the logic layer

Func PostLike(postData *models.ParamLikeData, userId int64) error {postData *models.ParamLikeData, userId int64) flag := redis.CheckLike(postData.PostId, If direction == postdata.direction && direction == models.DirectionLike {userId) if flag {// If direction == postdata.direction && direction == models. Return ErrAleadyLike} if direction == postdata.direction && direction == postdata.direction models.DirectionUnLike { return ErrAleadyUnLike } } err := redis.DoLike(postData.PostId, userId, postData.Direction) if err ! = nil { return err } err = redis.AddLike(postData.PostId, postData.Direction) if err ! = nil { return err } return nil }Copy the code

The DAO layer is mainly to get familiar with the basic operations of Redis

package redis import ( "go_web_app/utils" "github.com/go-redis/redis" "go.uber.org/zap" ) func getRedisKeyForLikeUserSet(postId int64) string { key := KeyPostLikeZetPrefix + utils.Int64ToString(postId) zap.L().Debug("getRedisKeyForLikeUserSet", zap.String("setKey", Func CheckLike(postId int64, postId int64, postId int64, postId int64, postId int64) userId int64) (int64, bool) { like := rdb.ZScore(getRedisKeyForLikeUserSet(postId), utils.Int64ToString(userId)) result, err := like.Result() if err ! = nil { zap.L().Error("checkLike error", zap.Error(err)) return 0, false } zap.L().Info("checkLike val", zap.Float64(utils.Int64ToString(userId), like.Val())) return int64(result), Func DoLike(postId int64, userId int64, direction int64) error { value := redis.Z{ Score: float64(direction), Member: utils.Int64ToString(userId), } _, err := rdb.ZAdd(getRedisKeyForLikeUserSet(postId), value).Result() if err ! = nil { zap.L().Error("doLike error", Zap. Error(err)) return err} return nil} func AddLike(postId int64, direction int64) error { _, err := rdb.ZIncrBy(KeyLikeNumberZSet, float64(direction), utils.Int64ToString(postId)).Result() if err ! = nil { zap.L().Error("AddLike error", zap.Error(err)) return err } return nil }Copy the code

Use transactions to improve

While the previous approach looks right, it’s still a bit of a problem, because we’re actually using two writes to like it, which should be both. This means using a transaction to wrap two writes together

Func DoLike(postId int64, userId int64, direction int64) error { pipeLine := rdb.TxPipeline() value := redis.Z{ Score: float64(direction), Member: utils.Int64ToString(userId), } pipeLine.ZAdd(getRedisKeyForLikeUserSet(postId), value) pipeLine.ZIncrBy(KeyLikeNumberZSet, float64(direction), utils.Int64ToString(postId)) _, err := pipeLine.Exec() if err ! = nil { zap.L().Error("doLike error", zap.Error(err)) return err } return nil }Copy the code

Latest hot list

The latest list is easy to understand, just sort by create_time

The hottest list is basically a list of likes from the most liked to the least liked

How to make the hottest list?

In fact, when Posting, the success of the post will be put into our Redis Zset.

Then when the list is hot, zset will sort it by the number of likes and return us the list of the corresponding post ids

With this ID slice, we can go to mysql to query the details of the post.

So how do we do this requirement

The first is to add a new record in Redis when you post

Let’s write the redis operation first

Func AddPost(postId int64) error {_, err := rdb.ZAdd(KeyLikeNumberZSet, redis.Z{ Score: 0, Member: utils.Int64ToString(postId), }).Result() if err ! = nil { zap.L().Error("AddPost", zap.Error(err)) return err } return nil }Copy the code

Then change the logic layer of our post

func CreatePost(post *models.Post) (msg string, Error) {post.id = snowflake.GenId() zap.l ().debug ("createPostLogic", zap.int64 ("postId", post.Id)) err = mysql.InsertPost(post) if err ! Err = redis.addPost (post.id) if err! = nil {return "failed", err} // Add a new record to zset. Return strconv.FormatInt(post.id, 10), nil} return strconv.FormatInt(post.id, 10), nil}Copy the code

And then we’re going to write our list interface

Let’s first define the parameters

type ParamListData struct {
   PageSize int64  `form:"pageSize" binding:"required"`
   PageNum  int64  `form:"pageNum" binding:"required"`
   Order    string `form:"order" binding:"required,oneof=time hot"`
}
Copy the code
Const (DirectionLike = 1 DirectionUnLike = -1 // OrderByTime = "time" // OrderByHot = "hot")Copy the code

The next step is to parse the parameters at the Controller layer

Func GetPostListHandler2(c *gin.Context) {p := new(models.paramlistData) // Check if err := c.ShouldBindQuery(p); err ! = nil { zap.L().Error("CreatePostHandler with invalid param", zap.Error(err)) errs, ok := err.(validator.ValidationErrors) if ! ok { ResponseError(c, CodeInvalidParam) } else { ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans))) } return } apiList, err := logic.GetPostList2(p) if err ! = nil { return } ResponseSuccess(c, apiList) }Copy the code

Focus on the Logic layer

func GetPostList2(params *models.ParamListData) (apiPostDetailList []*models.ApiPostDetail, Order == models.OrderByHot {err error) {// If params.Order == models.OrderByHot { err := redis.GetPostIdsByScore(params.PageSize, params.PageNum) if err ! = nil { return nil, err } postLists, err := mysql.GetPostListByIds(ids) if err ! = nil { return nil, Err} return rangeInitApiPostDetail(postLists)} else if params.Order == models.OrderByTime {// Latest return GetPostList(params.PageSize, params.PageNum) } return nil, nil }Copy the code

Order by create_time desc order by create_time desc

How to write the redis layer

Func GetPostIdsByScore(pageSize int64, pageNum int64) (ids []string, err error) { start := (pageNum - 1) * pageSize stop := start + pageSize - 1 ids, err = rdb.ZRevRange(KeyLikeNumberZSet, start, stop).Result() if err ! = nil { zap.L().Error("GetPostIdsByScore", zap.Error(err)) return nil, err } return ids, err }Copy the code

After getting the descending order of the post ID, you can go to mysql to query specific data

func GetPostListByIds(ids []string) (postList []*models.Post, Err error) {//FIND_IN_SET Returns a result set in the given order. SqlStr := "select post_id,title,content,author_id,community_id,create_time,update_time" + " from post where post_id in (?) order by FIND_IN_SET(post_id,?) " query, args, err := sqlx.In(sqlStr, ids, strings.Join(ids, ",")) if err ! = nil { return nil, err } query = db.Rebind(query) err = db.Select(&postList, query, args...) if err ! = nil { zap.L().Error("GetPostListByIds", zap.Error(err)) return nil, err } return postList, nil }Copy the code

In this way, the collection of posts has been searched

The last step is to wrap our POST data into API data

This logic, which we have written before, is only extracted so that we can call both logic

func rangeInitApiPostDetail(posts []*models.Post) (apiPostDetailList []*models.ApiPostDetail, err error) { for _, Err := mysql.getUsernameById (post.authorid) if err! = nil {ap.l ().warn ("no author ") err = nil return nil, err} err := GetCommunityById(post.CommunityId) if err ! = nil { zap.L().Warn("no community ") err = nil return nil, err } apiPostDetail := new(models.ApiPostDetail) apiPostDetail.AuthorName = username apiPostDetail.Community = community  apiPostDetail.Post = post apiPostDetailList = append(apiPostDetailList, apiPostDetail) } return apiPostDetailList, nil }Copy the code