minio 分布式文件管理

news2024/12/15 9:48:32

一、minio 是什么?

MinIO构建分布式文件系统,MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。

官网:https://www.minio.org.cn/

二、minio的部署

本项目采用docker搭建

首先需要创建,文件存储的目录。以后上传的文件,在这4个目录中都会进行存储(即:一个文件存储4份),保证数据的安全性

 mkdir -p /root/minio_data/data1
 mkdir -p /root/minio_data/data2
 mkdir -p /root/minio_data/data3
 mkdir -p /root/minio_data/data4
docker run -p 9000:9000 -p 9001:9001 --name minio \
  -v /root/minio_data/data1:/data1 \
  -v /root/minio_data/data2:/data2 \
  -v /root/minio_data/data3:/data3 \
  -v /root/minio_data/data4:/data4 \
  -e "MINIO_ROOT_USER=minioadmin" \
  -e "MINIO_ROOT_PASSWORD=minioadmin" \
  minio/minio server /data{1...4} --console-address ":9001"


  • 9000端口是作为S3 API端口,用于API的调用,9001端口用于Web控制台
  • minio/minio: 这是Docker镜像的名称
  • server /data{1...4}: 这部分告诉MinIO以服务器模式启动,并且使用/data1/data2/data3, 和 /data4这四个目录作为存储位置。
  • --console-address ":9001": 这个参数指定了MinIO Web控制台的监听地址和端口。这里设置为":9001",意味着Web控制台将监听容器内的9001端口。
  • 访问地址:http://ip地址:9001   账号:minioadmin 密码:minioadmin
  • 93dac0ad23b94507b17dfece45f17685.png

三、基本使用方法 

1.创建一个bucket

创建一个测试bucket,用以存储文件

69036144c9f1407da346466d7b05d7c8.png

 2.上传文件

f44f9ec88d30422cb22088411c5e3687.png

上传文件后,我们可以发现在,data1 data2 data3 data4 目录下都进行了存储

28be8fdb29d64de7a1eee118b265691c.png

测试minio的数据恢复过程:

1、首先删除一个目录。

删除目录后仍然可以在web控制台上传文件和下载文件。

稍等片刻删除的目录自动恢复。

2、删除两个目录。

删除两个目录也会自动恢复。

3、删除三个目录 。

由于 集合中共有4块硬盘,有大于一半的硬盘损坏数据无法恢复。

此时报错:We encountered an internal error, please try again.  (Read failed.  Insufficient number of drives online)在线驱动器数量不足。

 

四、项目依赖

这些项目中会用到的依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.3</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.8.1</version>
</dependency>
    <!--根据扩展名取mimetype-->
    <dependency>
        <groupId>com.j256.simplemagic</groupId>
        <artifactId>simplemagic</artifactId>
        <version>1.17</version>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.11</version>
    </dependency>

 需要将访问权限设置public,这样远程才能够访问到4ff11f59df064b6484ea2023b3776798.png

需要三个参数才能连接到minio服务。

dbca3be3b94845a9aa407b398e233346.png

五、图片上传

1.本地测试

包含上传文件、删除文件、下载文件、检查完整性

package com.xuecheng.media;

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;

/**
 * @description 测试MinIO
 * @author Mr.M
 * @date 2022/9/11 21:24
 * @version 1.0
 */
public class MinioTest {

    static MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://124.70.208.223:8089/") //9000端口用于API调用
                    .credentials("minioadmin", "minioadmin")
                    .build();
    private String getMimeType(String extension){
        if(extension==null)
            extension = "";
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);//根据扩展名获取MIME类型,比如.mp4文件的MIME类型是video/mp4
        //通用mimeType,字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if(extensionMatch!=null){
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

