分布式文件服务系统架构与设计
该项目利用FastDFS分布式文件系统以及集群等技术提供统一的接口对外提供文件存储和访问服务,使用户能够通过网络访问服务端的文件资源,满足海量数据存储的需求
架构
项目采用B/S模型,服务端与浏览器进行交互,将用户的操作转换为HTTP请求发送给服务端,在服务端使用Nginx作为反向代理服务器进行请求转发,由FastCGI进程处理对应的请求,FastDFS实现文件的分布式存储。同时使用MySQL作为项目数据信息的存储,Redis作为缓存存储session与热点数据。
系统设计
MySQL设计
file_info(文件信息表)
字段名 | 字段类型 | 约束 | 字段说明 |
---|---|---|---|
id | bigint | PRIMARY KEY、AUTO_INCREMENT | 文件序号 |
md5 | varchar(256) | NOT NULL | 文件md5 |
file_id | varchar(256) | NOT NULL | 文件id,对应FastDFS的文件路径 |
url | varchar(512) | NOT NULL | 文件的完整存储路径:192.168.52.139:80/group1/M00/00/00/xxx.png |
size | bigint | 文件大小(字节) | |
type | varchar(32) | 文件类型(png\zip…) | |
count | int | 文件引用计数 |
share_file_list(共享文件列表)
字段名 | 字段类型 | 约束 | 字段说明 |
---|---|---|---|
id | int | PRIMARY KEY、AUTO_INCREMENT | 主键 |
user | varchar(32) | NOT NULL | 文件所属用户 |
md5 | varchar(256) | NOT NULL | 文件md5 |
file_name | varchar(128) | 文件名 | |
pv | int | DEFUALT 1 | 文件下载量 |
create_time | timestamp | 文件共享时间 |
user_file_count(用户文件数量)
字段名 | 字段类型 | 约束 | 字段说明 |
---|---|---|---|
id | int | PRIMARY KEY、AUTO_INCREMENT | |
user | varchar(128) | NOT NULL、UNIQUE | 文件所属用户、共享的文件 |
count | int | 拥有的文件数量 |
user_file_list(用户文件列表)
字段名 | 字段类型 | 约束 | 字段说明 |
---|---|---|---|
id | int | PRIMARY KEY、AUTO_INCREMENT | |
user | varchar(32) | NOT NULL | 文件所属用户 |
md5 | varchar(256) | NOT NULL | 文件md5 |
create_time | timestamp | 文件创建时间 | |
file_name | varchar(128) | 文件名 | |
shared_status | int | 共享状态 | |
pv | int | 文件下载量 |
user_info(用户信息)
字段名 | 字段类型 | 字段约束 | 字段说明 |
---|---|---|---|
id | int | PRIMARY KEY、AUTO_INCREMENT | 用户id |
user_name | varchar(32) | UNIQUE、NOT NULL | 用户名称 |
nick_name | varchar(32) | UNIQUE、NOT NULL | 用户昵称 |
password | varchar(32) | NOT NULL | 密码 |
phone | varchar(16) | NOT NULL | 手机号码 |
varchar(64) | 邮箱 | ||
create_time | timestamp | 时间 |
Redis设计
API设计
项目使用FastCGI进程处理对应的请求,FastCGI会一直阻塞等待客户端连接,当有客户端连接到来的时候就获取连接以及判断数据包是否合法,如果合法就解析数据进行业务处理,否则输出错误。
//阻塞等待用户连接
while (FCGI_Accept() >= 0) {
// 获取数据包内容长度
char *contentLength = getenv("CONTENT_LENGTH");
int len;
printf("Content-type: text/html\r\n\r\n");
if( contentLength == NULL ) {
len = 0;
} else {
len = atoi(contentLength); //字符串转整型
}
// 没有请求相关信息
if (len <= 0) {
printf("No data from standard input.<p>\n");
LOG(REG_LOG_MODULE, REG_LOG_PROC, "len = 0, No data from standard input\n");
} else {
// 解析数据包进行业务处理
}
reg_cgi.c注册
业务逻辑
- 获取请求数据包内容
- 解析收到的用户注册信息的json数据包得到注册用户的相关信息(用户名、密码、邮箱等)
- 查询数据库判断用户是否存在,如果存在则将注册状态设置为失败,否则向数据库中插入新用户记录同时将注册状态设置为成功
- 根据注册状态是否成功将信息返回给用户
login_cgi.c登录
业务逻辑
- 获取请求数据包内容
- 解析收到的用户注册信息的json数据包得到用户名和密码
- 通过数据库查询判断用户名与密码是否正确,如果错误则将登录状态设置为失败;否则生成token存储到Redis中,并将登录状态设置为成功
- 根据登录状态是否成功将相关信息返回给用户
token
相当于令牌,是服务端生成的一个字符串,用于验证客户端的身份,不需要在服务端存储用户的登录记录
流程:
- 客户端使用用户名和密码进行登录
- 登录成功后服务端根据一定的规则生成一个token并保存,同时将token发送给客户端
- 客户端收到token后也保存起来
- 后续客户端发送请求的时候就带上token
- 服务端收到请求验证token是否合法,如果合法就继续请求
生成规则:生成4个随机数并对随机数进行加密,再将加密后的随机数进行base64编码,最后转换为定长的MD5
服务端将token保存在Redis中同时设置有效期(24小时),key为用户名,value为token
base64
使用64个字符表示任意二进制数据的方法,防止不可见的编码
二进制文件中可能存在很多无法显示和打印的字符,这些字符在网络中传输的过程中经过的不同设备处理方式可能不同,导致不可见的字符可能被错误处理
需要先对数据进行编码转换为可见的字符(比如ASCII码)保证数据的可靠传输
Base64将每三个8bit的字节转换为四个6bit的字节,再在6bit添两位高位0组成四个8bit的字节,转换后的字符串比原来要长1/3
md5_cgi.c\upload_cgi.c上传文件
每个文件都有一个唯一的MD5值用于唯一的标识文件
客户端在上传文件之前将文件的MD5上传到服务器上,服务器判断是否已经存在MD5:如果存在说明文件已经存在无需再上传;如果不存在才真正上传文件
md5_cgi.c秒传文件
业务逻辑
- 获取请求数据包内容
- 进行收到的文件上传信息的json数据包得到token和MD5
- 验证token信息,如果验证失败则给用户返回错误信息;否则进行文件上传处理
- 通过数据库查询MD5对应文件是否存在
- 如果存在
- 通过数据库查询用户是否已经有此文件,如果有则上传成功
- 如果没有则将文件引用计数+1,同时向数据库中用户文件列表插入记录表示用户上传了某个文件,同时将用户文件数量表用户对应数量+1
- 如果存在
- 根据上传状态是否成功将相关信息返回给用户
对于用户来说MD5+文件名一样才是重复的文件,但是对于FastDFS来说MD5一样就是重复的文件
upload_cgi.c上传文件
业务逻辑
- 获取上传文件相关信息并保存到本地临时文件中(文件上传者、文件名、文件大小等)
- 将文件存储到FastDFS中并得到文件的file_id
- 删除本地临时存放的上传文件
- 获取文件所存放storaged的host_name并拼接处完整的HTTP地址
- 将文件信息存储到数据库中
- 根据上传状态是否成功将相关信息返回给用户
upload_to_dstorage将文件上传到FastDFS中
采用多进程方式,通过匿名管道在父子进程之间传输数据。由子进程读取本地临时文件中的内容上传到FastDFS中,获取到file_id写入管道中,父进程读取管道中的内容并回收子进程的资源
为什么采用多进程方式:防止上传文件过程中发生错误导致进程崩溃进而导致整个上传文件系统崩溃;采用多进程方式子进程在上传文件过程中崩溃了父进程还可以获取子进程崩溃信息等,不会影响到父进程,避免系统不可用
获取文件所存放storaged的host_name并拼接处完整的HTTP地址
采用多进程方式,通过匿名管道在父子进程之间传输数据
子进程利用fdfs_file_info进程读取文件的详细存储信息,获取storage的host_name并写入管道中
父进程读取管道中的信息拼接出完整的HTTP地址
myfiles_cgi.c获取用户文件列表
业务逻辑
- 获取URL地址"?"后面的cmd命令
- 根据不同的cmd命令执行不同的请求
- count获取文件个数
- 验证token
- 通过数据库查询用户文件个数
- 获取用户文件信息
- 验证token
- pvasc按下载量升序
- pvdesc按下载量降序
- count获取文件个数
- 将查询到的结果返回给用户
sharefiles_cgi.c获取分享文件
获取共享文件个数
- 获取URL地址"?"后面的cmd=count
- 通过数据库查询用户文件个数
获取共享文件列表
- 获取URL地址"?"后面的cmd=normal
- 通过数据库查询用户分享文件列表
获取共享文件排行榜
- MySQL中共享文件数量和Redis共享文件数量对比,判断是否相等
- 如果不相等则清空Redis数据,从MySQL中导入数据到Redis中
- 从Redis读取数据并将相应信息返回给用户
dealfile_cgi.c文件处理
删除文件
业务逻辑
- 判断文件是否已经分享
- 文件标识:MD5+文件名
- 首先在Redis的文件分享列表中查询是否有对应的文件,如果有说明已经分享该文件了
- 如果Redis中没有则到MySQL中查询,如果MySQL中查询用户是否有该文件
- 如果文件已经被分享则在MySQL中删除用户分享该文件的对应记录,同时共享文件数量-1,并在Redis中移除相应记录的信息(分享文件列表、文件hash)
- 在MySQL中删除用户文件列表的数据并使用户文件数量-1
- 当文件引用计数为0时在storage中删除此文件
分享文件
业务逻辑
- 判断此文件是否已经分享了
- 文件标识:MD5+文件名
- 首先在Redis的文件分享列表中查询是否有对应的文件,如果有说明已经分享该文件了,直接给用户返回结果
- 如果Redis中没有则到MySQL中查询该文件是否已经分享
- 如果有则在Redis中记录该文件的分享信息
- 如果没有则在在MySQL中更新文件状态已经共享文件数量,同时在Redis中记录该文件的分享信息
更新文件下载计数
- 查询文件下载计数同时更新
dealsharefile_cgi.c处理分享文件
取消分享
业务逻辑
- 在MySQL中将文件分享状态设置为不分享
- 查询用户共享文件数量
- >1则共享文件数量-1,否则删除共享文件数量对应的行
- 删除共享文件列表中对应的记录
- 在Redis中删除对应文件记录
转存文件
业务逻辑
- 在MySQL中查询用户文件列表中是否已经存在该文件
- 如果存在则返回结果
- 如果不存在则在文件列表中增加对应文件的引用计数,同时用户文件列表中增加对应的记录,更新用户文件数量
下载共享文件
业务逻辑
- 在MySQL中更新对应文件的下载次数
- 在Redis中更新对应文件记录
sharepicture_cgi.c处理图片分享
图片分享
业务逻辑
- 生成提取码:MD5(用户名、文件MD5、随机数)
- 将提取码和文件MD5插入数据库中同时更新分享图片数量
- 向用户返回提取码