字节后端青训营抖音项目汇报文档(打工魂小组)
tbghg

基本信息:本文档主要介绍2022字节跳动后端青训营抖音项目

时间:2022.05-2022.06

成员:田冰航、徐洪湘、向政昌

图片有点多,飞书这里不能直接引用,重新下载有点不方便,所以图片有所删减,建议移步:极简版抖音项目汇报文档(打工魂小组)

一、项目概要

1.1 项目仓库和成果展示

  1. 项目仓库地址:GitHub项目地址
  2. 视频演示:抖音演示视频.mp4

1.2 项目环境配置说明

1.2.1 项目使用

  1. 已将数据库部署于服务器上,也可根据表设计模块中给出的建表语句在本地创建数据库
  2. 启动Redis(非必须)
  3. ByteDance/pkg/common/config.go中填写相应配置项(也可使用当前默认配置)
  4. 安装依赖。在ByteDance目录下运行go mod tidy
  5. 运行。运行go build && ByteDance.exe,端口开放于8000

1.2.2 项目说明

  1. 视频模块中采用阿里云OSS对象存储
  2. 数据库部署在服务器中,但服务器性能较差
  3. 采用ffmpeg获取视频封面,ffmpeg.exe已同步上传项目,但对于windows以外的电脑需要提前安装ffmpeg
  4. Redis并不是启动项目所必须的,但缺省时会缺少限制频率的功能

1.3 成员分工

成员 分工
田冰航 数据库设计,项目结构设计,用户注册功能,获取视频流功能,上传视频功能,查看已发布视频功能
向政昌 Validate数据验证,敏感词过滤,Redis中间件限制频率,评论功能, 点赞功能,相关功能文档撰写
徐洪湘 JWT令牌功能实现,数据库设计,项目结构设计,关注功能,相关功能文档攥写

1.4 技术使用

  • Gin
  • Gen
  • MySQL
  • OSS
  • Git
  • Redis
  • JWT

二、功能实现

2.1 用户模块

  1. 注册操作

    • 使用使用Validate验证器对参数进行验证

    • 检测用户名是否已存在

    • 校验密码强度,要求用户名必须小于32位字符,密码大于8位小于32位字符,至少包含一位数字,字母和特殊字符

    • 对密码加盐后使用md5加密,写入数据库中

    • 将用户ID封装后返回

  2. 登录操作

    • 使用使用Validate验证器对参数进行验证

    • 对密码进行同样的加密,检测数据库中是否存在该记录且deleted为0

    • 更新登录时间

    • 将user_id作为payload通过JWT生成Token

    • 将Token和user_id进行封装并返回

  3. 获取用户信息

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行

      2. 验证成功:将user_id存入上下文中

    • 查询用户信息封装后返回

2.2 视频模块

  1. 获取视频流

    • 使用JWT中间件对Token进行验证

      1. 验证失败:不进行操作,继续进行
      2. 验证成功:将user_id存入上下文中
    • 内联查询最新发布的十个视频及作者信息

      1. 上下文中含有user_id:并发查询十个视频是否被该用户点赞,作者是否被该用户关注

      2. 上下文中不含user_id:默认为视频未点赞,作者未被关注

    • 封装数据返回信息

  2. 发布视频

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 从参数中获取视频及视频信息,从上下文中获取user_id

    • 采用雪花算法生成视频及封面名称作为标识

    • 使用ffmpeg截取视频第一帧作为封面

    • 将封面、视频上传至阿里云OSS中,截取并上传封面与上传视频二者并发进行

    • 将视频名称、封面名称及视频和作者的相关信息存入数据库中

    • 封装发布完成情况并返回

  3. 查看已发布视频

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 从上下文中获取user_id

    • 内联查询已发布的所有视频及本人信息

    • 封装数据返回信息

2.3 关注模块

  1. 关注操作

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 使用使用Validate验证器对参数进行验证

    • 根据所传入信息对数据库进行更新(已经关注过),如果数据库没有改条数据,则创建该数据(没有关注过)

    • 根据查询信息进行数据封装后返回

  2. 获取关注列表

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行

      2. 验证成功:将user_id存入上下文中

    • 使用使用Validate验证器对参数进行验证

    • 根据登录用户id使用联合索引获取关注用户id列表,并发查询关注用户名和关注总数和粉丝总数

    • 根据查询信息进行数据封装后返回

  3. 获取粉丝列表

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 使用使用Validate验证器对参数进行验证

    • 根据登录用户id使用联合索引获取粉丝用户id列表,并发查询粉丝用户名和关注总数和粉丝总数

    • 根据查询信息进行数据封装后返回

