第二章 打造高性能的视频系统

视频与弹幕功能开发概要

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 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 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 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 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 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:表示当前被点赞的视频

视频收藏相关功能

数据库表设计及相关实体类设计

视频收藏记录表

收藏分组表

视频投币相关功能

数据库表设计及相关实体类设计

视频投币记录表

用户硬币数量表

视频评论相关功能

数据库表设计及相关实体类设计

视频评论表

参考文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: