这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。
简介
我们小组选择抖音项目,本次笔记分享如何使用Redis开发评论功能。
关键词:Redis MySQL Golang
需求说明
- 新增评论
- 删除评论
- 按时间倒序查看视频所有评论
为什么使用redis
从MySQL中直接查询结果,编码简单,维护方便,且在数据量较小的情况下,速度是可以接受的。
使用redis存储数据,会提高维护的成本,但是随着评论数的增加,使用redis查询的性能肯定会更高,查询速度更快,对MySQL数据库的压力更小。
MySQL表设计
| 字段名 | 数据类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| fk_user_id | int | 外键,发表评论的用户的id |
| fk_video_id | int | 外键,被评论的视频的id |
| content | varchar | 评论的内容 |
| status | int | 评论的状态 |
| create_time | datetime | 评论的创建时间 |
| update_time | datetime | 评论的修改时间 |
- 注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从大到小获取到的评论,就是按照时间倒序的评论列表。
编码
新增评论
service层
func CreateComment(userId int, videoId int, commentText string) (*model.Comment, error)
- 获取当前时间
now := time.Now() //获取当前时间
time := time.Unix(now.Unix(), 0) //将时间的精度降低到秒级
- 新建评论
NewComment := model.Comment{
UserID: userId,
VideoID: videoId,
Content: commentText,
CreateDate: time,
IsDeleted: 0,
UpdateDate: time,
}
- 将评论存入MySQL
//将评论存入MySQL中
dbWithTransaction, err := mysql.InsertComment(&NewComment)
if err != nil {
return nil, err
}
- 将评论存入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)
}
- 提交事务
dbWithTransaction.Commit() //提交事务
- 返回新建的评论
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
- 将MySQL中评论的状态改为删除状态
//修改mysql中评论的状态
dbWithTransaction, err := mysql.UpdateCommentStatus(commentId)
if err != nil {
return err
}
- 根据评论的id从MySQL中获取评论
comment, err := mysql.GetComment(commentId)
if err != nil {
return err
}
- 将reids中评论删除
//从redis中删除评论
key := tool.GetVideoCommentKey(comment.VideoID)
err = redis.RemoveComment(key, comment)
if err != nil {
dbWithTransaction.Rollback() //回滚事务
return err
}
- 提交事务
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)
- 从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
}