抖声设计篇| 青训营笔记

470 阅读10分钟

这是我参与「第五届青训营」笔记创作活动的第七天

极简抖音

开发设计篇

项目设计要求

一个基础功能两大方向:基础功能、互动方向、社交方向

  1. 基础功能

    • 视频Feed流:支持所有用户刷抖音,视频按照投稿时间倒序输出
    • 视频投稿:支持登录用户自己拍视频投稿
    • 个人主页:支持查看用户基本信息和投稿列表,注册用户流程简化
  2. 互动方向

    • 喜欢列表:登录用户可以对视频点赞,在个人主页喜欢Tab下能够查看点赞视频列表
    • 评论列表:支持未登录用户查看视频下的评论列表,登录用户能够发表评论
  3. 社交方向

    • 登录用户可以关注其他用户,能够在个人主页查看本人的关注数和粉丝数,查看关注列表和粉丝列表
    • 登录用户在消息页展示已关注的用户列表,点击用户头像进入聊天后可以发送消息

微服务方案

目前了解有两种方案

  1. go-zero
  2. hertz + kitex + gorm

都是非常简单实用、特性丰富的框架。开发模式基本差不多都可以选择,我偏向于hertz

微服务拆分

架构图的副本

api服务和各种rpc服务可以是单体或者集群,依据最终部署条件确定

  1. 会话服务

    • 实现所有服务功能的api
    • api层:调用各个rpc实现业务逻辑,对外提供接口
  2. 基础服务

    • 实现用户和视频的相关方法
    • 可能的表:用户、视频
    • rpc层:对内提供用户功能和视频功能的调用
    • 不需要调用其他rpc服务
  3. 互动服务

    • 实现互动方向的相关方法
    • 可能的表:用户点赞表——关联用户id与点赞视频id;评论表——评论用户、评论信息……(被评论主体只有视频,不需要树形结构)
    • 需要调用基础服务的rpc,获得用户信息视频信息等
  4. 社交服务

    • 实现社交方向的相关方法
    • 可能的表:关注与粉丝的表结构设计有多种,尽可能保证在搜索关注列表和粉丝列表都要高效,消息表
    • 需要调用基础服务的rpc,获得用户信息等

没有循环调用的rpc服务

如果所有服务本地都有代码,那么可以通过go module导入本地包

如果服务代码均在在不同人电脑上,那必须上传到github当中。关于rpc方法的调用源码可以通过go get获取 或者 直接复制代码到调用方

业务逻辑很简单,就不细说了

会话服务

api服务;提供所有功能的api,需要进行token认证;连接etcd,调用rpc,实现service部分的业务逻辑;

token说明:app自动重启会丢失token,登录过的用户会有一个token,用jwt生成,有效期不关心,测试时可以长一点,部署时可以短一点。token关键信息必须包含userId。

基础接口

