本次项目我们小组使用Go语言中Gin框架实现了极简版抖音的后端搭建。 选择使用Gin框架的原因: 1、小组内除唐振宇外,三人均无Go语言基础与开发经验,需要从头开始学习Go语言。 2、在录播课中,Web三件套教学时间较短且Hertz框架文档并不详细,故使用生态比较完善、文档更广泛更全面的框架Gin。
项目已部署于服务器,服务地址为:https://tiktok.hymsk.asia/ 静态资源访问地址为:https://static.tiktok.hymsk.asia/
项目Github地址为:https://github.com/Hua-ymsk/TikTok/
团队成员 | 主要贡献 |
---|---|
凌鑫杰 | 队长,管理日常团队协作与总体项目进度,负责服务器配置与项目整体设计与项目部署,数据库设计以及演示视频的录制剪辑,主要负责接口:关注操作、用户关注列表、用户粉丝列表、用户好友列表。 |
唐振宇 | 队员,辅助整体架构搭建,初步实现项目所需中间件,主要负责接口:视频流、发布视频、发布列表 |
魏海林 | 队员,整体架构设计,项目的优化以及拓展,项目中间件的设计优化,接口测试,主要负责接口:用户登录,用户注册,获取用户信息。 |
郑浩天 | 队员,辅助整体架构搭建,分析安全问题并给出解决方案,项目功能测试,主要负责接口:赞操作、评论操作、发送消息操作、喜欢列表、视频评论列表、聊天记录 |
- 开发环境(基本情况,不同队员软硬件环境可能有一定不同)
硬件环境:CPU:Intel i7-10875H 2.30GHz,16G RAM,1T SSD 软件环境:Windows10 64位、Visual Studio Code、GoLand、go1.19.5 windows/amd64
- 生产环境
硬件环境:CPU:Intel(R) Xeon(R) 2.50GHz,4G RAM,80G SSD 软件环境:CentOS 7.6、Nginx 1.6.2、MySQL 8.0.16、go1.19.5 linux/amd64
- Golang
- Gorm
- gorm是由go编写,在github上活跃度很高的一个对象关系映射库,它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了,有助于编码效率提升 。gorm还支持预加载、事务、库自动迁移等功能。
- jwt-go
- jwt(json web token)是一种用于前后端身份认证的方法,一个jwt由header,payload,和signature组成。header:包含了token类型和算法类型;payload:包含了一些用户自定义或jwt预定义的一些数据;signature:将header和payload经过base64编码,加上一个secret密钥,整体经过header中的算法加密后生成。将jwt认证作为路由中间件注册,即可在处理请求前解析请求携带的token,获取user_id,并设置到上下文,便于之 后业务进行。
- zap
- 一个好的日志记录器应具有以下功能: 能够将事件记录到文件中,而不是应用程序控制台。 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。 支持不同的日志级别。例如INFO,DEBUG,ERROR等。 能够打印基本信息,如调用文件/函数名和行号,日志时间等。 本项目将zap logger作为全局中间件注册,既可以在panic时自动记录函数调用栈,帮助定位bug,也可 以主动调用记录错误
- Gorm
- MySQL MySQL是一个关系型数据库管理系统, 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS 应用软件之一。由于其体积小、速度快、总体拥有成本低,尤其是开源这一特点,故在本项目中使用MySQL作为项目数据库,具体数据库设计详见——3.3 数据库设计部分。
- Nginx Nginx 是一个高性能的 HTTP 和反向代理 Web 服务器,其与传统 Web 服务器相比速度更快、并发更高,单请求或者高并发请求的环境下,Nginx 都会比其他 Web 服务器响应的速度更快,配置简单,扩展性强,具有高可靠性的特点。 在本次项目实践中,使用Nginx作为代理服务器,进行配置SSL证书与域名访问,实现反向代理并作为静态资源服务器处理静态资源请求(提供视频服务)。
- ffmpeg FFmpeg是一套可以用来记录、转换数据音频、视频,并能将其转化为流的开源计算机程序。本项目使用ffmpeg来获取视频第一帧截图作为封面,保存到服务器上,再用nginx反向代理供外部访问。
根据E-R图,结合项目具体需求,本项目一共设计6张表,分别为users表、videos表、comments表、likes表、follows表、chats表。 由于本项目体量小,数据库较为简单,在数据库设计中,我们采用了使用触发器进行相对应表中对应数量字段的加减,进行级联更新,可以防止由于后端代码逻辑问题造成的一些错误,更快更高效的维护数据。 表相关具体设计如下:
字段 | 说明 |
---|---|
id | 自增主键 |
user_name | 唯一键,用户名 |
password | 使用MD5码进行加密加盐后得到的用户密码 |
nickname | 昵称 |
fans | 粉丝数 |
follows | 关注数 |
字段 | 说明 |
---|---|
id | 自增主键 |
user_id | 外键(依赖于users.id),发布视频的用户id |
video_url | 视频播放URL |
cover_url | 视频封面URL |
timestamp | 视频发布时间戳 |
title | 视频标题 |
likes_num | 视频点赞数 |
comments_num | 视频评论数 |
字段 | 说明 |
---|---|
id | 自增主键 |
user_id | 外键(依赖于users.id),发布评论的用户id |
video_id | 外键(依赖于videos.id),评论视频的视频id |
timestamp | 视频发布时间戳 |
content | 评论内容(内容小于1000字符) |
触发器:当comments表插入/删除时修改videos表对应视频comments_num字段
字段 | 说明 |
---|---|
id | 自增主键 |
user_id | 外键(依赖于users.id),点赞的用户id |
video_id | 外键(依赖于videos.id),被点赞视频的视频id |
联合唯一索引 | user_id&video_id |
触发器:当likes表插入/删除时修改videos表对应视频likes_num字段
字段 | 说明 |
---|---|
id | 自增主键 |
following_user_id | 外键(依赖于users.id),关注者用户id |
followed_user_id | 外键(依赖于users.id),被关注者用户id |
relationship | 好友标记,若为好友则为1,非好友则为0 |
联合唯一索引 | following_user_id&followed_user_id |
触发器:当follows表插入/删除时修改users表对应用户fans与follows字段
字段 | 说明 |
---|---|
id | 自增主键 |
send_user_id | 外键(依赖于users.id),发送信息者用户id |
receive_user_id | 外键(依赖于users.id),接受信息者用户id |
timestamp | 信息发送时间戳 |
content | 信息内容(内容小于1000字符) |
用户头像url、顶部大图url、个人简介(因涉及到的内容均无接口可进行更改都设定了初始值)
根据可选参数latest_time按时间返回指定数量的视频数据,若没有请求中没有latest_time,则将当前时间作为latest_time,latest_time的值应为每次返回视频列表最后一项的发布时间。当请求中有token时,还应检查该用户是否点赞视频。
用户注册时候,提供用户名和密码,然后对两个参数合法性的检验(用户是否存在,长度,是否为空),默认名称为用户名,然后对用户密码进行加盐加密,最后对用户进行创建,然后使用JWT中间件返回token,最后返回用户ID和Token以及响应码。
用户登录时,提供用户名和密码,然后对这两个参数进行合法性检验(长度,是否为空,用户是否存在),然后对密码进行加盐加密后匹配,最后返回用户ID和Token以及响应码。
调用这个接口的时候,传入Token和当前用户ID参数,首先会校验Token,然后对用户ID进行校验(用户是否存在,ID是否合法),然后通过查询当前用户与传入用户ID的关系,判断二者的关系,最终返回响应。
**注意:**此接口的token是由form-data携带,本项目jwt中间件默认token由query参数携带,所以要对jwt中间件获取token方式做兼容处理。 视频数据作为文件直接存在服务器中,由nginx反向代理为外部提供访问。使用ffmpeg为视频截图制作封面,图片的格式选择jpg比png更好,因为大小更小,截图速度快9倍左右。将jwt中间件解析出的user_id作为作者id,由controller层从上下文获取。
首先根据token中解析出的user_id获取该用户的视频列表,再遍历视频列表,获取每个视频的作者信息,和点赞信息。其中获取作者信息的同时,还要判断是否关注。(个人认为这个响应数据的设计不太合理,既然是一个用户的视频列表,那么作者信息查一次就可以了,没有必要video_list中每一项都去查作者信息)
注意:登录用户可以对视频点赞 执行赞操作之前首先通过中间件校验token,校验成功后通过GetInt64方法获取user_id,再根据action_type的值对指定的video_id进行点赞或者取消赞操作
- 赞操作:首先判断赞是否存在,存在则返回错误;不存在则执行点赞操作,只需在likes表中插入一条新信息即可
- 取消赞操作:首先判断赞是否存在,不存在则返回错误;存在则执行取消赞操作,因为前面已经判断赞是否存在,所以只需在likes表中删除该条信息
注意:is_follow字段表示是否关注,均指token所解析出的user_id对传递的user_id;且未登录的不能查看喜欢列表 首先使用中间件将token解析为user_id,然后根据传递的user_id通过gorm查询喜欢列表,并用JSON格式返回数据
注意:登陆的用户才能对视频进行评论 执行评论操作之前首先通过中间件校验token,校验成功后通过GetInt64方法获取user_id,再根据action_type的值对指定的video_id进行发布评论和删除评论操作
- 发布评论(返回comment_text)首先在comments表上添加一条评论信息,评论需要user_id、video_id、timestamp、content字段,插入成功后,返回comment_id给logic层,再查询评论用户信息,最后返回数据给controller层
- 删除评论(返回commen_id)直接根据comment_id删除评论信息,删除出错则返回错误信息,无需返回评论信息
注意:is_follow字段表示是否关注,均指token所解析出的user_id对传递的视频作者;且未登陆的用户可以查看评论列表
- 登陆的用户:首先使用中间件将token解析为user_id,然后根据传递的video_id通过gorm查询视频评论列表,用JSON格式返回数据
- 未登陆的用户:直接根据传递的video_id通过gorm查询视频评论列表,用JSON格式返回数据
执行操作之前,先通过中间件校验Token,然后对用户ID进行校验,再根据token中解析出的user_id与参数to_user_id进行后续操作(根据action_type值执行关注操作或取消关注操作)。
- 关注操作:用户执行关注操作时,先检查关注对象是否为自己,是则返回错误;执行关注,先查询是否有对方关注信息存在,若存在修改标记并插入一条新信息(以上两个操作作为一个事务);否则只插入一条新信息
- 取消关注:执行取消关注,先查询是否有对方关注信息存在,若存在修改标记并删除自己的关注信息(以上两个操作作为一个事务);否则只删除自己的关注信息
首先通过中间件校验Token,根据user_id参数值,通过数据库连接查询关注信息列表并格式化转换成JSON格式并返回数据。
首先通过中间件校验Token,根据user_id参数值,通过数据库连接查询得到粉丝信息列表并格式化转换成JSON格式并返回数据。
首先通过中间件校验Token,根据user_id参数值,通过数据库连接查询得到好友(即用户双方互相关注)信息列表并格式化转换成JSON格式并返回数据。 但是此处需要添加一个字段:用户头像url,因为用户头像在APP中无法更改,我们组使用的是静态图片,详见:【抖音大项目中一些坑】
首先查看聊天记录之前,需要将token通过中间件解析为user_id,然后根据传递的to_user_id和pre_msg_time字段的值,通过gorm查询在pre_msg_time之后的新消息信息,因为要进行轮询访问,所以只需要新的消息,获取数据之后转化为JSON格式并返回数据 开发中遇到了一些问题,主要是测试的时候发现对接不上,最后发现APP相关的API接口出现了一些问题,导致这个接口开发不是很顺畅,详见:【抖音大项目中一些坑】。
执行发送操作之前首先通过中间件校验token,校验成功后通过GetInt64方法获取user_id,再根据action_type的值对指定的video_id进行发送消息操作,因为只开放了发送消息,所以只需要根据传递的to_user_id的值和content的值,将token解析的用户编辑的消息发送给传递的to_user_id,并返回操作成功的数据
https://www.bilibili.com/video/BV1dg4y1p7L1/?vd_source=d8303f53ec80efe39094f58fdcd2db40
- 因为大部分成员无Go语言基础与开发经验,对相关知识不太了解,项目没有进行很好的优化
- 只使用了MySQL数据库,没有进一步在持久层加入使用缓存技术(如Redis),需要进一步优化
- 项目APP前端关于用户聊天记录轮询未基于id去重,会造成重复轮询
- 好友列表中用户头像无法更改,头像图片文件使用的是静态图片与静态url
- 持久层缓存
- 在查询业务添加redis缓存,缓解mysql数据库压力
- 缓存策略:只删除,不更新。一旦DB数据出现修改,就删除对应的缓存,而不去更新。
- 缓存内容:只缓存完整的行数据,不缓存行的部分数据,因为这样数据更新时无法定位哪些数据要删除
- 缓存处理方式:基于主键缓存
- for循环中协程处理业务 for循环中的业务处理开启协程处理
架构设计可以向微服务分布式架构方向发展,将分层结构更加细化,把每个功能都改变成微服务,在当前结构当中,如果某层出现问题,可能使这一层或者其他接口瘫痪,这样影响还是很大,把各个功能微服务化后,每个服务提供给需要的功能,如果一旦出现问题爆炸半径小,解耦程度高,独立扩展性更高,更具有通用性。
虽然项目的完成度很高,但是还是使用的单体架构,代码之间的业务很复杂,对一个功能进行拓展的时候至少会影响两个以上的功能,此外代码存在冗余,耦合、重构困难等情况,不过由于我们对golang基本都是0基础,所以很多东西只是设想去实现,但由于时间原因和熟练度原因就并没有去实现,比如使用redis非关系型数据库对点赞操作进行存储解决高并发的问题,使用消息队列对评论列表进行削峰处理等,但是我们都在尽全力的优化这个项目,让这个项目在我们力所能及的范围内达到最好。