字节跳动青训营-抖音项目

字节跳动青训营-抖音项目

一、项目介绍

“NoBugVideo”基于Gin Web框架,采用微服务架构,使用Kong集成Consul做服务发现,实现了“抖音”的基本功能,包括视频流的推送、视频投稿、用户的注册与登录,以及社交(用户之间的关注与聊天)与互动(用户对视频点赞及评论)等功能。

项目服务地址:http://124.221.120.88:8000

Github地址:https://github.com/xu-jq/simple-DY

我们团队实现了包括基础功能在内的两大方向:互动方向社交方向 ,根据项目考核的4个标准,自评如下:

评价项 实现情况
功能实现 微服务与其他资源能够正常运行,完全实现文档中定义的全部接口,边界情况处理良好
代码质量 项目结构清晰,包划分合理,代码符合编码规范
服务性能 数据表设置了合理的索引,代码中尽量使用并行处理提高性能
安全可靠 通过GORM框架防止SQL注入,通过JWT进行用户的认证,防止越权

二、项目分工

团队成员 主要贡献
@汪辉 开发社交模块,搭建Kong集成Consul做服务发现
@许珺琪 开发用户互动相关模块包括点赞评论等相关接口、搭建redis服务
@张兆 开发用户模块与视频模块相关接口、搭建MySQL、RabbitMQ等服务

三、项目实现

3.1 技术选型与相关开发文档

抖音上线于2016年9月26日,一开始是定位于专注于新生代的音乐创意短视频App,视频时常限制在15s内。年轻人比较爱赶新潮,乐于尝试新鲜事物,通过清晰明确定位在“潮流”“炫酷”“技术流”的方式,抖音吸引了第一批忠实粉丝。当产品功能逐渐完善后,抖音在运营方面开始发力,用户迎来大幅增长。抖音的主力用户群体年龄段上升,已经从早期的18岁到24岁,上升到了25岁到30岁用户。随着用户的快速增长,在内容层面也向着更加主流化、多元化的方向转变。

架构方面比较常见的有三种:

  1. 单体应用

所有的模块打包到一起部署运行,在开发小型项目上有独特优势:易于调试、部署,运维方便。缺点是容错性低,不可靠。只能通过运行更多的服务器水平扩展, 而不同的应用服务对资源的需求不同,且不可持续发展。

  1. SOA面向服务架构

面向服务架构是一种设计方法,设计上通常是自上而下的,服务间松散耦合。ESB集成不同协议的服务,做消息的转化、解释、路由从而联通各个服务,解决企业通信问题,服务松耦合、可扩展。缺点是SOA更多的面向企业服务,服务拆分粒度很大,更多的是为了复用。

  1. 微服务

微服务是去中心化的SOA的扩展,强调服务彻底的组件化,一个组件就是一个产品,服务切分力度更小,设计上更多的是自下而上的。服务间通过轻量级的协议进行通信,并根据服务本身需要独立化部署。从产品视角出发,更多聚焦可扩展性,兼顾可维护性。

综合上述几种服务的对比,我们最终选择了微服务架构,并使用下面的技术栈:

  • 分布式中间件:Consul
  • 网关:Kong
  • 数据库:MySQL
  • orm框架:GORM
  • 缓存:Redis
  • 消息队列:RabbitMQ
  • 对象存储:七牛云对象存储Kodo
  • Web框架:Gin
  • RPC 框架:GRPC
  • 数据传输协议:protobuf
  • 用户鉴权中间件:JWT
  • 配置文件:viper

3.1.1 需求分析

一、用户模块

用户模块包括用户注册、用户登录和用户信息三个部分。

  1. 用户注册接口 POST-/douyin/user/register/

新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token。

接口定义:

message douyin_user_register_request{
    string username = 1; // 注册用户名,最长32个字符
    string password = 2; // 密码,最长32个字符
}

message douyin_user_register_response{
    int32 status_code = 1; // 状态码,0-成功,其他值-失败
    string status_msg = 2; // 返回状态描述
    int64 user_id = 3; // 用户id
    string token = 4; // 用户鉴权token
}
  1. 用户登录接口 POST-/douyin/user/login/

通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token

接口定义:

message douyin_user_login_request{
    string username = 1; // 登录用户名
    string password = 2; // 登录密码
}

message douyin_user_login_response{
    int32 status_code = 1; // 状态码,0-成功,其他值-失败
    string status_msg = 2; // 返回状态描述
    int64 user_id = 3; // 用户id
    string token = 4; // 用户鉴权token
}
  1. 用户信息接口 GET-/douyin/user/

获取登录用户的 id、昵称,如果实现社交部分的功能,还会返回关注数和粉丝数。

接口定义:

message douyin_user_request{
    int64 user_id = 1; // 用户id
    string token = 2; // 用户鉴权token
}

message douyin_user_response{
    int32 status_code = 1; // 状态码,0-成功,其他值-失败
    string status_msg = 2; // 返回状态描述
    User user = 3; // 用户信息
}
二、视频模块