2.4 评论模块

  1. 评论操作

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 使用使用Validate验证器对参数进行验证

    • 根据所传入信息,若action_type为1且评论内容存在,数据库中创建该数据,若action_type为2且视频id存在对数据库进行更新(软删除)。

    • 根据查询信息进行数据封装后返回

  2. 获取评论列表

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 使用使用Validate验证器对参数进行验证

    • 根据视频id使用联合查询获取评论信息列表(包含评论用户信息)

    • 根据查询信息进行数据封装后返回

2.5 点赞模块

  1. 点赞操作

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 使用使用Validate验证器对参数进行验证

    • 根据所传入信息对数据库进行更新(已经点赞过,取消点赞),如果数据库没有该条数据,则创建该数据(没有点赞过,进行点赞)

    • 根据查询信息进行数据封装后返回

  2. 获取点赞列表

    • 使用JWT中间件对Token进行验证

      1. 验证失败:返回失败原因,阻止向下运行
      2. 验证成功:将user_id存入上下文中
    • 使用使用Validate验证器对参数进行验证

    • 根据登录用户id使用联合查询获取点赞视频信息列表(包含视频作者部分信息),然后并发查询视频作者关注总数、粉丝总数和是否已关注

    • 根据查询信息进行数据封装后返回

三、代码质量

3.1 MVC

  1. Request
    • 创建数据传输对象封装传入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type RegUserData struct {
ID int `json:"user_id"`
Token string `json:"token"`
}

type LoginData struct {
ID int `json:"user_id"`
Token string `json:"token"`
}

type GetUserInfoData struct {
ID int32 `json:"id"`
UseName string `json:"name"`
FollowCount int64 `json:"follow_count"`
FollowerCount int64 `json:"follower_count"`
IsFollow bool `json:"is_follow"`
}
  1. Response

    • sevice层封装返回数据的对象

    • controller层进一步封装数据对象,如添加返回响应信息,响应码等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户登录返回值
type loginResponse struct {
common.Response
user.LoginData
}

// 用户注册返回值
type regUserResponse struct {
common.Response
user.RegUserData
}

// 获取用户信息
type getUserInfoResponse struct {
common.Response
User user.GetUserInfoData `json:"user"`
}

// RegisterLoginRequest 注册 登录请求
type RegisterLoginRequest struct {
Username string `form:"username" validate:"required"`
Password string `form:"password" validate:"required"`
}
  1. controller

controller层只负责token验证,数据有效性验证,和数据请求的预处理和返回数据的封装,达到进一步解耦,职责明确

  1. service

service层只负责接受来自controller层的数据,所有的业务逻辑都在这里处理,并调用repository层中设计数据库的操作,最后封装来自数据库的数据,返回给controller层

  1. repository

repository层只负责对数据库的操作,比如DDL,DQL等,如果数据库出现问题,则可以直在repository层查找错误,利于后期维护

3.2 命名规范

  1. 模块命名

以user模块举例:

  • contoller:query_xxx_info.go
  • service命名query_xxx_info.go
  • repository层则是模块名xxx.go
  1. 函数命名:采用驼峰命名法

  2. 变量命名:根据英文释义采用驼峰命名法,可直观看出变量作用

3.3 常量与配置管理

  1. 常量管理

将所有消息规整在一个文件中,便于根据报错信息查找引用及时进行定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用户注册
const (
AlreadyRegisteredStatusMsg = "该用户名已被注册"
RegisterSuccessStatusMsg = "注册成功"
MatchFailedStatusMsg = "账号密码需要小于32字符,密码包含至少一位数字,字母和特殊字符"
)

// 用户登录
const (
WrongUsernameOrPasswordMsg = "用户名或密码错误"
LoginSuccessStatusMsg = "登陆成功"
AccountBlocked = "账号已被冻结"
)

