使用Redis开发评论功能 | 青训营笔记

975 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。

简介

我们小组选择抖音项目,本次笔记分享如何使用Redis开发评论功能。

关键词:Redis MySQL Golang

需求说明

  • 新增评论
  • 删除评论
  • 按时间倒序查看视频所有评论

为什么使用redis

从MySQL中直接查询结果,编码简单,维护方便,且在数据量较小的情况下,速度是可以接受的。

使用redis存储数据,会提高维护的成本,但是随着评论数的增加,使用redis查询的性能肯定会更高,查询速度更快,对MySQL数据库的压力更小。

MySQL表设计

字段名数据类型说明
idint主键
fk_user_idint外键,发表评论的用户的id
fk_video_idint外键,被评论的视频的id
contentvarchar评论的内容
statusint评论的状态
create_timedatetime评论的创建时间
update_timedatetime评论的修改时间
  • 注1:status字段为0时,表示该条评论正常展示,status为1时表示该条字段被删除,不进行展示。以后可对status字段的含义进行扩展,如status为2表示评论正在审核,status为3时表示审核通过。
  • 注2:MySQL中删除的效率要低于修改的效率
  • 注3:update_time字段设置根据当前时间戳更新

总体思路

  • 新增评论
    • 评论存入MySQL
    • 评论存入redis
  • 删除评论
    • 将评论从redis删除
    • 修改MySQL中status字段为1
  • 按时间倒序查看视频所有评论
    • 获取reids中该视频的所有评论

redis数据结构的的选择

我们使用有序集合(sorted set)作为redis中存储评论的数据结构,原因是获取评论列表的时候,需要根据发布评论的时间倒序展示,所以我们有一定的排序需求。我们可以将评论的发布时间作为sorted set中的score,将评论存储到value中。这样获取评论列表时,根据score从大到小获取到的评论,就是按照时间倒序的评论列表。

redis命令手册

编码

新增评论

service层

func CreateComment(userId int, videoId int, commentText string) (*model.Comment, error)
  1. 获取当前时间
now := time.Now()                //获取当前时间
time := time.Unix(now.Unix(), 0) //将时间的精度降低到秒级
  1. 新建评论
NewComment := model.Comment{
   UserID:     userId,
   VideoID:    videoId,
   Content:    commentText,
   CreateDate: time,
   IsDeleted:  0,
   UpdateDate: time,
}
  1. 将评论存入MySQL
//将评论存入MySQL中
dbWithTransaction, err := mysql.InsertComment(&NewComment)
if err != nil {
   return nil, err
}
  1. 将评论存入reids中
//将评论存入Redis中
key := tool.GetVideoCommentKey(videoId)
err = redis.AddCommentToSortedSet(key, now.Unix(), &NewComment)
if err != nil {
   dbWithTransaction.Rollback() //事务回滚
   return nil, err
}

获取key的函数放到tool包下

const split = ":"

func GetVideoCommentKey(videoId int) string {
   return "video" + split + "comment" + split + strconv.Itoa(videoId)
}
  1. 提交事务
dbWithTransaction.Commit() //提交事务
  1. 返回新建的评论
return &NewComment, nil

dao层

MySQL中执行insert前需要开启事务

func InsertComment(comment *model.Comment) (*gorm.DB, error) {
   dbWithTransaction := db.Begin() //开启事务
   if err := dbWithTransaction.Create(&comment).Error; err != nil {
      return nil, err
   }
   return dbWithTransaction, nil
}

将评论插入redis层需要考虑一下序列化的事,参考这篇文章:go-redis 优雅存储结构体 - 简书 (jianshu.com)

type Comment struct {
   ID         int       `json:"id"`
   UserID     int       `json:"user_id" db:"user_id" `
   VideoID    int       `json:"video_id" db:"video_id" binding:"required"`
   Content    string    `json:"content" db:"content" binding:"required"`
   CreateDate time.Time `json:"create_date" db:"create_date"`
   IsDeleted  int       `db:"is_deleted"` //0表示显示;1表示删除
   UpdateDate time.Time `json:"update_date" db:"update_date"`
}

func (Comment) TableName() string {
   return "comment"
}

var _ encoding.BinaryMarshaler = new(Comment)
var _ encoding.BinaryUnmarshaler = new(Comment)

func (m *Comment) MarshalBinary() (data []byte, err error) {
   return json.Marshal(m)
}

func (m *Comment) UnmarshalBinary(data []byte) error {
   return json.Unmarshal(data, m)

}
func AddCommentToSortedSet(key string, score int64, comment *model.Comment) error {
   z := redis.Z{
      Score:  float64(score),
      Member: comment,
   }

   _, err := rdb.ZAdd(key, z).Result()
   if err != nil {
      return err
   }
   return nil
}

删除评论

service层

func DeleteComment(commentId int) error
  1. 将MySQL中评论的状态改为删除状态
//修改mysql中评论的状态
dbWithTransaction, err := mysql.UpdateCommentStatus(commentId)
if err != nil {
   return err
}
  1. 根据评论的id从MySQL中获取评论
comment, err := mysql.GetComment(commentId)
if err != nil {
   return err
}
  1. 将reids中评论删除
//从redis中删除评论
key := tool.GetVideoCommentKey(comment.VideoID)
err = redis.RemoveComment(key, comment)
if err != nil {
   dbWithTransaction.Rollback() //回滚事务
   return err
}
  1. 提交事务
dbWithTransaction.Commit() //提交事务

dao层

MySQL中执行update前需要开启事务

// UpdateCommentStatus 将is_deleted字段的值改为1
func UpdateCommentStatus(commentId int) (*gorm.DB, error) {
   dbWithTransaction := db.Begin() //开启事务
   err := dbWithTransaction.Table("comment").Where("id = ? ", commentId).Update("is_deleted", 1).Error
   if err != nil {
      return nil, err
   }
   return dbWithTransaction, nil
}

redis如果刚才序列化的工作(go-redis 优雅存储结构体 - 简书 (jianshu.com))弄过了,就没啥说的了。

func RemoveComment(key string, comment *model.Comment) error {
   count, err := rdb.ZRem(key, comment).Result()
   if err != nil {
      return err
   }
   if count == 0 {
      return errors.New("redis中删除评论失败")
   }
   return nil
}

获取评论列表

service层

func ListComment(videoId int) (*[]model.Comment, error)
  1. 从redis中获取评论
key := tool.GetVideoCommentKey(videoId)
commentList, err := redis.ListComment(key)
if err != nil {
   return nil, err
}

dao层

// ListComment 根据分数从高到低获取所有的评论
func ListComment(key string) (*[]model.Comment, error) {

   var commentList []model.Comment
   err := rdb.ZRevRange(key, 0, -1).ScanSlice(&commentList)
   if err != nil {
      return nil, err
   }

   return &commentList, nil
}

成果展示

image-20220603121855299.png

image-20220603121915595.png

image-20220603122030579.png