视频模块包括包括视频Feed流获取、视频投稿和获取用户投稿列表三个模块

  1. 视频流接口 GET-/douyin/feed/

不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个。

接口定义:

message douyin_feed_request{
    int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
    string token = 2;  // 可选参数,登录用户设置
}

message douyin_feed_response{
    int32 status_code = 1; // 状态码,0-成功,其他值-失败
    string status_msg = 2; // 返回状态描述
    repeated Video video_list = 3; // 视频列表
    int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
}
  1. 发布列表接口 GET-/douyin/publish/list/

登录用户的视频发布列表,直接列出用户所有投稿过的视频。

接口定义:

message douyin_publish_list_request{
    int64 user_id = 1; // 用户id
    string token = 2; // 用户鉴权token
}

message douyin_publish_list_response{
    int32 status_code = 1; // 状态码,0-成功,其他值-失败
    string status_msg = 2; // 返回状态描述
    repeated Video video_list = 3; // 用户发布的视频列表
}
  1. 视频投稿接口 POST-/douyin/publish/action/

登录用户选择视频上传。

接口定义:

message douyin_publish_action_request{
    string token = 1; // 用户鉴权token
    bytes data = 2; // 视频数据
    string title = 3; // 视频标题
}

message douyin_publish_action_response{
    int32 status_code = 1; // 状态码,0-成功,其他值-失败
    string status_msg = 2; // 返回状态描述
}
三、点赞模块
  1. 点赞操作接口 POST-/douyin/favorite/action/

登录用户对视频进行点赞与取消点赞操作。

接口定义:

message douyin_favorite_action_request {
   string token = 1; // 用户鉴权token
   int64 video_id = 2; // 视频id
   int32 action_type = 3; // 1-点赞,2-取消点赞
}

message douyin_favorite_action_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
}
  1. 点赞列表接口 GET-/douyin/favorite/list/

登录用户的所有点赞视频。

接口定义:

message douyin_favorite_list_request {
   int64 user_id = 1; // 用户id
   string token = 2; // 用户鉴权token
}

message douyin_favorite_list_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   repeated Video video_list = 3; // 用户点赞视频列表
}
四、评论模块
  1. 评论操作接口 POST-/douyin/comment/action/

登录用户对视频进行评论。

接口定义:

message douyin_comment_action_request {
   string token = 1; // 用户鉴权token
   int64 video_id = 2; // 视频id
   int32 action_type = 3; // 1-发布评论,2-删除评论
   string comment_text = 4; // 用户填写的评论内容,在action_type=1的时候使用
   int64 comment_id = 5; // 要删除的评论id,在action_type=2的时候使用
}

message douyin_comment_action_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   Comment comment = 3; // 评论成功返回评论内容,不需要重新拉取整个列表
}
  1. 视频评论列表接口 GET-/douyin/comment/list/

查看视频的所有评论,按发布时间倒序。

接口定义:

message douyin_comment_list_request {
   string token = 1; // 用户鉴权token
   int64 video_id = 2; // 视频id
}

message douyin_comment_list_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   repeated Comment comment_list = 3; // 评论列表
}
五、关注模块
  1. 关注操作接口 POST-/douyin/relation/action/

登录用户对其他用户进行关注或取消关注。实现用户之间的关注关系维护,登录用户能够关注或取关其他用户,同时自己能够看到自己关注过的所有用户列表,以及所有关注自己的用户列表。

接口定义:

message douyin_favorite_list_request {
   int64 user_id = 1; // 用户id
   string token = 2; // 用户鉴权token
}

message douyin_favorite_list_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   repeated Video video_list = 3; // 用户点赞视频列表
}
  1. 用户关注列表 GET-/douyin/relatioin/follow/list/

登录用户关注的所有用户列表。

message douyin_favorite_list_request {
   int64 user_id = 1; // 用户id
   string token = 2; // 用户鉴权token
}

message douyin_favorite_list_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   repeated Video video_list = 3; // 用户点赞视频列表
}
  1. 用户粉丝列表 GET-/douyin/relation/follower/list/

所有关注登录用户的粉丝列表。

message douyin_favorite_list_request {
   int64 user_id = 1; // 用户id
   string token = 2; // 用户鉴权token
}

message douyin_favorite_list_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   repeated Video video_list = 3; // 用户点赞视频列表
}
  1. 用户好友列表 GET-/douyin/relation/friend/list/

互相关注的用户列表。

message douyin_favorite_list_request {
   int64 user_id = 1; // 用户id
   string token = 2; // 用户鉴权token
}

message douyin_favorite_list_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
   string status_msg = 2; // 返回状态描述
   repeated Video video_list = 3; // 用户点赞视频列表
}
六、消息模块

客户端通过定时轮询服务端接口查询消息记录

  1. 聊天记录 GET-/douyin/message/chat/

