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

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

视频与弹幕功能开发概要

  • FastDFS文件服务器搭建、相关工具类开发
  • 视频上传、视频处理、视频获取、视频在线播放、视频下载
  • 弹幕系统、数据统计、社交属性(点赞、投币、收藏、评论)

FastDFS文件服务器

什么是 FastDFS ?

  • 开源的轻量级分布式文件系统,用于解决大数据量存储和负载均衡等问题。
  • 优点:支持HTTP协议传输文件(结合Nginx);对文件内容做Hash处理,节约磁盘空间;支持负载均衡、整体性能较佳。
  • 适用系统类型:中小型系统

FastDFS 的两个角色是什么 ?

  • FastDFS的二个角色:跟踪服务器(Tracker)、存储服务器(Storage)
  • 跟踪服务器:主要做调度工作,起到负载均衡的作用。它是客户端和存储服务器交互的枢纽
  • 存储服务器:主要提供容量和备份服务,存储服务器是以(Group)为单位,每个组内可以有多台存储服务器,数据互为备份。文件及属性(Meta Data)都保存在该服务器上

FastDFS 架构图

Untitled

Nginx

  • Nginx是反向代理服务器。代理其实就是中间人,客户端通过代理发送请求到互联网上的服务器,从而获取想要的资源。
  • Nginx的主要用途:反向代理、负载均衡。
  • Nginx的主要特点:跨平台、配置简单易上手、高并发、内存消耗小、稳定性高。

Nginx 正向代理的特点:

服务端不知道客户端、客户端知道代理端

Untitled

Nginx 反向代理的特点:

服务端知道客户端、客户端不知道代理端

Untitled

Nginx 结合 FastDFS 实现文件资源HTTP访问

Untitled

断点续传

大文件上传的痛点是什么呢 ?

  • 如果文件过大,会导致带宽紧张,请求速度下降
  • 如果上传过程当中,服务中断或者是网络中断、页面崩溃等等情况,导致文件上传失败了;如果我们上传的是大文件的话,需要重新上传,这个过程是非常让人崩溃的!

什么是断点续传 ?

将大文件进行分片,分片的意思

演示文件分片

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);
    }

}

秒传

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

Untitled

添加视频

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

文件表

Untitled

视频投稿记录表

Untitled

标签表

Untitled

视频标签关联表

Untitled

视频在线观看(下载)

Untitled

Untitled

视频点赞相关功能

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

视频点赞表

Untitled

  • userId:表示当前给这个视频点赞的用户
  • videoId:表示当前被点赞的视频

视频收藏相关功能

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

视频收藏记录表

Untitled

收藏分组表

Untitled

视频投币相关功能

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

视频投币记录表

Untitled

用户硬币数量表

Untitled

视频评论相关功能

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

视频评论表

Untitled

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿小羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值