   //上传文件
   @Test
   void upload() {
       try {
           String filename="E:\\Users\\31118\\Pictures\\Snipaste_2024-11-10_23-08-04.png";
           String bucketName = "001/test001.jpg";
           String bucket ="testbucket";
           String mimeType = getMimeType(".jpg");
           UploadObjectArgs testbucket = UploadObjectArgs.builder()
                   .bucket(bucket)
                   .filename(filename) //本地文件路径
                   .object(bucketName) //上传到bucket下的路径
                   .contentType(mimeType)//默认根据扩展名确定文件
                   .build();
           minioClient.uploadObject(testbucket);
           check(filename,bucketName,bucket);
           System.out.println("上传成功");
       } catch (Exception e) {
           e.printStackTrace();
           System.out.println("上传失败");
       }

   }
   //删除文件
    @Test
    void delete(){
        try {
            RemoveObjectArgs testbucket = RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.jpg").build();
            minioClient.removeObject(testbucket);
            System.out.println("删除成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("删除失败");
        }
    }
    //查看/下载文件
    @Test
    void getFile() {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("001/test001.jpg").build();
        try(
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
                FileOutputStream outputStream = new FileOutputStream(new File("E:\\图片.gif"));//输出路径
        ) {
            IOUtils.copy(inputStream,outputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //对上传之后和下载完成后的文件进行完整性检查,防止丢包
    //将上传完成后的文件和本地的临时文件的md5的值进行比对,如果一致,则说明上传和下载成功
    void check(String fileName,String bucketName,String bucket){
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(bucketName).build();
        //校验文件的完整性对文件的内容进行md5
        try {
            //获取远程文件的md5
            FilterInputStream fileInputStream1 = minioClient.getObject(getObjectArgs);
            String source_md5 = DigestUtils.md5Hex(fileInputStream1);
            //获取本地文件的md5
            FileInputStream fileInputStream = new FileInputStream(new File(fileName));
            String local_md5 = DigestUtils.md5Hex(fileInputStream);
            if(source_md5.equals(local_md5)){
                System.out.println("下载成功");
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

 上传文件时contentType("")属性并不是强制要求设置的,但一般建议设置,以便浏览器进行识别该文件的类型

 

2、java服务器远程部署-图片上传

minio:
  endpoint: http://124.70.208.223:9000 #API访问路径
  accessKey: minioadmin #登录账号
  secretKey: minioadmin #登录密码
  bucket:
    files: mediafiles #文件/图片 存在的位置
    videofiles: video #视频存储的位置

 

文件上传时,获取md5,作为主键保存在文件表中

后续上传的如果是同一个文件时,他们的md5的值是一致的,不在进行二次存储

配置类注册,方便后面直接使用

package com.xuecheng.media.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @description minio配置
 */
 @Configuration
public class MinioConfig {


 @Value("${minio.endpoint}")
 private String endpoint;
 @Value("${minio.accessKey}")
 private String accessKey;
 @Value("${minio.secretKey}")
 private String secretKey;

 @Bean
 public MinioClient minioClient() {

  return MinioClient.builder()
                  .endpoint(endpoint)
                  .credentials(accessKey, secretKey)
                  .build();
 }
}

控制层接收到MultipartFile后,这是获取一些常见属性的办法,方便对文件进行存储

    @ApiOperation("上传文件")
    @RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) //对文件类型进行声明
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
        //文件大小
        long fileSize = filedata.getSize();
        //文件名称
        String originalFilename = filedata.getOriginalFilename();
        //创建临时文件
        File tempFile = File.createTempFile("minio", "temp"); //createTempFile 方法会生成一个唯一的文件名,该文件名由前缀、一个随机生成的字符串和后缀组成。例如/minio1234567890temp
        //上传的文件拷贝到临时文件
        filedata.transferTo(tempFile);
        //文件路径
        String absolutePath = tempFile.getAbsolutePath();

    }

 

 3.上传文件

 public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
  try {
   UploadObjectArgs testbucket = UploadObjectArgs.builder()
           .bucket(bucket)
           .object(objectName)
           .filename(localFilePath)
           .contentType(mimeType)
           .build();
   minioClient.uploadObject(testbucket);
   log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
   System.out.println("上传成功");
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
   XueChengPlusException.cast("上传文件到文件系统失败");
  }
  return false;
 }

4.需要用到的工具方法

获取文件Md5

 //获取文件的md5
 private String getFileMd5(File file) {
  try (FileInputStream fileInputStream = new FileInputStream(file)) {
   String fileMd5 = DigestUtils.md5Hex(fileInputStream);
   return fileMd5;
  } catch (Exception e) {
   e.printStackTrace();
   return null;
  }
 }

获取年月日结构目录

 //获取文件默认存储目录路径 年/月/日
 private String getDefaultFolderPath() {
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  String format = sdf.format(new Date());
     return format.replace("-", "/")+"/";
 }

根据扩展名获取MIME类型

比如.mp4文件的MIME类型是video/mp4

 private String getMimeType(String extension){ //传入.jpg
  if(extension==null)
   extension = "";
  //根据扩展名取出mimeType
  ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
  //通用mimeType,字节流
  String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
  if(extensionMatch!=null){
   mimeType = extensionMatch.getMimeType();
  }
  return mimeType;
 }

这边只进行关键信息的展示,数据库相关操作根据项目自行处理

图片的访问链接是:

服务器ip:9000/mediafiles/2024/11/27/e0abb735ab793fae5568c2ed537ab37c.jpg

注意9000是API地址,9001是web服务地址

 

六、视频上传-断点续传

minio限制,视频至少以5mb,划分

1.文件上传前检查文件是否已上传

先通过前端计算出视频md5的值,传给后端,检测该视频是否已经存在,

    @ApiOperation(value = "文件上传前检查文件")
    @PostMapping("/upload/checkfile")
    public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) throws Exception {
        return bigFilesService.checkFile(fileMd5);
    }
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        //查询文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {
            //桶
            String bucket = mediaFiles.getBucket();
            //存储目录
            String filePath = mediaFiles.getFilePath();
            //文件流
            InputStream stream = null;
            try {
                stream = minioClient.getObject(
                        GetObjectArgs.builder()
                                .bucket(bucket)
                                .object(filePath)
                                .build());

                if (stream != null) {
                    //文件已存在
                    return RestResponse.success(true);
                }
            } catch (Exception e) {
                log.info("文件不存在,准备开始分块上传");
            }
        }
        //文件不存在
        return RestResponse.success(false);
    }

 2.分块上传前检测分块是否已上传

根据后端的响应信息,若该视频不存在,前端对视频划分为一个个分块,并计算每个分块的md5值

将分块的md5值,传给后端,判断该分块是否存在,若该分块不存在则上传分块

 

  @ApiOperation(value = "分块文件上传前的检测")
    @PostMapping("/upload/checkchunk")
    public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception {
        return bigFilesService.checkChunk(fileMd5,chunk);
    }
 @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {

        //得到分块文件目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunkIndex;

        //文件流
        InputStream fileInputStream = null;
        try {
            fileInputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket_videoFiles)
                            .object(chunkFilePath)
                            .build());

            if (fileInputStream != null) {
                //分块已存在
                log.info("分块{}已存在",chunkIndex);
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            //minio中没有该分块,上传分块
            log.info("分块{}不存在,开始上传",chunkIndex);
        }
        //分块未存在
        return RestResponse.success(false);
    }

3.上传分块

    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk,String localFilePath) {


        //得到分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunk;

        try {
            //将文件存储至minIO
            addMediaFilesToMinIO(localFilePath, bucket_videoFiles,chunkFilePath);
            return RestResponse.success(true);
        } catch (Exception ex) {
            ex.printStackTrace();
            log.debug("上传分块文件:{},失败:{}",chunkFilePath,ex.getMessage());
        }
        return RestResponse.validfail(false,"上传分块失败");
    }

 /**
  * @description 将文件写入minIO
  * @param localFilePath  文件地址
  * @param bucket  桶
  * @param objectName 对象名称
  * @return void
  * @author Mr.M
  * @date 2022/10/12 21:22
  */
 public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
  try {
   UploadObjectArgs testbucket = UploadObjectArgs.builder()
           .bucket(bucket)
           .object(objectName)
           .filename(localFilePath)
           .contentType(mimeType)
           .build();
   minioClient.uploadObject(testbucket);
   log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
   System.out.println("上传成功");
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
   XueChengPlusException.cast("上传文件到文件系统失败");
  }
  return false;
 }