当前登录用户和其他指定用户的聊天消息记录

message douyin_message_chat_request{
    required string token=1;//用户鉴权token
    required int64 to_user_id=2;//对方用户id
    required int64 pre_msg_time=3;//上次最新消息的时间
}

message douyin_message_chat_response {
    required int:32 status_code=1;//状态码,g-成功,其他值-失败
    optional string status._msg=2;//返回状态描述
    repeated Message message_list=3;//消息列表
}
message Message{
    required int64 id=1;//消息id
    required int64 to_user_id=2;//该消息接收者的d
    required int64 from_user_id=3;//该消息发送者的id
    required string content=4;//消息内容
    optional int64 create_time=5;//消息创建时间
}
  1. 消息操作 POST-/douyin/message/action/

登录用户对消息的相关操作,目前只支持消息发送

message douyin_relation_action_request{
    required string token=1;//用户鉴权token
    required int64 to_user_id=2;//对方用户id
    required int32 action_type=3;//1-发送消息
    required string content=4;//消息内容
}
message douyin_relation_action_response{
    required int32 status._code=1;//状态码,g-成功,其他值-失败
    optional string status_msg=2;//返回状态描述
}

3.2 架构设计

运行流程:

  1. 后端服务启动,根据注册中心consul的地址(1.1.1.1:8500),将自己注册到注册中心 。
  2. 客户端访问域名,根据解析找到kong网关地址(2.2.2.2:8000)。
  3. kong网关根据客户端传过来的服务名匹配到对应的Routes,再根据Routes找到对应的Service details 。
  4. 然后拿着Service details里面配置Host,去找consul地址(1.1.1.1:8600)。
  5. 根据名称查询consul的dns表,进而找到对应的ip+端口 。
  6. 找到对应的服务,然后通信。

3.2.1 用户模块

1. 整体架构设计

2. 详细设计
2.1. 用户注册

用户注册的逻辑比较简单,请求的参数中只包含用户的用户名与密码,不支持手机注册以及各种验证码操作。因此用户的唯一识别信息为用户名。如果数据库中存在相同的用户名,则认为这个用户已经存在,拒绝注册;否则则允许用户注册,并在数据库中分配给这个用户唯一的id。最后调用JWT生成Token返回响应,作为在Token生效期间的用户的唯一标识符。

用户注册流程:

  1. DY-api.UserRegister处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserRegister
  2. 服务端根据用户名查询数据库,如果发现重名用户名,则直接返回错误
  3. 未发现重名用户名,则通过md5加盐(用户名)对密码进行加密,加密后插入数据库,数据库返回唯一自增ID
  4. 服务端返回成功响应给DY-api.UserRegister
  5. DY-api.UserRegister利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
2.2. 用户登录

用户登录请求的参数中只包含用户的用户名与密码,不支持手机登录以及各种验证码操作。因此用户的唯一识别信息为用户名。如果数据库中不存在相同的用户名,则认为这个用户不存在,拒绝登录;否则则允许用户登录,并返回数据库中这个用户的唯一id。同时调用JWT生成Token返回响应,作为在Token生效期间的用户的唯一标识符。

用户登录流程:

  1. DY-api.UserLogin处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserLogin
  2. 服务端根据用户名查询数据库,如果未发现相同用户名,则直接返回错误,否则返回通过用户名查询出来的用户id和密码
  3. 对用户输入的密码进行md5加盐(用户名)加密,与上一步返回的密码进行比较,如果不匹配直接返回错误
  4. 密码匹配,则服务端返回成功响应给DY-api.UserLogin
  5. DY-api.UserLogin利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
2.3. 用户信息

用户信息请求的参数包括要请求的用户的id和当前登录的用户的Token。返回的用户信息应该包括用户的名称,用户的关注人数和粉丝人数,以及用户与当前登录用户的关注关系。因此除了调用DY-api.UserInfo获取用户的基本信息之外,还需要调用DY-srv.GetFollowList与DY-srv.GetFollowerList获取用户的关注人和用户的粉丝列表。两个Count数值可以通过查看切片的大小获得,关注关系需要遍历切片进行搜索。

在对不同的服务进行调用的时候采取并行调用的方式,服务全部返回后在api层进行拼接,从而提高效率。

用户信息流程:

  1. DY-api.UserInfo处理请求,将请求中带有的id字段传递到服务端DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList
  2. 并行请求三个服务,其中DY-srv.UserInfo根据id字段查询数据库,如果id有效,则返回用户姓名,否则返回错误
  3. 等待三个服务全部成功返回后,填充响应中的User的五个字段
    1. id与name字段通过DY-srv.UserInfo的响应直接获取
    2. followcount通过获取DY-srv.GetFollowList返回的切片长度获取
    3. followercount通过获取DY-srv.GetFollowerList返回的切片长度获取
    4. 通过Token获取当前的登录用户id,在DY-srv.GetFollowerList切片内部查询,如果查询到为True,否则为False
  4. 构建响应结构体并返回给客户端