// 获取用户信息
const (
UserIDNotExistMsg = "用户ID不存在"
GetUserInfoSuccessMsg = "获取用户信息成功"
)

// 等……
  1. 配置管理

将数据库、OSS和JWT相关配置项进行抽离,便于对项目进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const MySqlDSN = ""

// Redis 配置
const (
RedisLocalhost = "localhost:6379"
RedisPassword = ""
RedisDB = 0
)

// MD5Salt MD5加密时的盐
const MD5Salt = "UII34HJ6OIO"

// JWT
const (
Issuer = "xhx" // 签发人
MySecret = "F3Jfa5AD"
TokenExpirationTime = 14 * 24 * time.Hour * time.Duration(1) // Token过期时间
)

// OSSPreURL OSS前缀
const OSSPreURL = ""

// SensitiveWordsPath 敏感词路径
const SensitiveWordsPath = "./utils/SensitiveWords.txt"

3.4 日志记录

使用go.uber.org/zap进行日志管理,文件配置项如下:

1
2
3
4
5
6
7
8
//文件writeSyncer
fileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
Filename: "./logs/ByteDance.log", //日志文件存放目录
MaxSize: 1, //文件大小限制,单位MB
MaxBackups: 5, //最大保留日志文件数量
MaxAge: 30, //日志文件保留天数
Compress: false, //是否压缩处理
})

全局配置Log变量,记录四种类型的日志:Info、Warn、Error、Fatal

  • Info类型

MySQL、Redis、OSS初始化成功时输出

1
2
3
4
5
6
db, err = gorm.Open(mysql.Open(common.MySqlDSN))
if err != nil {
utils.Log.Fatal("数据库连接错误" + err.Error())
} else {
utils.Log.Info("MySQL连接成功")
}
  • Warn类型

Redis未连接时输出(未连接Redis会确实限制频率功能,当项目可以正常运行)

1
2
3
4
5
6
7
8
9
// 通过 *redis.Client.Ping() 来检查是否成功连接到了redis服务器
_, err := RedisDb.Ping().Result()
if err != nil {
utils.Log.Warn("Redis连接失败")
return false
} else {
utils.Log.Info("Redis连接成功")
return true
}
  • Error类型:程序正常运行时,不该出现的错误

数据库查询错误、类型转化错误等

1
2
3
4
5
6
7
8
9
// 文件上传日志记录
if fileType == "video" {
fileSuffix = ".mp4"
} else if fileType == "picture" {
fileSuffix = ".jpg"
} else {
Log.Error("无法上传" + fileType + "类型文件")
return false
}
  • Fatal类型:错误出现后,系统无法正常启动

MySQL连接失败

1
2
3
4
5
6
db, err = gorm.Open(mysql.Open(common.MySqlDSN))
if err != nil {
utils.Log.Fatal("数据库连接错误" + err.Error())
} else {
utils.Log.Info("MySQL连接成功")
}

四、项目设计

4.1 数据库设计

4.1.1 数据库ER图

image

4.1.2 数据库表

  1. user表
    • username,deleted联合索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create table user
(
id int auto_increment comment 'PK,直接自增'
primary key,
username varchar(32) not null comment 'UK,账号',
password varchar(32) not null comment '密码(MD5)',
enable tinyint default 1 null comment '账号是否可用',
deleted tinyint default 0 null comment '删除标识位',
login_time datetime default CURRENT_TIMESTAMP null,
create_time datetime default CURRENT_TIMESTAMP null comment '注册时间'
)
comment '用户表,储存用户信息';

create index user_username_deleted_index
on user (username, deleted);
  1. video表
    1. author_id,removed,deleted联合索引
    2. time,removed,deleted联合索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
create table video
(
id int auto_increment
primary key,
author_id int not null,
play_url varchar(32) not null,
cover_url varchar(32) not null,
time int not null,
title varchar(128) not null,
removed tinyint default 0 not null,
deleted tinyint default 0 not null,
constraint video_user_id_fk
foreign key (author_id) references user (id)
)
comment '存储视频信息';

create index video_author_id_removed_deleted_index
on video (author_id, removed, deleted);

create index video_time_removed_deleted_index
on video (time, removed, deleted);
  1. follow表
    1. user_id,removed,deleted联合索引
    2. fun_id,removed,deleted联合索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create table follow