syntax = "proto3";
package api.douyin.core;
option go_package="api/douyin/core";
​
import "api.proto";
​
message DouyinFeedRequest {
  optional int64 latest_time = 1[(api.query)="latest_time"]; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
  optional string token = 2[(api.query)="token"]; // 可选参数,登录用户设置
}
message DouyinFeedResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated Video video_list = 3; // 视频列表
  optional int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
}
​
message Video {
  int64 id = 1; // 视频唯一标识 18
  User author = 2; // 视频作者信息
  string play_url = 3; // 视频播放地址
  string cover_url = 4; // 视频封面地址
  int64 favorite_count = 5; // 视频的点赞总数
  int64 comment_count = 6; // 视频的评论总数
  bool is_favorite = 7; // true-已点赞,false-未点赞
  string title = 8; // 视频标题
}
message User {
  int64 id = 1; // 用户id
  string name = 2; // 用户名称
  optional int64 follow_count = 3; // 关注总数
  optional int64 follower_count = 4; // 粉丝总数
  bool is_follow = 5; // true-已关注,false-未关注
}
​
message DouyinUserRegisterRequest {
  string username = 1[(api.query)="username"]; //注册用户名,最长32个字符
  string password = 2[(api.query)="password"]; //密码,最长32个字符
}
​
message DouyinUserRegisterResponse {
  int32 status_code = 1; //状态码,0-成功,其他值-失败
  optional string status_msg = 2; //返回状态描述
  int64 user_id = 3; //用户id
  string token = 4; //用户鉴权token
}
​
message DouyinUserLoginRequest {
  string username = 1[(api.query)="username"]; // 登录用户名
  string password = 2[(api.query)="password"]; // 登录密码
}
message DouyinUserLoginResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  int64 user_id = 3; // 用户id
  string token = 4; // 用户鉴权token
}
​
message DouyinUserRequest {
  int64 user_id = 1[(api.query)="user_id"]; // 用户id
  string token = 2[(api.query)="token"]; // 用户鉴权token 7
}
message DouyinUserResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  User user = 3; // 用户信息
}
​
message DouyinPublishActionRequest {
  string token = 1[(api.body)="token"]; // 用户鉴权token
  bytes data = 2[(api.body)="data"]; // 视频数据
  string title = 3[(api.body)="title"]; // 视频标题
}
message DouyinPublishActionResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
}
​
message DouyinPublishListRequest {
  int64 user_id = 1[(api.query)="user_id"]; // 用户id
  string token = 2[(api.query)="token"]; // 用户鉴权token
}
message DouyinPublishListResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述 12
  repeated Video video_list = 3; // 用户发布的视频列表
}
​
​
service CoreService {
  // 视频流接口
  rpc FeedRequest(DouyinFeedRequest) returns(DouyinFeedResponse) {
    option (api.get) = "/douyin/feed";
  }
  // 用户注册接口
  rpc RegisterRequest(DouyinUserRegisterRequest) returns(DouyinUserRegisterResponse) {
    option (api.post) = "/douyin/user/register";
  }
  // 用户登录接口
  rpc LoginRequest(DouyinUserLoginRequest) returns(DouyinUserLoginResponse) {
    option (api.post) = "/douyin/user/login";
  }
  // 用户信息
  rpc UserRequest(DouyinUserRequest) returns(DouyinUserResponse) {
    option (api.get) = "/douyin/user";
  }
  // 视频投稿
  rpc PublishActionRequest(DouyinPublishActionRequest) returns(DouyinPublishActionResponse) {
    option (api.post) = "/douyin/publish/action";
  }
  // 发布列表
  rpc PublishListRequest(DouyinPublishListRequest) returns(DouyinPublishListResponse) {
    option (api.get) = "/douyin/publish/list";
  }
}

互动接口