3.2.2 视频模块

1. 整体架构设计

img

2. 详细设计
2.1. 视频流

获取视频流的请求参数包括视频的最新时间和当前用户的Token信息。如果当前用户在登录的状态下请求视频流,则通过最新时间在数据库中查询前30个视频的信息,包括视频本身的id和作者的id。获得最多30个视频的信息后,创建多个协程(视频数量个数),根据每个id获得视频的其他信息,如作者的详细信息,视频的点赞数量,作者的关注人数等等。在调用不同服务的时候也采用并行调用的方式。最后对返回的全部响应进行组织返回。

如果用户没有登录,则Token信息为空,那么返回的响应中缺少一些交互信息,如当前用户是否已经对当前视频点赞等等。

获取视频流流程:

  1. DY-api.Feed处理请求,准备请求服务

  2. 首先请求DY-srv.Feed服务,根据时间戳查询数据库,查询出不超过时间戳的前30个视频,查询后返回视频列表

  3. 随后并行请求视频列表中的每一个视频(即最大并发数为30)

  4. 对每一个视频,根据前一个服务响应的作者的id并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录Author响应相关的5个字段

  5. 对每一个视频,根据视频id并行请求DY-srv.douyinCommentList和DY-srv.douyinLikeVideo,对于每个视频

    1. commentCount通过获取DY-srv.douyinCommentList返回的切片长度获取
    2. favoriteCount通过获取DY-srv.douyinLikeVideo返回的切片长度获取
    3. 通过Token获取当前的登录用户id,在DY-srv.douyinLikeVideo切片内部查询,如果查询到为True,否则为False
  6. 等待全部的视频返回响应后,构建响应结构体并返回给客户端

2.2. 发布列表

获取用户视频发布列表的请求参数包括用户的id和当前用户的Token信息。两者不一定是相同的用户,因为用户在观看视频的同时点击用户头像即可以看到这个视频作者的信息和作者的视频发布列表。

如果当前用户是查看自己的视频发布列表,则通过用户的id在数据库中查询发布的视频的信息。获得最多视频的信息后,创建多个协程(视频数量个数),根据每个id获得视频的其他信息,如视频的点赞数量,作者的关注人数等等。在调用不同服务的时候也采用并行调用的方式。最后对返回的全部响应进行组织返回。

如果Token信息为空,则当前场景是用户查看其他用户的发布视频列表。那么返回的响应中缺少一些交互信息,如当前用户是否已经对当前作者的视频点赞等等。

获取视频发布列表流程:

  1. DY-api.PublishList处理请求,准备请求服务

  2. 首先请求DY-srv.PublishList服务,根据id查询数据库,如果id在数据库中不存在,则直接返回错误,然后根据用户id查询发布的视频列表并返回

  3. 随后并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录User响应相关的5个字段

  4. 对每一个视频,根据视频id并行请求DY-srv.douyinCommentList和DY-srv.douyinLikeVideo,对于每个视频

    1. commentCount通过获取DY-srv.douyinCommentList返回的切片长度获取
    2. favoriteCount通过获取DY-srv.douyinLikeVideo返回的切片长度获取
    3. 通过Token获取当前的登录用户id,在DY-srv.douyinLikeVideo切片内部查询,如果查询到为True,否则为False
  5. 等待全部的视频返回响应后,构建响应结构体并返回给客户端

2.3. 视频投稿

视频投稿的请求参数中包括用户的Token,上传的视频流数据以及视频的标题。其中视频流是用户从本地上传得到的,视频的标题是用户自行输入得到的。上传视频必须是在登录的状态下,因此必须包含用户的Token信息。获得参数后,根据Token信息解析出当前用户的id,然后根据用户id判断是否存在这个用户的文件夹。如果不存在文件夹则新建用户文件夹。创建文件夹后将视频流写入这个文件夹下的视频文件,同时调用ffmpeg对视频的封面进行截取从而获得视频的首图。确认视频文件与图片文件都保存在本地后,构建返回的响应,并将上传文件的消息推送到消息队列中,此时消息队列将视频文件和图片文件异步上传到对象存储当中,上传结束后将视频信息写入数据库,在下次请求视频流的过程中就可以请求到这个视频了。

其中使用RabbitMQ进行异步处理,在服务器带宽有限的情况下,上传视频对用户来说基本无感,增加了用户的体验。且上传到对象存储后视频和图片的展示和下载速度也会更快,方便用户查看视频。

视频投稿流程:

  1. DY-api.PublishAction处理请求,将请求中的字段传递到服务端DY-srv.PublishAction
  2. 服务端从Token中获取id信息,如果无法获取id,直接返回错误
  3. 服务端根据id信息查询数据库,获取用户信息,如果id并不存在于数据库,则直接返回错误
  4. 服务端判断本地存放视频与图片文件的文件夹是否存在,如果不存在则创建文件夹
  5. 服务端将接收到的请求中的字节流写入文件,并调用ffmpeg对视频的第一帧进行截图作为封面,同样写入图片文件
  6. 服务端将文件上传信息传递给消息队列,直接返回成功响应给客户端
  7. 消息队列接收到消息后并行上传视频和图片文件,两者都上传成功后将视频信息写入数据库

