这是我参与「第五届青训营」笔记创作活动的第七天
极简抖音
开发设计篇
项目设计要求
一个基础功能两大方向:基础功能、互动方向、社交方向
-
基础功能
- 视频Feed流:支持所有用户刷抖音,视频按照投稿时间倒序输出
- 视频投稿:支持登录用户自己拍视频投稿
- 个人主页:支持查看用户基本信息和投稿列表,注册用户流程简化
-
互动方向
- 喜欢列表:登录用户可以对视频点赞,在个人主页喜欢Tab下能够查看点赞视频列表
- 评论列表:支持未登录用户查看视频下的评论列表,登录用户能够发表评论
-
社交方向
- 登录用户可以关注其他用户,能够在个人主页查看本人的关注数和粉丝数,查看关注列表和粉丝列表
- 登录用户在消息页展示已关注的用户列表,点击用户头像进入聊天后可以发送消息
微服务方案
目前了解有两种方案
- go-zero
- hertz + kitex + gorm
都是非常简单实用、特性丰富的框架。开发模式基本差不多都可以选择,我偏向于hertz
微服务拆分
api服务和各种rpc服务可以是单体或者集群,依据最终部署条件确定
-
会话服务
- 实现所有服务功能的api
- api层:调用各个rpc实现业务逻辑,对外提供接口
-
基础服务
- 实现用户和视频的相关方法
- 可能的表:用户、视频
- rpc层:对内提供用户功能和视频功能的调用
- 不需要调用其他rpc服务
-
互动服务
- 实现互动方向的相关方法
- 可能的表:用户点赞表——关联用户id与点赞视频id;评论表——评论用户、评论信息……(被评论主体只有视频,不需要树形结构)
- 需要调用基础服务的rpc,获得用户信息视频信息等
-
社交服务
- 实现社交方向的相关方法
- 可能的表:关注与粉丝的表结构设计有多种,尽可能保证在搜索关注列表和粉丝列表都要高效,消息表
- 需要调用基础服务的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
}