syntax = "proto3";
​
package api.douyin.extra.first;
option go_package="api/douyin/extra/first";
​
import "api.proto";
​
message DouyinFavoriteActionRequest {
  string token = 1[(api.query)="token"]; //用户鉴权token
  int64 video_id = 2[(api.query)="video_id"]; //视频id
  int32 action_type = 3[(api.query)="action_type"]; //1-点赞,2-取消点赞
}
​
message DouyinFavoriteActionResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
}
​
message DouyinFavoriteListRequest {
  int64 user_id = 1[(api.query)="user_id"]; // 用户id
  string token = 2[(api.query)="token"]; // 用户鉴权token
}
​
message DouyinFavoriteListResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated Video video_list = 3; // 用户点赞视频列表
}
message Video {
  int64 id = 1; // 视频唯一标识
  User author = 2; // 视频作者信息
  string play_url = 3; // 视频播放地址
  string cover_url = 4; // 视频封面地址
  int64 favorite_count = 5; // 视频的点赞总数
  int64 comment_count = 6; // 视频的评论总数
  bool is_favorite = 7; // true-已点赞,false-未点赞
  string title = 8; // 视频标题
}
message User {
  int64 id = 1; // 用户id
  string name = 2; // 用户名称 29
  optional int64 follow_count = 3; // 关注总数
  optional int64 follower_count = 4; // 粉丝总数
  bool is_follow = 5; // true-已关注,false-未关注
}
​
message DouyinCommentActionRequest {
  string token = 1[(api.query)="token"]; // 用户鉴权token
  int64 video_id = 2[(api.query)="video_id"]; // 视频id
  int32 action_type = 3[(api.query)="action_type"]; // 1-发布评论,2-删除评论
  optional string comment_text = 4[(api.query)="comment_text"]; // 用户填写的评论内容,在action_type=1的时候使用
  optional int64 comment_id = 5[(api.query)="comment_id"]; // 要删除的评论id,在action_type=2的时候使用
}
message DouyinCommentActionResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  optional Comment comment = 3; // 评论成功返回评论内容,不需要重新拉取整个列表
}
message Comment {
  int64 id = 1; // 视频评论id
  User user =2; // 评论用户信息
  string content = 3; // 评论内容
  string create_date = 4; // 评论发布日期,格式 mm-dd
}
​
message DouyinCommentListRequest {
  string token = 1[(api.query)="token"]; // 用户鉴权token
  int64 video_id = 2[(api.query)="video_id"]; // 视频id
}
message DouyinCommentListResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated Comment comment_list = 3; // 评论列表
}
​
service InteractionService {
  //赞操作
  rpc FavoriteAction(DouyinFavoriteActionRequest) returns(DouyinFavoriteActionResponse) {
    option (api.post) = "/douyin/favorite/action";
  }
  //喜欢列表
  rpc FavoriteList(DouyinFavoriteListRequest) returns(DouyinFavoriteListResponse) {
    option (api.get) = "/douyin/favorite/list";
  }
  //评论操作
  rpc CommentAction(DouyinCommentActionRequest) returns(DouyinCommentActionResponse) {
    option (api.post) = "/douyin/comment/action";
  }
  //视频评论列表
  rpc CommentList(DouyinCommentListRequest) returns(DouyinCommentListResponse) {
    option (api.get) = "/douyin/comment/list";
  }
}

社交接口

syntax = "proto3";
​
package api.douyin.extra.second;
option go_package = "api/douyin/extra/second";
​
import "api.proto";
​
message DouyinRelationActionRequest {
  string token = 1[(api.query)="token"]; // 用户鉴权token
  int64 to_user_id = 2[(api.query)="to_user_id"]; // 对方用户id
  int32 action_type = 3[(api.query)="action_type"]; // 1-关注,2-取消关注
}
message DouyinRelationActionResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
}
​
message DouyinRelationFollowListRequest {
  int64 user_id = 1[(api.query)="user_id"]; // 用户id
  string token = 2[(api.query)="token"]; // 用户鉴权token
}
message DouyinRelationFollowListResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated User user_list = 3; // 用户信息列表
}
message User {
  int64 id = 1; // 用户id
  string name = 2; // 用户名称
  optional int64 follow_count = 3; // 关注总数
  optional int64 follower_count = 4; // 粉丝总数
  bool is_follow = 5; // true-已关注,false-未关注
}
​
message DouyinRelationFollowerListRequest {
  int64 user_id = 1[(api.query)="user_id"]; // 用户id
  string token = 2[(api.query)="token"]; // 用户鉴权token
}
message DouyinRelationFollowerListResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated User user_list = 3; // 用户列表
}
​
message DouyinRelationFriendListRequest {
  int64 user_id = 1[(api.query)="user_id"]; // 用户id
  string token = 2[(api.query)="token"]; // 用户鉴权token
}
message DouyinRelationFriendListResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated User user_list = 3; // 用户列表
}
​
message DouyinMessageChatRequest {
  string token = 1[(api.query)="token"]; // 用户鉴权token
  int64 to_user_id = 2[(api.query)="to_user_id"]; // 对方用户id
}
​
message DouyinMessageChatResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  repeated Message message_list = 3; // 消息列表
}
​
message Message {
  int64 id = 1; // 消息id
  int64 to_user_id = 2; // 该消息接收者的id
  int64 from_user_id =3; // 该消息发送者的id
  string content = 4; // 消息内容
  optional string create_time = 5; // 消息创建时间
}
​
message DouyinMessageActionRequest {
  string token = 1; // 用户鉴权token
  int64 to_user_id = 2; // 对方用户id
  int32 action_type = 3; // 1-发送消息
  string content = 4; // 消息内容
}
​
message DouyinMessageActionResponse {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
}
​
​
service SocietyService {
  //关系操作
  rpc RelationAction(DouyinRelationActionRequest) returns(DouyinRelationActionResponse) {
    option (api.post) = "/douyin/relation/action";
  }
  //用户关注列表
  rpc RelationFollowList(DouyinRelationFollowListRequest) returns(DouyinRelationFollowListResponse) {
    option (api.get) = "/douyin/relation/follow/list";
  }
  //用户粉丝列表
  rpc RelationFollowerList(DouyinRelationFollowerListRequest) returns(DouyinRelationFollowerListResponse) {
    option (api.get) = "/douyin/relation/follower/list";
  }
  //用户好友列表
  rpc RelationFriendList(DouyinRelationFriendListRequest) returns(DouyinRelationFriendListResponse) {
    option (api.get) = "/douyin/relation/friend/list";
  }
  //消息方案一
  //...不考虑,好像读取云端消息记录的说明
  //消息方案二
  //...使用消息方案二
  //聊天记录
  rpc MessageChat(DouyinMessageChatRequest) returns(DouyinMessageChatResponse) {
    option (api.get) = "/douyin/message/chat/";
  }
  //发送消息
  rpc MessageAction(DouyinMessageActionRequest) returns(DouyinMessageActionResponse) {
    option (api.post) = "/douyin/message/action/";
  }
}