3.2.3 点赞模块

1. 整体架构设计

img

2. 详细设计
2.1 点赞操作

点赞操作分为对未点赞的视频点赞以及对已点赞的视频取消点赞。点赞操作接口的请求参数包括,用户token;视频id;操作类型(1–点赞,2–取消点赞)。通过解析用户token可获得用户id。构建一个redis集合,将用户已经点赞的视频将其按照k-v形式存入redis。

2.1.1 对视频点赞

当请求参数操作类型的值为1时,即为点赞操作,点赞操作是要对用户未点赞的视频进行点赞,首先在redis集合中查询该用户是否对此视频点赞过,若点赞过则返回视频已点赞,若未点赞,则将该条点赞记录先插入redis再插入数据库中,最后返回成功的响应码。

2.1.2 对视频取消点赞

当请求参数操作类型的值为2时,即为取消点赞操作,取消点赞操作是要对用户点赞的视频进行取消,首先在redis集合中查询该用户是否对此视频点赞过,若未点赞过则返回视频暂未点赞,若点赞了,则将该条点赞记录先从redis中删除再从数据库中删除,最后返回成功的响应码。

2.2 喜欢列表

喜欢列表接口的请求参数为用户id和用户token,先根据token验证用户身份与登录状态,若成功,则根据用户id查询用户的喜欢列表,将喜欢列表封装进响应结构体中,返回参数中还需要视频相关信息,通过调用视频服务接口,获取视频相关信息,并封装到响应结构体中,最终将响应结构体返回。

3.2.4 评论模块

1. 整体架构设计

img

2. 详细设计
2.1 评论操作

评论操作分为发表评论和删除评论,评论操作接口的请求参数包括用户token,视频id,操作类型(1–发表评论,2–删除评论),评论内容(发表评论时),评论id(删除评论时)。首先根据token验证用户身份与登录状态,若成功,则解析token获取用户id。

2.1.1 发表评论

当操作类型等于1时,表示是发表评论,将对应评论内容,用户id,视频id,添加进数据库,并且将评论列表封装进响应结构体,同时调用社交服务,获取对应的用户信息,将用户信息也封装进响应结构体,最后将其返回。

2.1.2 删除评论

当操作类型等于2时,表示是删除评论,将评论id对应的数据从数据库中删除,并返回删除成功的信息。

2.2 评论列表

评论列表接口的请求参数为视频id和用户token,先根据token验证用户身份与登录状态,若成功,则根据视频id查询视频的评论列表,将评论列表封装进响应结构体中,返回参数中还需要用户相关信息,通过调用社交服务接口,获取用户相关信息,并封装到响应结构体中,最终将响应结构体返回。

3.2.5 社交模块

社交模块的整体设计如下图:

其中 social-api程序是使用Gin框架搭建的Web服务。主要接受url请求,通过路由绑定handler处理函数,添加授权中间件。social-api部署了多个,并将自己注册在Consule服务上,支持负载均衡,并通过服务发现调用gRPC服务。

social-srv是业务处理代码,主要和MySQL数据库打交道。social-srv可以部署在多个不同服务器上,并将自己注册到Consul上来实现负载均衡,提供被其他服务发现。

详细设计:

  1. 关注模块

关注接口的请求参数为用户ID和被关注的用户ID,先根据token验证用户身份与登录状态,若成功,则向数据库插入数据,同时互相关注的用户会成为朋友,在朋友界面显示朋友列表,并展现最近的一条消息。用户也可以在信息详情页面来查看关注的用户和粉丝。

  1. 消息模块

通过用户ID和朋友ID可以新增一条消息。使用定时调用接口的方式来获取消息。

3.3 数据库设计

3.3.1 videos表

字段如下:

名称 类型 说明
id bigint 视频唯一id,自增主键
author_id bigint 视频作者id
file_name varchar 文件名称
publish_time bigint 发布时间
title varchar 视频标题

索引设置:

  1. 视频唯一id的自增主键索引
  2. 发布时间的索引,用户在数据库中查询指定时间范围的视频
  3. 作者id的索引,用于查询指定作者的视频列表

3.3.2 users表

名称 类型 说明
id bigint 用户id,自增主键
name varchar 用户名
password varchar 用户密码

索引设置:

  1. 用户id的自增主键索引
  2. 用户名与密码的联合索引,用于在数据库中匹配用户

3.3.3 comments表

名称 类型 说明
id bigint 评论唯一id,自增主键
user_id bigint 评论发布者的id
video_id bigint 评论发布位置的视频id
comment_text varchar 评论内容
create_time datetime 评论创建时间