md5目录结构

分块存储目录:d/a/da112e234adasdasd/chunk

    //得到分块文件的目录
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

修改文件大小的限制

 前端对文件分块的大小为5MB,SpringBoot web默认上传文件的大小限制为1MB

spring:
  servlet:
    multipart:
      max-file-size: 50MB #单个文件的大小限制
      max-request-size: 50MB #单次请求的大小限制

4.合并分块

@ApiOperation(value = "合并文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) throws Exception {
    Long companyId = 1232141425L;
    UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
    uploadFileParamsDto.setFileType("001002");
    uploadFileParamsDto.setTags("课程视频");
    uploadFileParamsDto.setRemark("");
    uploadFileParamsDto.setFilename(fileName);

    return bigFilesService.mergechunks(companyId,fileMd5,chunkTotal,uploadFileParamsDto);
}
 @Override
    public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        //=====获取分块文件路径=====
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //组成将分块文件路径组成 List<ComposeSource>
        List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i) //从0开始,迭代到chunkTotal,依次获取所有分块文件,0 1 2
                .limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                        .bucket(bucket_videoFiles)
                        .object(chunkFileFolderPath+i)
                        .build())
                .collect(Collectors.toList());
        //=====合并=====
        //文件名称
        String fileName = uploadFileParamsDto.getFilename();
        //文件扩展名
        String extName = fileName.substring(fileName.lastIndexOf("."));
        //合并文件路径
        String mergeFilePath = getFilePathByMd5(fileMd5, extName);
        try {
            //合并文件
            ObjectWriteResponse response = minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucket_videoFiles)
                            .object(mergeFilePath)
                            .sources(sourceObjectList)
                            .build());
            log.debug("合并文件成功:{}",mergeFilePath);
        } catch (Exception e) {
            log.debug("合并文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);
            return RestResponse.validfail(false, "合并文件失败。");
        }

        // ====验证md5====
        File minioFile = downloadFileFromMinIO(bucket_videoFiles,mergeFilePath);
        if(minioFile == null){
            log.debug("下载合并后文件失败,mergeFilePath:{}",mergeFilePath);
            return RestResponse.validfail(false, "下载合并后文件失败。");
        }

        try (InputStream newFileInputStream = new FileInputStream(minioFile)) {
            //minio上文件的md5值
            String md5Hex = DigestUtils.md5Hex(newFileInputStream);
            //比较md5值,不一致则说明文件不完整
            if(!fileMd5.equals(md5Hex)){
                return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");
            }
            //文件大小
            uploadFileParamsDto.setFileSize(minioFile.length());
        }catch (Exception e){
            log.debug("校验文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);
            return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");
        }finally {
            if(minioFile!=null){ //删除下载的临时文件
                minioFile.delete();
            }
        }

        //文件入库
        currentProxy.addMediaFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_videoFiles,mergeFilePath);
        //=====清除分块文件=====
        clearChunkFiles(chunkFileFolderPath,chunkTotal);
        return RestResponse.success(true);
    }

下载至本地,用于md5检测

 

获得合并后文件存储路径

    private String getFilePathByMd5(String fileMd5,String fileExt){
        return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
    }

将上传后的文件下载至本地