基础服务

user 表

type User struct {
   gorm.Model
   Name     string `gorm:"size:256"`
   Password string `gorm:"size:256"`
}

video 表

type Video struct {
   gorm.Model
   UserId         uint
   PlayUrl        string `gorm:"size:500"`
   CoverUrl       string `gorm:"size:500"`
   
   Title          string `gorm:"size:50"`
}

实现对user表和video表的增删改查,提供增删改查的rpc调用,注册进etcd当中

互动服务

user_favourite 表——用户点赞表(用户-视频 1对多)

点赞是一个实时性的操作

在写操作非常频繁的情况下,可以先缓存到内存中,异步定时更新到mysql,减少对mysql数据库的压力

读写操作暂时放在redis中,异步定时更新到mysql

type UserFavourite struct {
  gorm.Model
  UserId  uint
  VideoId uint
  status  uint8 `gorm:"default:1"` //点赞 状态为1 取消赞状态为0
}

冷热数据物理存储分开存储的思路是对的,冷热数据读写特性不同(冷数据的读写比例高于热数据),分开储存之后可以采用不同的cache策略,冷数据因为更新少可以直接同步一份至redis这类NOSQL服务,业务层直接从redis读取,减少对mysqldb的压力;热数据因更新较频繁,可以根据用户id(或者说uin)hash到多台写服务,并先写至写服务器的本地缓存中,再异步定时批量更新至mysql,减少对mysql的写压力。

comment 表——评论表(只有对video主体对评论,没有对评论的评论)

type Comment struct {
   gorm.Model
   content    string `gorm:"size:256"`
   FromUserId uint
   ToVideoId uint
}

社交服务

relation 表——关注粉丝表

type Relation struct {
   gorm.Model
   FromUserId uint
   ToUserId   uint
   // FromUserId 关注了 ToUserId;
   // 查询关注列表 即 select to_user_id from relation where from_user_id = ? and rel_type 
   // 查询粉丝列表 即 select from_user_id from relation where to_user_id = ?
   RelType uint8 `gorm:"default:1"` //1为有效 0为无效
}

查询朋友不包括自己

message_chat 表——聊天表

type MessageChat struct {
   gorm.Model
   // FromUserId 给 ToUserId 发送的 MsgContent
   MsgContent string `gorm:size:256`
   FromUserId uint
   ToUserId uint
}