索引设置:

  1. 评论id的自增主键索引
  2. 视频id的索引,用于在数据库中查询某条视频对应的评论内容

3.3.4 follows表

名称 类型 说明
id bigint 关注关系id,自增主键
user_id bigint 用户id
follower_id bigint 关注的用户id

索引设置:

  1. 关注关系id的自增主键索引
  2. 用户id和关注的用户id的联合索引,用于在数据库中查询两个用户之间的关注关系
  3. 关注的用户id索引,用于在数据库中查询用户的关注关系

3.3.5 likes表

名称 类型 说明
id bigint 喜欢关系id,自增主键
user_id bigint 点赞用户的id
video_id bigint 被点赞的视频id

索引设置:

  1. 喜欢关系id的自增主键索引
  2. 用户和点赞视频的联合索引,用于在数据库中查询某个用户是否对某个视频点赞
  3. 用户id索引,用于在数据库中查询某个用户的点赞的视频的id
  4. 视频id索引,用于在数据库中查询某个视频的点赞用户的id

3.3.6 messages表

名称 类型 说明
id bigint 消息唯一id,自增主键
user_id bigint 发送消息的用户id
to_user_id bigint 接收消息的用户id
sent_time datetime 消息发送时间
content varchar 消息内容

索引设置:

  1. 消息id的自增主键索引
  2. 发送用户id索引,用于查询数据库中指定的用户发送的消息
  3. 接收用户id索引,用于查询数据库中指定的用户接收的消息

3.4 项目代码介绍

后端项目总体分为两个大部分:

  1. web项目(simple-DY/DY-api/):使用Gin框架来获取用户请求,连接GRPC远程调用服务,最后返回数据。
  2. service项目(simple-DY/DY-srvs/):GRPC编写的微服务。

项目的总体结构如下所示:

├── simple-DY
│   ├── db.sql        // 数据库初始化文件
│   ├── DY-api           // web项目
│   │   ├── interact-web    // 互动模块
│   │   ├── social-web      // 社交模块
│   │   └── video-web       // 视频模块
│   ├── DY-srvs          // service项目
│   │   ├── interact-srv    // 互动模块
│   │   ├── social-srv      // 社交模块
│   │   └── video-srv       // 视频模块
│   ├── go.mod
│   ├── go.sum
│   └── README.md

3.4.1 video服务(包括视频模块和用户模块)

  1. api层

代码结构:

video-web
├── api
│   ├── base.go
│   ├── feed.go
│   ├── info.go
│   ├── otherapi.go
│   ├── publishaction.go
│   ├── publishlist.go
│   ├── userinfo.go
│   ├── userlogin.go
│   └── userregister.go
├── config
│   └── config.go
├── config-debug.yaml
├── config-pro.yaml
├── global
│   └── global.go
├── initialize
│   ├── config.go
│   ├── logger.go
│   ├── router.go
│   ├── srv_conn.go
│   └── validator.go
├── logs
│   └── video-web.log
├── main.go
├── middlewares
│   ├── cors.go
│   └── jwt.go
├── models
│   ├── base.go
│   ├── jwt.go
│   ├── other.go
│   ├── request.go
│   └── response.go
├── proto
│   ├── simpledy_grpc.pb.go
│   ├── simpledy.pb.go
│   └── simpledy.proto
├── README.md
└── utils
    └── consul
        └── register.go

详细说明:

  • api:编写路由的Handler处理函数
  • config:读取yaml文件时的接收结构体
  • *.yaml:配置文件
    • config-debug.yaml:线下开发使用的配置文件
    • config-pro.yaml: 线上配置文件
  • global:存放全局变量,例如config信息,连接信息等
  • initialize:初始化程序代码
    • config.go:读取配置文件
    • logger.go:日志配置
    • router.go:gin路由
    • srv_conn.go:连接微服务
    • validator.go:翻译器
  • logs:日志文件
  • main.go:主程序入口
  • middlewares:gin的自定义中间件
    • cors.go:跨域中间件
    • jwt.go:JWT中间件
  • models:用户请求参数的结构体
  • proto:编写和生成proto文件
  • README.md:说明文件
  • utils:工具类
    • consul:调用consul api进行服务注册发现等操作
  1. srv层

代码结构:

.
├── config
│   └── config.go
├── config-debug.yaml
├── config-pro.yaml
├── global
│   └── global.go
├── handler
│   ├── base.go
│   ├── feed.go
│   ├── publishaction.go
│   ├── publishlist.go
│   ├── userinfo.go
│   ├── userlogin.go
│   ├── userregister.go
│   └── videoinfo.go
├── initialize
│   ├── config.go
│   ├── db.go
│   ├── handler.go
│   └── logger.go
├── logs
│   └── video-srv.log
├── main.go
├── models
│   ├── base.go
│   └── db.go
├── proto
│   ├── simpledy_grpc.pb.go
│   ├── simpledy.pb.go
│   └── simpledy.proto
├── README.md
└── utils
    ├── backup
    │   └── backup.go
    ├── consul
    │   └── register.go
    ├── dao
    │   ├── followdao.go
    │   ├── userdao.go
    │   └── videodao.go
    ├── ffmpeg
    │   └── extractFirstFrame.go
    ├── freeport
    │   └── port.go
    ├── jwt
    │   └── token.go
    ├── md5salt
    │   └── md5.go
    ├── oss
    │   └── upload.go
    └── rabbitmq
        ├── base.go
        ├── consumer.go
        └── producer.go

