【Golang实现】B站点赞功能的思考与简单实现

写在前面

本着学习的目的,我们来了解一下B站的点赞功能纠结是怎么做的?或者说我们应该如果实现一个点赞功能?
当然本人不是b站员工,也没有参与过b站的任何活动,所以我们就用自己的想法,如果是我们,怎么去实现日活过亿的app的点赞功能。

1. 需求分析

我们先明确一下需求,这个点赞功能最主要影响两点:

  • 观众与点赞的关系,也就是观众有没有对这个视频点过赞。直观影响是点赞按钮的样式。
  • 视频与点赞的关系,也就是这个视频有多少点赞。影响了这个视频的推荐程度。

明确了我们的需求之后,我们再来思考。如何从上亿用户中找到这个人是否看过这个视频?难道进行 io 吗?这显然是不行的!

在这里插入图片描述

2. 技术方案研究

2.1 观众与点赞的关系的实现

2.1.1 思路

既然直接io不行,那当然是上缓存啦!那么我们如何设置这个缓存的key呢?

我的想法是:维护一个用户的一个视频的点赞队列,其实对于b站来说,一个视频封顶就200w的点赞数,而b站的用户大多喜欢白嫖。

所以 这个 key可以这样设计:

video:like:user:{
    
    user_id}

Value 对应的就是用户已经点赞的视频的 id

用go实现就是这样:

func VideoUserLikeKey(userId int64) string {
    
    
	return fmt.Sprintf("video:like:user:%d", userId)
}	

2.1.2 存储

那么在 service 中我们只需要接受 user_id 和 视频 id 即可

videoInfo, err := json.Marshal(map[string]interface{
    
    }{
    
    
  "user_id":      req.UserId,
  "video_id":     req.VideoId,
  "created_time": req.CreatedTime,
})
if err != nil {
    
    
  log.LogrusObj.Infoln(err)
  return
}

// 推入redis,用户纬度,判断用户与video的纬度,保存用户最近点赞的500条
err = cache.GlobalRedisClient.LPush(cache.VideoUserLikeKey(req.UserId), req.VideoId).Err()
if err != nil {
    
    
  log.LogrusObj.Infoln(err)
  return
}

这样我们就保存好了 video 和 user 的关系了,我们判断完谁点赞,谁没点之后,直接查缓存就好了~

对于大部分人来说,其实对某一个视频只是推送的时候点进去看,然后再点赞,而一般都看完之后,再去搜这个视频,取消点赞。当然也有这种情况,那这种情况怎么做呢?其实就遍历一下最近存储的600个里面有没有,如果有的话,把这条删除就好了。 这个取消点赞的操作是远远比点赞少的。

当然前端应该也会有一些延迟发送,或延迟请求的优化,我不太清楚,但应该会有(,我们这里只注重后端。

那么我们解决完了这个第一个需求之后,我们再来看第二个需求。

2.2 视频与点赞的关系的实现

那如果更新这个视频的点赞数呢?难道直接数据库的值++吗?这肯定不可能,因为数据库连接的io是非常慢的,何况是写操作!

我们这里可以有一个延迟消费的情况,我们上一步不是已经把用户和点赞的关系存到缓存里面了吗?

我们这里异步一份,将这个消息,发送给MQ,然后MQ会积压,定时消费。

发送给MQ

// 视频纬度,rabbitmq 接受积压,累计消费, 推送 mq,定时改变video的点赞数
err = rabbitmq.SendMessage(ctx, video_consts.RabbitMqLikeQueue, videoInfo)
if err != nil {
    
    
  log.LogrusObj.Infoln(err)
  return
}

消费MQ,堆积在中间件中完成设置,这就是中间件团队的事情了,不关我们 curd boy 的事情~

func (s *VideoSync) RunVideoLike(ctx context.Context) error {
    
    
	rabbitMqQueue := video_consts.RabbitMqLikeQueue
	likeVideoInfo, err := rabbitmq.ConsumeMessage(ctx, rabbitMqQueue)
	if err != nil {
    
    
		return err
	}
	var forever chan struct{
    
    }

	go func() {
    
    
		for d := range likeVideoInfo {
    
    
			log.LogrusObj.Infof("Received run story like : %s", d.Body)

			// 落库
			reqRabbitMQ := new(video_types.LikeVideoReq)
			err = json.Unmarshal(d.Body, reqRabbitMQ)
			if err != nil {
    
    
				log.LogrusObj.Infof("Received run story like : %s", err)
			}

			err = service.LikeVideoMQ2MySQL(ctx, reqRabbitMQ)
			if err != nil {
    
    
				log.LogrusObj.Infof("Received run story like : %s", err)
			}

		}
	}()

	log.LogrusObj.Infoln(err)
	<-forever

	return nil
}

定时改变一次数据库,因为C端,其实数据并不是很重要,重要的是用户的体验 ,然后如果上面所说的。他取消了呢?同样的也是维护一个MQ,去定时消费,但统一减去一个值。

大致流程如下所示

image-20230315012919406

当然啦!实际的场景是比这个复杂非常多的,因为有非常多台机器,还有很多关于消息发送的丢失,重复消费,幂等性,读写分离,负载均衡等等的问题,我们只是简化了非常多。

猜你喜欢

转载自blog.csdn.net/weixin_45304503/article/details/129543643