(
id int auto_increment
primary key,
user_id int null,
fun_id int not null,
removed tinyint default 0 not null,
deleted tinyint default 0 not null,
constraint follow_user_id2fun_fk_2
foreign key (fun_id) references user (id),
constraint follow_user_id2user_fk
foreign key (user_id) references user (id)
)
comment '关注表';

create index follow_fun_id_removed_deleted_index
on follow (fun_id, removed, deleted);

create index follow_user_id_removed_deleted_index
on follow (user_id, removed, deleted);
  1. favorite表
    1. video_id,removed,deleted联合索引
    2. user_id,removed,deleted联合索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create table favorite
(
id int auto_increment
primary key,
video_id int not null,
user_id int not null,
removed tinyint default -1 not null,
deleted tinyint default 0 not null,
constraint favorite_user_id_fk
foreign key (user_id) references user (id),
constraint favorite_video_id_fk
foreign key (video_id) references video (id)
)
comment '用户视频点赞表';

create index favorite_user_id_video_id_removed_deleted_index
on favorite (user_id, video_id, removed, deleted);

create index favorite_video_id_removed_deleted_user_id_index
on favorite (video_id, removed, deleted, user_id);
  1. comment表
    1. user_id
    2. video_id,removed,deleted联合索引
    3. create_time,removed,deleted联合索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
create table comment
(
id int auto_increment
primary key,
user_id int not null,
video_id int not null,
create_time datetime default CURRENT_TIMESTAMP not null,
removed tinyint default 0 not null,
deleted tinyint default 0 not null,
content text not null,
constraint comment_user_id_fk
foreign key (user_id) references user (id),
constraint comment_video_id_fk
foreign key (video_id) references video (id)
)
comment '评论表';

create index comment_create_time_removed_deleted_index
on comment (create_time, removed, deleted);

create index comment_video_id_removed_deleted_index
on comment (video_id, removed, deleted);

4.2 项目结构设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
ByteDance
│ .gitignore
│ ffmpeg.exe // 截取视频第一帧
│ go.mod
│ Readme.md
│ router.go // 创建路由
│ server.go // 项目启动入口

├─cmd
│ ├─user
│ │ │ user_common_model.go // user模块中共用的结构体
│ │ │
│ │ ├─controller // 控制层,接受参数,编写流程逻辑,返回信息
│ │ │ query_user_info.go
│ │ │
│ │ ├─repository // 负责与数据库的交互
│ │ │ user.go
│ │ │
│ │ └─service // 处理流程中的主要函数
│ │ query_user_info.go
│ │
│ ├─comment // 其他模块与user模块结构相同
│ ├─favorite
│ ├─follow
│ └─video

├─dal // MySQL、Redis初始化
│ │ dal.go
│ ├─method
│ │ dal_common_method.go // 共用的查询方法
│ │ method.go // 自定义查询方法,用Gen生成
│ │
│ ├─model // Gen生成的数据模型
│ └─query // Gen生成的数据库操作方法

├─logs // 日志存放位置
├─pkg
│ ├─common
│ │ common.go // 模块公用部分
│ │ config.go // 配置项
│ │
│ ├─middleware // 中间件
│ │ middleware.go
│ │
│ └─msg // 定义返回消息
│ msg.go

└─utils // 工具类
│ jwt.go // 生成Token令牌
│ log.go // 日志生成
│ password.go // MD5加密,检测密码强度
│ SensitiveWords.txt // 项目
│ sensitive_word.go
│ snowflake.go // 雪花算法
│ upload_file.go // OSS中上传文件
└─generate
generate.go // Gen生成模块与方法

五、项目亮点

5.1 代码管理

使用Git进行分工合作,对版本进行控制 (1)版本迭代更加清晰 (2)提升开发效率 (3)利于代码review的实现,规范团队项目开发

5.2高性能

5.2.1 OSS对象存储

采用阿里云oss存储上传视频 (1)节省服务器空间,单独的文件管理界面,管理网站文件和本地电脑一样方便

5.2.2 雪花算法

采用雪花算法生成给上传的视频设置随机id (1)高性能:ID在内存生成,不依赖数据库 (2)高可用:ID在内存生成,不依赖数据库 (3)容量大:每秒能生成百万量级的ID