详细说明:

  • config:读取yaml文件时的接收结构体
  • *.yaml:配置文件
    • config-debug.yaml:线下开发使用的配置文件
    • config-pro.yaml: 线上配置文件
  • global:存放全局变量,例如config信息,连接信息等
  • handler:主要的逻辑代码,proto的service的实现类
  • initialize:初始化程序代码
    • config.go:读取配置文件
    • db.go:数据库全局连接
    • handler.go:监听客户端连接
    • logger.go:日志配置
  • logs:日志文件
  • main.go:主程序入口
  • models:用户请求参数的结构体
  • proto:编写和生成proto文件
  • README.md:说明文件
  • utils:工具类
    • backup:备份用户上传的视频和图片文件
    • consul:调用consul api进行服务注册发现等操作
    • dao:数据库相关操作
    • ffmpeg:视频首页截图
    • freeport:获取空闲网络端口
    • jwt:鉴权Token的生成与解析
    • md5salt:密码加密存储
    • oss:七牛云对象存储相关操作
    • rabbitmq:消息队列相关操作

3.4.2 interact服务(包括点赞模块和评论模块)

  1. api层
interact-web
├── api
│   ├── base.go
│   ├── comment.go
│   └── like.go
├── config
│   └── config.go
├── global
│   └── global.go
├── initialize
│   ├── config.go
│   ├── logger.go
│   ├── router.go
│   ├── srv_conn.go
│   └── validator.go
├── main.go
├── middlewares
│   ├── cors.go
│   └── jwt.go
├── models
│   └── request.go
├── proto
│   ├── simpledy_grpc.pb.go
│   ├── simpledy.pb.go
│   └── simpledy.proto
├── router
│   ├── comment.go
│   └── like.go
└── utils
    └── register
        └── consul
            └── register.go
  1. srv层
interact-srv
├── build.sh
├── config
│   └── config.go
├── global
│   └── global.go
├── handler
│   └── interact.go
├── initalize
│   ├── config.go
│   ├── db.go
│   ├── logger.go
│   ├── rdb.go
│   └── srvs_conn.go
├── main.go
├── model
│   ├── base.go
│   ├── comment.go
│   ├── like.go
│   └── video.go
├── proto
│   ├── simpledy_grpc.pb.go
│   ├── simpledy.pb.go
│   └── simpledy.proto
└── utils
    ├── addr.go
    ├── jwt
    │   └── token.go
    ├── key
    │   └── key.go
    └── register
        └── consul
            └── register.go

3.4.3 social服务(包括关注模块和消息模块)

  1. api层
social-web
├── api
│   ├── base.go
│   ├── message.go
│   └── relation.go
├── config
│   └── config.go
├── config-debug.yaml
├── config-pro.yaml
├── forms
│   ├── message.go
│   └── relation.go
├── global
│   └── global.go
├── initialize
│   ├── config.go
│   ├── logger.go
│   ├── router.go
│   ├── srv_conn.go
│   └── validator.go
├── main.go
├── middlewares
│   ├── cors.go
│   └── jwt.go
├── models
│   └── request.go
├── proto
│   ├── simpledy_grpc.pb.go
│   ├── simpledy.pb.go
│   └── simpledy.proto
├── router
│   ├── message.go
│   └── relation.go
└── utils
    ├── addr.go
    └── register
        └── consul
            └── register.go
  1. srv层
social-srv
├── build.sh
├── config
│   └── config.go
├── config-debug.yaml
├── config-pro.yaml
├── global
│   └── global.go
├── handler
│   └── social.go
├── initialize
│   ├── config.go
│   ├── db.go
│   └── logger.go
├── main.go
├── model
│   └── base.go
└── proto
    ├── simpledy_grpc.pb.go
    ├── simpledy.pb.go
    └── simpledy.proto

四、测试结果

4.1 功能测试

通过Apifox的自动化测试,构建不同实际使用中可能遇到的情况,对接口进行充分测试。

1. 用户注册接口 /douyin/user/register/

需要对如下的用例进行测试:

  1. 注册不存在的用户名-返回成功响应
  2. 注册已经存在的用户名-返回失败响应

测试结果:

img

2. 用户登录接口 /douyin/user/login/