将下载文件的md5的值与前端传递过来时视频md5值进行比较,判断视频上传时是否出现丢包

 public File downloadFileFromMinIO(String bucket,String objectName){
        //临时文件
        File minioFile = null;
        FileOutputStream outputStream = null;
        try{
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            //创建临时文件
            minioFile=File.createTempFile("minio", ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream,outputStream);
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(outputStream!=null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

删除分块

视频上传成功后,删除之前上传的分块

   private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){

        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r->{
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
        }
    }

分块文件清理问题

上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗?怎么做的?

1、在数据库中有一张文件表记录minio中存储的文件信息。

2、文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成。

3、当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录。

 

视频文件格式转换

视频文件的格式有很多中,我们需要把视频格式统一转换为mp4,下面以avi文件格式转换为mp4格式举例

FFmpeg进行媒体文件的转换

ffmpeg的安装及基本使用:

xxl-job分布式任务调度

由于媒体文件转换需要处理的时间,我们采用xxl-job进行分布式任务调度

xxl-job的基本使用方法:

多服务执行:

23c389d5ceb543d99564fc2a2f9d2c52.png

-Dserver.port=63051 -Dxxl.job.executor.port=9998

什么是乐观锁、悲观锁?

synchronized是一种悲观锁,在执行被synchronized包裹的代码时需要首先获取锁,没有拿到锁则无法执行,是总悲观的认为别的线程会去抢,所以要悲观锁。

乐观锁的思想是它不认为会有线程去争抢,尽管去执行,如果没有执行成功就再去重试。

为了防止多个分布式任务,执行同一个行为,需要使用分布锁进行来控制

1、基于数据库实现分布锁

利用数据库主键唯一性的特点,或利用数据库唯一索引、行级锁的特点,多个线程同时去更新相同的记录,谁更新成功谁就抢到锁。

数据库表的设计

在上传文件之后,将需要格式转换的文件,存入media_process数据库

b90b028bcd4547988dfec01730c0e1a5.png

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for media_process
-- ----------------------------
DROP TABLE IF EXISTS `media_process`;
CREATE TABLE `media_process`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `file_id` varchar(120) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件标识',
  `filename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件名称',
  `bucket` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '存储桶',
  `file_path` varchar(512) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '存储路径',
  `status` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '状态,1:未处理,2:处理成功  3处理失败 4处理中',
  `create_date` datetime NOT NULL COMMENT '上传时间',
  `finish_date` datetime NULL DEFAULT NULL COMMENT '完成时间',
  `fail_count` int NULL DEFAULT 0 COMMENT '失败次数',
  `url` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '媒资文件访问地址',
  `errormsg` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '失败原因',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `unique_fileid`(`file_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

谁先抢到,谁处理

视频处理完成后,转存如 media_process_history表中,在media_process表中,删除该条记录


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for media_process_history
-- ----------------------------
DROP TABLE IF EXISTS `media_process_history`;
CREATE TABLE `media_process_history`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `file_id` varchar(120) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件标识',
  `filename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件名称',
  `bucket` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '存储源',
  `status` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '状态,1:未处理,2:处理成功  3处理失败',
  `create_date` datetime NOT NULL COMMENT '上传时间',
  `finish_date` datetime NOT NULL COMMENT '完成时间',
  `url` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '媒资文件访问地址',
  `fail_count` int NULL DEFAULT 0 COMMENT '失败次数',
  `file_path` varchar(512) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '文件路径',
  `errormsg` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '失败原因',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

 

   @XxlJob("videoJobHandler")
    public void videoJobHandler() throws Exception {
        // 分片参数
    int shardIndex = XxlJobHelper.getShardIndex();
    int shardTotal = XxlJobHelper.getShardTotal();
    List<MediaProcess> mediaProcessList = null;
    int size = 0;
    try {
        //取出cpu核心数作为一次处理数据的条数
        int processors = Runtime.getRuntime().availableProcessors();
        //获取待处理视频
        //一次处理视频数量不要超过cpu核心数,避免CPU超载
        mediaProcessList = mediaFileProcessService.getMediaProcessList(shardIndex, shardTotal, processors);
        size = mediaProcessList.size();
        log.debug("取出待处理视频任务{}条", size);
        if (size < 0) {
            return;
        }
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }
    //启动size个线程的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(size);
    //计数器,用于等待所有线程执行完毕
    CountDownLatch countDownLatch = new CountDownLatch(size);
    //将处理任务加入线程池
    mediaProcessList.forEach(mediaProcess -> {
        threadPool.execute(() -> { //所以线程,通过循环同时启动
            try {
                //任务id
                Long taskId = mediaProcess.getId();
                //抢占任务,将任务status状态改为4正在处理
                boolean b = mediaFileProcessService.startTask(taskId);
                if (!b) {
                    return;
                }
                log.debug("开始执行任务:{}", mediaProcess);
                //下边是处理逻辑
                //桶
                String bucket = mediaProcess.getBucket();
                //存储路径
                String filePath = mediaProcess.getFilePath();
                //原始视频的md5值
                String fileId = mediaProcess.getFileId();
                //原始文件名称
                String filename = mediaProcess.getFilename();
                //将要处理的文件下载到服务器上
                File originalFile = bigFilesService.downloadFileFromMinIO(mediaProcess.getBucket(), mediaProcess.getFilePath());
                if (originalFile == null) {
                    log.debug("下载待处理文件失败,originalFile:{}", mediaProcess.getBucket().concat(mediaProcess.getFilePath()));
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "下载待处理文件失败");
                    return;
                }
                //处理结束的视频文件
                File mp4File = null;
                //创建临时文件,作为转化后的文件
                try {
                    mp4File = File.createTempFile("mp4", ".mp4");
                } catch (IOException e) {
                    log.error("创建mp4临时文件失败");
                    //保存任务是失败的结果
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "创建mp4临时文件失败");
                    return;
                }
                //视频处理结果
                String result = "";
                try {
                    String absolutePath = mp4File.getAbsolutePath();//包含了,文件名
                    String localPath = absolutePath.substring(0, absolutePath.lastIndexOf("\\")+1);
                    //开始处理视频
                    Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpegpath, originalFile.getAbsolutePath(), mp4File.getName(),localPath);
                    //开始视频转换,成功将返回success
                    result = videoUtil.generateMp4();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("处理视频文件:{},出错:{}", mediaProcess.getFilePath(), e.getMessage());
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "下载待处理文件失败");
                }
                if (!result.equals("success")) {
                    //记录错误信息
                    log.error("处理视频失败,视频地址:{},错误信息:{}", bucket + filePath, result);
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, result);
                    return;
                }
    
                //将mp4上传至minio
                //mp4在minio的存储路径
                String objectName = getFilePath(fileId, ".mp4");
                //访问url
                String url = "/" + bucket + "/" + objectName;
                try {
                    bigFilesService.addMediaFilesToMinIO(mp4File.getAbsolutePath(), "video/mp4", bucket, objectName);
                    //将url存储至数据,并更新状态为成功,并将待处理视频记录删除存入历史
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "2", fileId, url, null);
                } catch (Exception e) {
                    log.error("上传视频失败或入库失败,视频地址:{},错误信息:{}", bucket + objectName, e.getMessage());
                    //最终还是失败了
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "处理后视频上传或入库失败");
                }
            }finally {
                countDownLatch.countDown();  //线程数减一
            }
        });
    });
    //等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
    countDownLatch.await(30, TimeUnit.MINUTES);
    }

    private String getFilePath(String fileMd5,String fileExt){
        return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
    }

 

当前需要处理的视频文件,需要根据计算机 当前计算机启动的服务下标(从0开始...),当前计算机启动服务总个数 和 计算机的线程数计算得出,因为若计算机的线程数为8,一次性最多处理8个视频

sql语句这样设计的目的是为了给每个服务(执行器),分配任务。一台8核的计算机,一次性最多分配8个任务

 @Override
 public List<MediaProcess> getMediaProcessList(int shardIndex, int shardTotal, int count) {
     return mediaProcessMapper.selectListByShardIndex(shardTotal, shardIndex, count);
 }
    /**
     * @description 根据分片参数获取待处理任务,一次处理视频数量不要超过cpu核心数,避免CPU超载
     * @param shardTotal  分片总数
     * @param shardIndex  分片序号
     * @param count 任务数
     * @return java.util.List<com.xuecheng.media.model.po.MediaProcess>
     * @author Mr.M
     * @date 2022/9/14 8:54
     */
    @Select("select * from media_process t where t.id % #{shardTotal} = #{shardIndex} and (t.status = '1' or t.status = '3') and t.fail_count < 3 limit #{count}")
    List<MediaProcess> selectListByShardIndex(@Param("shardTotal") int shardTotal, @Param("shardIndex") int shardIndex, @Param("count") int count);

 Sql语句的查询,原理如下

f5cc1cce94384b7799acf265302e436d.png

 

上边两个执行器实例那么分片总数为2,序号为0、1,从任务1开始,如下:

1  %  2 = 1    执行器2执行

2  %  2 =  0    执行器1执行

3  %  2 =  1     执行器2执行

以此类推.

 

一个服务(执行器),所以线程同时执行,为了防止多个线程执行的是同一个任务,当前线程执行时需要前开启任务时,将数据库的状态设置为4,表示正在处理中,防止下次执行时,被其他执行器抢占

    /**
     * 开启一个任务
     * @param id 任务id
     * @return 更新记录数
     */
    @Update("update media_process m set m.status='4' where (m.status='1' or m.status='3') and m.fail_count<3 and m.id=#{id}")
    int startTask(@Param("id") long id);

若视频转换过程中出现异常,失败次数+1,失败次数达到3此不在执行。

若视频转换成功,修改任务状态为2,并将其存入历史进程表中,在当前表中删除该条记录

@Transactional
 @Override
 public void saveProcessFinishStatus(Long taskId, String status, String fileId, String url, String errorMsg) {
  //查出任务,如果不存在则直接返回
  MediaProcess mediaProcess = mediaProcessMapper.selectById(taskId);
  if(mediaProcess == null){
   return ;
  }
  //处理失败,更新任务处理结果
  LambdaQueryWrapper<MediaProcess> queryWrapperById = new LambdaQueryWrapper<MediaProcess>().eq(MediaProcess::getId, taskId);
  //处理失败
  if(status.equals("3")){
   MediaProcess mediaProcess_u = new MediaProcess();
   mediaProcess_u.setStatus("3");
   mediaProcess_u.setErrormsg(errorMsg);
   mediaProcess_u.setFailCount(mediaProcess.getFailCount()+1);
   mediaProcessMapper.update(mediaProcess_u,queryWrapperById);
   log.debug("更新任务处理状态为失败,任务信息:{}",mediaProcess_u);
   return ;
  }
  //任务处理成功
  MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);
  if(mediaFiles!=null){
   //更新媒资文件中的访问url
   mediaFiles.setUrl(url);
   mediaFilesMapper.updateById(mediaFiles);
  }
  //处理成功,更新url和状态
  mediaProcess.setUrl(url);
  mediaProcess.setStatus("2");
  mediaProcess.setFinishDate(LocalDateTime.now());
  mediaProcessMapper.updateById(mediaProcess);

  //添加到历史记录
  MediaProcessHistory mediaProcessHistory = new MediaProcessHistory();
  BeanUtils.copyProperties(mediaProcess, mediaProcessHistory);
  mediaProcessHistoryMapper.insert(mediaProcessHistory);
  //删除mediaProcess
  mediaProcessMapper.deleteById(mediaProcess.getId());

 }

工具类

检查视频时长,校验两个视频时长是否相等,等待进程处理完毕

package com.xuecheng.base.utils;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 此文件作为视频文件处理父类,提供:
 * 1、查看视频时长
 * 2、校验两个视频的时长是否相等
 *
 */
public class VideoUtil {

    String ffmpeg_path;//ffmpeg的安装位置

    public VideoUtil(String ffmpeg_path){
        this.ffmpeg_path = ffmpeg_path;
    }


    //检查视频时间是否一致
    public Boolean check_video_time(String source,String target) {
        String source_time = get_video_time(source);
        //取出时分秒
        source_time = source_time.substring(source_time.lastIndexOf(":")+1);
        String target_time = get_video_time(target);
        //取出时分秒
        target_time = target_time.substring(target_time.lastIndexOf(":")+1);
        if(source_time == null || target_time == null){
            return false;
        }
        float v1 = Float.parseFloat(source_time);
        float v2 = Float.parseFloat(target_time);
        float abs = Math.abs(v1 - v2);
        if(abs<1){//转化是会有细微差距,属于正常现象
            return true;
        }
        return false;
    }


    //获取视频时间(时:分:秒:毫秒)
    public String get_video_time(String video_path) {
        /*
        ffmpeg -i  lucene.mp4
         */
        List<String> commend = new ArrayList<String>();
        commend.add(ffmpeg_path);
        commend.add("-i");
        commend.add(video_path);
        try {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command(commend);
            //将标准输入流和错误输入流合并,通过标准输入流程读取信息
            builder.redirectErrorStream(true);
            Process p = builder.start();
            String outstring = waitFor(p);
            System.out.println(outstring);
            int start = outstring.trim().indexOf("Duration: ");
            if(start>=0){
                int end = outstring.trim().indexOf(", start:");
                if(end>=0){
                    String time = outstring.substring(start+10,end);
                    if(time!=null && !time.equals("")){
                        return time.trim();
                    }
                }
            }

        } catch (Exception ex) {

            ex.printStackTrace();

        }
        return null;
    }
    //等待一个外部进程(通过Process对象表示)完成,并在此过程中捕获该进程的标准输出和错误输出。
    public String waitFor(Process p) {
        InputStream in = null;
        InputStream error = null;
        String result = "error";
        int exitValue = -1;
        StringBuffer outputString = new StringBuffer();
        try {
            in = p.getInputStream();
            error = p.getErrorStream();
            boolean finished = false;
            int maxRetry = 600;//每次休眠1秒,最长执行时间10分种
            int retry = 0;
            while (!finished) {
                if (retry > maxRetry) {
                    return "error";
                }
                try {
                    while (in.available() > 0) {
                        Character c = new Character((char) in.read());
                        outputString.append(c);
                        System.out.print(c);
                    }
                    while (error.available() > 0) {
                        Character c = new Character((char) in.read());
                        outputString.append(c);
                        System.out.print(c);
                    }
                    //进程未结束时调用exitValue将抛出异常
                    exitValue = p.exitValue();
                    finished = true;

                } catch (IllegalThreadStateException e) {
                    Thread.currentThread().sleep(1000);//休眠1秒
                    retry++;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    System.out.println(e.getMessage());
                }
            }
        }
        return outputString.toString();

    }


    public static void main(String[] args) throws IOException {
        String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置
        VideoUtil videoUtil = new VideoUtil(ffmpeg_path);
        String video_time = videoUtil.get_video_time("E:\\ffmpeg_test\\1.avi");
        System.out.println(video_time);
    }
}

avi格式转mp4格式

package com.xuecheng.base.utils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Mp4VideoUtil extends VideoUtil {

    String ffmpeg_path;//ffmpeg的安装位置
    String video_path;
    String mp4_name;
    String mp4folder_path;
    public Mp4VideoUtil(String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path){
        super(ffmpeg_path);
        this.ffmpeg_path = ffmpeg_path;
        this.video_path = video_path;
        this.mp4_name = mp4_name;
        this.mp4folder_path = mp4folder_path;
    }
    //清除已生成的mp4
    private void clear_mp4(String mp4_path){
        //删除原来已经生成的m3u8及ts文件
        File mp4File = new File(mp4_path);
        if(mp4File.exists() && mp4File.isFile()){
            mp4File.delete();
        }
    }
    /**
     * 视频编码,生成mp4文件
     * @return 成功返回success,失败返回控制台日志
     */
    public String generateMp4(){
        //清除已生成的mp4
//        clear_mp4(mp4folder_path+mp4_name);
        clear_mp4(mp4folder_path);
        /*
        ffmpeg.exe -i  lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 .\lucene.mp4
         */
        List<String> commend = new ArrayList<String>();
        //commend.add("D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe");
        commend.add(ffmpeg_path);
        commend.add("-i");
//        commend.add("D:\\BaiduNetdiskDownload\\test1.avi");
        commend.add(video_path);
        commend.add("-c:v");
        commend.add("libx264");
        commend.add("-y");//覆盖输出文件
        commend.add("-s");
        commend.add("1280x720");
        commend.add("-pix_fmt");
        commend.add("yuv420p");
        commend.add("-b:a");
        commend.add("63k");
        commend.add("-b:v");
        commend.add("753k");
        commend.add("-r");
        commend.add("18");
        commend.add(mp4folder_path  + mp4_name );
        String outstring = null;
        try {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command(commend);
            //将标准输入流和错误输入流合并,通过标准输入流程读取信息
            builder.redirectErrorStream(true);
            Process p = builder.start();
            outstring = waitFor(p);

        } catch (Exception ex) {

            ex.printStackTrace();

        }
        Boolean check_video_time = this.check_video_time(video_path, mp4folder_path + mp4_name);
        if(!check_video_time){
            return outstring;
        }else{
            return "success";
        }
    }

    public static void main(String[] args) throws IOException {
        //ffmpeg的路径
        String ffmpeg_path = "F:\\environment\\ffmpeg-7.0.2-full_build\\bin\\ffmpeg.exe";//ffmpeg的安装位置
        //源avi视频的路径
        String video_path = "E:\\Users\\31118\\Videos\\1.avi";
        //转换后mp4文件的名称
        String mp4_name = "1.mp4";
        //转换后mp4文件的路径
        String mp4_path = "E:\\Users\\31118\\Videos\\"; //结尾路径,需要加上\\
        //创建工具类对象
        Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);
        //开始视频转换,成功将返回success
        String s = videoUtil.generateMp4();
        System.out.println(s);

    }
}

任务补偿机制

如果有线程抢占了某个视频的处理任务,如果线程处理过程中挂掉了,该视频的状态将会一直是处理中,其它线程将无法处理,这个问题需要用补偿机制。

单独启动一个任务找到待处理任务表中超过执行期限但仍在处理中的任务,将任务的状态改为执行失败。

任务执行期限是处理一个视频的最大时间,比如定为30分钟,通过任务的启动时间去判断任务是否超过执行期限。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2259870.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为FreeBuds Pro 4丢了如何找回?(附查找功能使用方法)

华为FreeBuds Pro 4查找到底怎么用&#xff1f;华为FreeBuds Pro 4有星闪精确查找和离线查找&#xff0c;离线查找功能涵盖播放铃声、导航定位、星闪精确查找、上线通知、丢失模式、遗落提醒等。星闪精确查找是离线查找的子功能&#xff0c;当前仅华为FreeBuds Pro 4充电盒支持…

深度学习:基于MindSpore的极简风大模型微调

什么是PEFT&#xff1f;What is PEFT&#xff1f; PEFT(Parameter Efficient Fine-Tuning)是一系列让大规模预训练模型高效适应于新任务或新数据集的技术。 PEFT在保持大部分模型权重冻结&#xff0c;只修改或添加一小部份参数。这种方法极大得减少了计算量和存储开销&#x…

【一本通】最小圈

【一本通】最小圈 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 对于一张有向图&#xff0c;要你求图中最小圈的平均值最小是多少&#xff0c;即若一个圈经过k个节点&#xff0c;那么一个圈的平均值为圈上k条边权的和除以k&#xff0c;现要…

ansible自动化运维(四)jinjia2模板

Jinjia2模板 前面说到playbook组成的时候&#xff0c;有介绍到template模块&#xff0c;而template模块对模板文件进行渲染时&#xff0c;使用的就是jinja2模板引擎&#xff0c;jinja2本身就是基于python的模板引擎&#xff0c;所以下面先来了解一下jinjia2模板的一些用法 基…

【USB-HID】“自动化键盘“

这里写目录标题 【USB-HID】"自动化键盘"1. 前言2. 框架3. 实现3.1 模拟键盘按键输入 【USB-HID】“自动化键盘” 1. 前言 最近从朋友那了解了一种"自动化键盘"&#xff0c;能够通过上位机录制按键脚本&#xff0c;然后执行脚本&#xff0c;实现物理键盘…

使用ECK 快速部署 Elasticsearch 集群 + Kibana

部署 ECK [2.12] 安装说明 ElasticCloudonKubernetes(ECK)是一个 Elasticsearch Operator&#xff0c;但远不止于此。ECK 使用 Kubernetes Operator 模式构建而成&#xff0c;需要安装在您的 Kubernetes 集群内&#xff1b; 借助 Elastic Cloud on Kubernetes (ECK)&#xff0…

ruoyi Cannot find module ‘@/views/system/user/index‘

Cannot find module /views/system/user/index 删除node_module 后打包成功

从开始实现扩散概率模型 PyTorch 实现

目录 一、说明 二、从头开始实施 三、线性噪声调度器 四、时间嵌入 五、下层DownBlock类块 六、中间midBlock类块 七、UpBlock上层类块 八、UNet 架构 九、训练 十、采样 十一、配置&#xff08;Default.yaml&#xff09; 十二、数据集 (MNIST) keyword&#xff1a; Diffusion…

航空航天总线协议分析ARINC429

ARINC429是商用飞机和运输机运用最广泛的总线之一&#xff0c;ARINC是美国航空无线电公司(Aeronautical Radio INC.)的缩写&#xff0c;ARINC429总线协议是美国航空电子工程委员会于1977年7月提出发表并获批准使用&#xff0c;它的规范全称是数字式信息传输系统(Digital Inform…

Unity UGUI图片循环列表插件

效果展示&#xff1a; 下载链接&#xff1a;https://gf.bilibili.com/item/detail/1111843026 概述&#xff1a; LoopListView2 是一个与 UGUI ScrollRect 相同的游戏对象的组件。它可以帮助 UGUI ScrollRect 以高效率和节省内存的方式支持任意数量的项目。 对于具有10,000个…

【经验分享】私有云运维的知识点

最近忙于备考没关注&#xff0c;有次点进某小黄鱼发现首页出现了我的笔记还被人收费了 虽然我也卖了一些资源&#xff0c;但我以交流、交换为主&#xff0c;笔记都是免费给别人看的 由于当时刚刚接触写的并不成熟&#xff0c;为了避免更多人花没必要的钱&#xff0c;所以决定公…

解决MAC装win系统投屏失败问题(AMD显卡)

一、问题描述 电脑接上HDMI线后&#xff0c;电脑上能显示有外部显示器接入&#xff0c;但是外接显示器无投屏画面 二、已测试的方法 1 更改电脑分辨&#xff0c;结果无效 2 删除BootCamp&#xff0c;结果无效 3更新电脑系统&#xff0c;结果无效 4 在设备管理器中&#…

huggingface NLP -Transformers库

1 特点 1.1 易于使用&#xff1a;下载、加载和使用最先进的NLP模型进行推理只需两行代码即可完成。 1.2 灵活&#xff1a;所有型号的核心都是简单的PyTorch nn.Module 或者 TensorFlow tf.kears.Model&#xff0c;可以像它们各自的机器学习&#xff08;ML&#xff09;框架中的…

1. 机器学习基本知识(2)——机器学习分类

1.4 机器学习分类 1.4.1 训练监督 1. 监督学习&#xff1a;已对训练数据完成标记 分类&#xff1a;根据数据及其分类信息来进行训练&#xff0c;使模型能够对新的数据进行分类 回归&#xff1a;给出一组特征值来预测目标数值 2. 无监督学习&#xff1a;没有对训练数据进行任…

[C#]使用winform部署ddddocr的onnx模型进行验证码识别文字识别文字检测

【算法介绍】 ddddocr是一个强大的Python OCR&#xff08;光学字符识别&#xff09;库&#xff0c;特别适用于验证码识别。它利用深度学习技术&#xff0c;如卷积神经网络&#xff08;CNN&#xff09;和循环神经网络&#xff08;RNN&#xff09;&#xff0c;对图像中的文字进行…

day10 电商系统后台API——接口测试(使用postman)

【没有所谓的运气&#x1f36c;&#xff0c;只有绝对的努力✊】 目录 实战项目简介&#xff1a; 1、用户管理&#xff08;8个&#xff09; 1.1 登录 1.2 获取用户数据列表 1.3 创建用户 1.4 修改用户状态 1.5 根据id查询用户 1.6 修改用户信息 1.7 删除单个用户 1.8 …

云服务器搭建lamp的wordpress

Ubuntu系统搭建过程目录 一、检查环境1.1 检查是否安装Apache1.2 检查是否安装Mysql1.3 检查是否安装PHP 二、安装Apache截图 三、安装Mysql3.1 安全安装配置3.2 修改权限和密码3.3 重启MySQL服务 四、安装PHP4.1 测试截图 五、下载并安装wordpress以及配置5.1 下载并解压移动5…

C#速成(GID+图形编程)

常用类 类说明Brush填充图形形状,画刷GraphicsGDI绘图画面&#xff0c;无法继承Pen定义绘制的对象直线等&#xff08;颜色&#xff0c;粗细&#xff09;Font定义文本格式&#xff08;字体&#xff0c;字号&#xff09; 常用结构 结构说明Color颜色Point在平面中定义点Rectan…

babeltrace与CTF相关学习笔记-4

babeltrace与CTF相关学习笔记-4 写在前面metadata_string 重头开始定位&#xff0c;操作meta的位置bt_ctf_trace_get_metadata_string stream部分内存的问题 写在前面 正在并行做几件事。 在编译过程中&#xff0c;突然想到&#xff0c;还是再详细研究一下之前的例程。 一是详…

多旋翼无人机 :桨叶设计—跷跷板结构

多旋翼无人机 &#xff1a;桨叶设计——跷跷板结构 前言跷跷板结构 前言 2024年11月&#xff0c;大疆发布了最新的农业无人机T70和T100。其中T70不同于以往的机型&#xff0c;在桨夹处采用了翘翘板结构&#xff0c;大疆将其命名为“挥舞桨叶”。 T70 无人机如下 放大其中螺旋…