第二章 打造高性能的视频系统
视频与弹幕功能开发概要
- FastDFS文件服务器搭建、相关工具类开发
- 视频上传、视频处理、视频获取、视频在线播放、视频下载
- 弹幕系统、数据统计、社交属性(点赞、投币、收藏、评论)
FastDFS文件服务器
什么是 FastDFS ?
- 开源的轻量级分布式文件系统,用于解决大数据量存储和负载均衡等问题。
- 优点:支持HTTP协议传输文件(结合Nginx);对文件内容做Hash处理,节约磁盘空间;支持负载均衡、整体性能较佳。
- 适用系统类型:中小型系统
FastDFS 的两个角色是什么 ?
- FastDFS的二个角色:跟踪服务器(Tracker)、存储服务器(Storage)
- 跟踪服务器:主要做调度工作,起到负载均衡的作用。它是客户端和存储服务器交互的枢纽
- 存储服务器:主要提供容量和备份服务,存储服务器是以
组
(Group)为单位,每个组内可以有多台存储服务器,数据互为备份。文件及属性(Meta Data)都保存在该服务器上
FastDFS 架构图
Nginx
- Nginx是反向代理服务器。代理其实就是中间人,客户端通过代理发送请求到互联网上的服务器,从而获取想要的资源。
- Nginx的主要用途:反向代理、负载均衡。
- Nginx的主要特点:跨平台、配置简单易上手、高并发、内存消耗小、稳定性高。
Nginx 正向代理的特点:
服务端不知道客户端、客户端知道代理端
Nginx 反向代理的特点:
服务端知道客户端、客户端不知道代理端
Nginx 结合 FastDFS 实现文件资源HTTP访问
断点续传
大文件上传的痛点是什么呢 ?
- 如果文件过大,会导致带宽紧张,请求速度下降
- 如果上传过程当中,服务中断或者是网络中断、页面崩溃等等情况,导致文件上传失败了;如果我们上传的是大文件的话,需要重新上传,这个过程是非常让人崩溃的!
什么是断点续传 ?
将大文件进行分片,分片的意思
演示文件分片
FastDFSUtil.java
@Component
public class FastDFSUtil {
@Autowired
private FastFileStorageClient fastFileStorageClient;
// 支持断点续传的依赖
@Autowired
private AppendFileStorageClient appendFileStorageClient;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 默认是组1
private static final String DEFAULT_GROUP = "group1";
private static final String PATH_KEY = "path-key:";
private static final String UPLOADED_SIZE_KEY = "uploaded-size-key:";
private static final String UPLOADED_NO_KEY = "uploaded-no-key:";
// 每个分片的大小
private static final int SLICE_SIZE = 1024 * 1024 * 2;
/**
* 获取文件的类型
*
* @param file
* @return
*/
public String getFileType(MultipartFile file) {
if (file == null) {
throw new ConditionException("非法文件!");
}
String fileName = file.getOriginalFilename();
// 获取文件名称最后一个"."的位置,这样子可以根据"."的位置截取出文件的类型
int index = fileName.lastIndexOf(".");
// 获取文件的类型
String fileType = fileName.substring(index + 1);
return fileType;
}
/**
* 上传文件
*
* @param file
* @return
* @throws Exception
*/
public String uploadCommonFile(MultipartFile file) throws Exception {
Set<MetaData> metaDataSet = new HashSet<>();
String fileType = this.getFileType(file);
StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileType, metaDataSet);
// 返回文件在服务器的路径
return storePath.getPath();
}
/**
* 上传可以断点续传的文件
*
* @param file
* @return
* @throws IOException
*/
public String uploadAppenderFile(MultipartFile file) throws Exception {
String fileName = file.getOriginalFilename();
String fileType = this.getFileType(file);
StorePath storePath = appendFileStorageClient.uploadAppenderFile(DEFAULT_GROUP, file.getInputStream(), file.getSize(), fileType);
// 返回文件在服务器的路径
return storePath.getPath();
}
/**
* 修改可以断点续传的文件
*
* @param file
* @param filePath 文件的路径
* @param offset 从文件的哪个位置开始添加
*/
public void modifyAppenderFile(MultipartFile file, String filePath, long offset) throws Exception {
appendFileStorageClient.modifyFile(DEFAULT_GROUP, filePath, file.getInputStream(), file.getSize(), offset);
}
/**
* 根据文件路径删除文件
*
* @param filePath
* @return
*/
public void deleteFile(String filePath) {
fastFileStorageClient.deleteFile(filePath);
}
// --------------------------- 演示文件分片的具体流程 ---------------------------
/**
* 通过文件分片来上传一个文件
*
* @param file
* @param fileMd5 文件经过md5加密后,可以形成它唯一的一个标识符,可以进行秒传功能的开发,也可以用于redis的key
* @param sliceNo 当前要上传的分片是第几片
* @param totalSliceNo 总共要上传的分片数
* @return
*/
public String uploadFileBySlices(MultipartFile file, String fileMd5, Integer sliceNo, Integer totalSliceNo) throws Exception {
if (file == null || sliceNo == null || totalSliceNo == null) {
throw new ConditionException("参数异常!");
}
// 分片上传之后系统返回的文件路径
String pathKey = PATH_KEY + fileMd5;
// 当前已经上传的分片加起来的总大小
String uploadedSizeKey = UPLOADED_SIZE_KEY + fileMd5;
// 目前一共上传了多少个分片,和总分片数进行比对;如果相同说明已经完成了所有分片的上传
String uploadedNoKey = UPLOADED_NO_KEY + fileMd5;
// 先判断当前已经上传的文件分片的大小
String uploadedSizeStr = redisTemplate.opsForValue().get(uploadedSizeKey);
Long uploadedSize = 0L;
if (!StringUtils.isNullOrEmpty(uploadedSizeStr)) {
uploadedSize = Long.valueOf(uploadedSizeStr);
}
// 获取文件类型
String fileType = this.getFileType(file);
// 上传的是第一个分片,用uploadAppenderFile方法
if (sliceNo == 1) {
// 第一个分片上传的文件路径
String path = this.uploadAppenderFile(file);
if (StringUtils.isNullOrEmpty(path)) {
throw new ConditionException("上传失败!");
}
// 1.先把path存储到redis的pathKey
redisTemplate.opsForValue().set(pathKey, path);
// 2.再把文件大小更新到redis的uploadedSize
uploadedSize += file.getSize();
redisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));
// 3.更新已经上传的分片数到redis的uploadedNoKey
redisTemplate.opsForValue().set(uploadedNoKey, "1");
} else { // 如果不是第一个分片
// 获取已经上传的文件分片的路径
String filePath = redisTemplate.opsForValue().get(pathKey);
if (StringUtils.isNullOrEmpty(filePath)) {
throw new ConditionException("上传失败!");
}
this.modifyAppenderFile(file, filePath, uploadedSize);
// 1.更新已经上传的分片数+1
redisTemplate.opsForValue().increment(uploadedNoKey);
// 2.再把文件大小更新到redis的uploadedSize
uploadedSize += file.getSize();
redisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));
}
// 比对已经上传的文件分片数 和 总共要上传的分片数,一致说明文件上传完成,并且可以清除redis里面相关的key
// 然后返回给前端一个上传好的文件路径
String uploadedNoStr = redisTemplate.opsForValue().get(uploadedNoKey);
Integer uploadedNo = Integer.valueOf(uploadedNoStr);
String resultPath = "";
// 上传结束
if (uploadedNo.equals(totalSliceNo)) {
// 获取文件路径,用于返回给前端
resultPath = redisTemplate.opsForValue().get(pathKey);
List<String> keyList = Arrays.asList(pathKey, uploadedSizeKey, uploadedNoKey);
// 清除redis相关的key和value
redisTemplate.delete(keyList);
}
return resultPath;
}
/**
* 把一个文件转换成多个分片
*
* @param multipartFile
*/
public void convertFileToSlices(MultipartFile multipartFile) throws Exception {
String fileName = multipartFile.getOriginalFilename();
String fileType = this.getFileType(multipartFile);
// 将multipartFile转化成java自带的文件类型
File file = this.MultipartFileToFile(multipartFile);
// 文件的大小
long fileLength = file.length();
// 计数器,方便后面文件分片名称的生成
int count = 1;
// i表示每个分片的起始位置
for (int i = 0; i < fileLength; i += SLICE_SIZE) {
// "r"表示读权限,读写权限就是"rw"
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
randomAccessFile.seek(i);
byte[] bytes = new byte[SLICE_SIZE];
int len = randomAccessFile.read(bytes);
String path = "/Users/xiexu/tmpfile/" + count + "." + fileType;
File slice = new File(path);
FileOutputStream fos = new FileOutputStream(slice);
fos.write(bytes, 0, len);
fos.close();
randomAccessFile.close();
count++;
}
file.delete();
}
/**
* 将MultipartFile转换成File类型
*
* @param multipartFile
* @return
*/
public File MultipartFileToFile(MultipartFile multipartFile) throws Exception {
String originalFilename = multipartFile.getOriginalFilename();
// 数组包含文件名称和文件类型
String[] fileName = originalFilename.split("\\.");
File file = File.createTempFile(fileName[0], "." + fileName[1]);
multipartFile.transferTo(file);
return file;
}
}
FileApi.java
@RestController
public class FileApi {
@Autowired
private FileService fileService;
@PostMapping("/md5files")
public JsonResponse<String> getFileMD5(MultipartFile file) throws Exception {
String fileMD5 = fileService.getFileMD5(file);
return new JsonResponse<>(fileMD5);
}
/**
* 断点续传
*
* @param slice
* @param fileMd5
* @param sliceNo
* @param totalSliceNo
* @return
* @throws Exception
*/
@PutMapping("/file-slices")
public JsonResponse<String> uploadFileBySlices(MultipartFile slice, String fileMd5, Integer sliceNo, Integer totalSliceNo) throws Exception {
String filePath = fileService.uploadFileBySlices(slice, fileMd5, sliceNo, totalSliceNo);
return new JsonResponse<>(filePath);
}
}
秒传
数据库表设计及相关实体类设计
添加视频
数据库表设计及相关实体类设计
文件表
视频投稿记录表
标签表
视频标签关联表
视频在线观看(下载)
视频点赞相关功能
数据库表设计及相关实体类设计
视频点赞表
- userId:表示当前给这个视频点赞的用户
- videoId:表示当前被点赞的视频
视频收藏相关功能
数据库表设计及相关实体类设计
视频收藏记录表
收藏分组表
视频投币相关功能
数据库表设计及相关实体类设计
视频投币记录表
用户硬币数量表
视频评论相关功能
数据库表设计及相关实体类设计
视频评论表