5.2.3 GEN自定义模型

首先进行表设计,将数据库部署在服务器上,通过Gen生成模型和查询方式,对常用查询方法采用自定义方法的方式 (1)简化代码且减少编码时间

5.2.4 MySQL索引

数据库表统一采用InnoDB存储引擎,索引结构默认B+树,通过主键索引,二级索引中遵循最左前缀法则对数据进行搜索 (1)主键索引也为聚集索引,聚集索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据 (2)二级索引也为非聚集索引,更新代价比聚集索引要小。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的,只存放主键信息或指针

5.2.5 单例模式

用init函数对每个模块的dao对象,数据库连接等进行初始化,放在内存当中,当要使用的时候,即可以从内存中直接取出,不用新建对象 (1)提供了对唯一实例的受控访问 (2)由于在系统内存中只存在一个对象,因此可以 节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能 (3)避免对共享资源的多重占用 (4)通过饿汉模式,在所有程序执行开始前被调用直接创建并初始化单例对象,所以并不存在线程安全的问题

5.2.6 并发编程

通过go语言的协程,优雅地进行并发编程,以此并发对数据库进行操作,大大提升其响应速度,因为MySQL的InnoDB引擎支持事务,所以不用担心起并发安全问题 (1)Goroutine所需要的内存通常只有2kb,而线程则需要1Mb,内存消耗更少 (2)由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低

5.3 安全问题

5.3.1 Token验证

采用JWT进行Token验证,中间件中对Token进行解析和判断,阻止Token不合法或已过期的请求,将token中携带的user_id信息写入 *gin.Context 声明的参数中供后续使用,考虑到前端未采用refresh_token进行刷新,将Token过期时间设置为14天 (1)解决了用户鉴权问题 (2)防止 CSRF攻击

5.3.2 防止SQL注入

根据所选框架为Gen,Gen 提供了自动同步数据表结构体到 GORM 模型,使用非常简单,即使数据库字段信息改变,可以一键同步,数据库查询相关代码可以一键生成,CRUD 只需要调用对应的方法,开发体验飞起 (1)GEN 采用了类型安全限制,所有参数都做了安全限制,完全不用担心存在注入 (2)自定义 SQL 只需要通过模板注释到 interface 的方法上,自动生成安全的代码SQL ,也不会出现SQL 注入问题

5.3.3 参数校验

采用Validate对请求参数进行合法性校验,阻止不符合参数要求的请求,二次避免SQL、XSS注入,防止通过发送请求对程序进破坏。

1
2
3
4
5
6
7
8
9
10
11
12
// FavoriteListResponse 点赞列表返回值
type FavoriteListResponse struct {
common.Response
VideoList []video.TheVideoInfo `json:"video_list"`
}

// FavoriteActionRequest 点赞与取消请求
type FavoriteActionRequest struct {
Token string `form:"token" validate:"required,jwt"`
VideoId int64 `form:"video_id" validate:"required,numeric,min=1"`
ActionType int32 `form:"action_type" validate:"required,numeric,oneof=1 2"`
}

5.3.4 密码安全

对参数进行校验,要求用户名必须小于32位字符,密码大于8位小于32位字符,至少包含一位数字,字母和特殊字符。 将用户密码加盐使用md5加密再存入数据库中确保用户密码的安全性

5.3.5 敏感词检测

采用go-wordsfilter对评论和发布视频请求中的文本信息进行敏感词检测,敏感词类型涉及黄赌毒、党政、欺骗消费者等方面。检测到敏感词后,阻止本次请求,并写入日志。

5.4 高可用

5.4.1 恶意请求限流

采用Redis记录请求ip,设置过期时间为5秒,同一ip5秒内访问超过100次的其余请求在中间件中被阻止,避免网站负载升高或者造成网站带宽阻塞而拒绝或无法响应正常用户的请求,防止通过发送请求对程序进破坏。

5.4.2 性能测试

测试工具:Apifox v2.1.16,Jmeter 采用Apifox进行测试集接口自动化,根据不同测试环境和测试数据,生成压力测试报告进行局部调优 假设实际运行开发中 以最大流量的视频流为例 40线程数 RT平均耗时为1.2s 吞吐量为37.3/min

image

image

 评论
评论插件加载失败
正在加载评论插件