需要对如下的用例进行测试:

  1. 登录已经存在的用户名且密码正确-返回成功响应
  2. 登录不存在的用户名-返回失败响应
  3. 登录已经存在的用户名,但是密码错误-返回失败响应

测试结果:

img

3. 用户信息接口 /douyin/user/

需要对如下的用例进行测试:

  1. 用户id存在且Token正确-返回成功响应
  2. 用户id存在但Token为空或不正确-返回成功响应(但是没有是否关注与是否点赞等关系信息)
  3. 用户id不存在-返回失败响应

测试结果:

img

4. 视频流接口 /douyin/feed/

需要对如下的用例进行测试:

  1. 未登录用户请求视频流(包括Token错误的情况)-返回成功响应(但是缺少是否对视频点赞等关系信息)
  2. 登录用户请求视频流-返回完整的成功响应

测试结果:

img

5. 发布列表接口 /douyin/publish/list/

需要对如下的用例进行测试:

  1. 用户id存在且Token正确-返回成功响应
  2. 用户id存在但Token为空或不正确-返回成功响应(但是没有是否点赞等关系信息)
  3. 用户id不存在-返回失败响应

测试结果:

img

6. 视频投稿接口 /douyin/publish/action/

需要对如下的用例进行测试:

  1. 正常上传视频-返回成功响应
  2. Token为空或Token不正确-返回错误响应

测试结果:

img

7. 社交模块

img

8. 互动模块

4.2 性能测试

五、其他资料

接口文档(旧版)

汇报文档

课程汇总

抖音项目方案说明

极简抖音App使用说明

青训营大项目答疑

六、项目总结与反思

1. 目前仍存在的问题

  • 在视频模块中,上传视频的大小有限制,如果超过了限制会返回网络错误,无法将视频字节流传递到服务器端。
  • 观看视频时,一个服务器的宽带顶不住,有点卡。
  • 获取消息的API由于是定时查询,消息会重叠。
  • 若出现对短时间内一个视频进行大量点赞操作,写入数据库操作会太频繁,可以考虑将点赞记录进行定期写入数据库。

2. 已识别出的优化项

  • 视频模块中可以对用户的视频习惯进行分类,每一次获取视频流的时候对用户进行视频推荐
  • 用户模块可以增加邮箱或手机号等验证方式,并添加密码找回的功能,增加安全性
  • 粉丝列表、用户的聊天记录、关注列表和朋友列表可以使用Redis的List数据结构来存储,来降低MySQL的压力
  • 用户聊天的消息推送可以使用websocket长连接来避免每次建立链接释放链接所消耗的资源。
  • 用户聊天的消息推送可以使用MQ消息队列来实现,不查表可以减低MySQL压力和消息的实时性。
  • 点赞功能将点赞记录存在redis中,减少数据库查询压力。

3. 架构演进的可能性

  • 微服务基本根据路由进行拆分,拆分不够合理,服务之间耦合的地方稍多。后续可以将微服务进行进一步拆分,真正做到将所有的功能打包成独立的单元。
  • 可以从微服务架构演进为Serverless。Serverless是一种构建和管理基于微服务架构的完整流程,允许你在服务部署级别而不是服务器部署级别来管理你的应用部署。它与传统架构的不同之处在于,完全由第三方管理,由事件触发,存在于无状态(Stateless)、暂存(可能只存在于一次调用的过程中)计算容器内。构建无服务器应用程序意味着开发者可以专注在产品代码上,而无须管理和操作云端或本地的服务器或运行时。Serverless真正做到了部署应用无需涉及基础设施的建设,自动构建、部署和启动服务。

4. 项目过程中的反思与总结

在参加青训营期间,官方提供了全面的课程,涵盖了创作技巧、内容制作、问题分析等多个方面。这些课程不仅提供了实用的知识和技能,还可以让我们更好地理解抖音平台和用户需求。抖音青训营项目还提供了多种资源支持,包括专业导师、团队合作等。这些资源可以帮助我们更好地实践和落地自己的创意。

回顾整个项目的过程,我们团队做了如下总结:

  • 在代码编写的过程中,保持良好的编码规范不仅对自己以后复习代码节省时间,同事对代码的理解也会更方便。
  • 在实践中学习新的知识和技能。
  • 好记性不如烂笔头。伴学笔记的习惯值得我们继续保持。
  • 在协作开发中,团队的活力来源于不断的交流。通过交流和合作,我们学到了很多新的创作思路和理念。

七、参考资料

https://grpc.io/

https://www.jianshu.com/p/4e4ff6be6af9

https://www.apifox.cn/apidoc/shared-09d88f32-0b6c-4157-9d07-a36d32d7a75c/api-50707523

https://juejin.cn/post/7174037539345399839

https://blog.csdn.net/cc18868876837/article/details/90672971

https://www.woshipm.com/evaluating/1552722.html


字节跳动青训营-抖音项目
https://zhangzhao219.github.io/2023/03/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project/
作者
Zhang Zhao
发布于
2023年3月3日
许可协议