minio 分布式文件管理

一、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分钟,通过任务的启动时间去判断任务是否超过执行期限。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【射频IC学习笔记】4 D类功率放大器PA电路设计/loadpull仿真/输出功率及效率PAE计算

一、功率放大器设计指标及电路结构 1. 设计指标 功率放大器的指标要求如下图所示采用D类的开关类型功率放大器&#xff0c;理论上开关类型的PA能够做到100%的效率&#xff0c;但实际上会有一些偏差。像D类功放并不适合高功率射频信号的输出&#xff0c;因为其在射频功率上面的…

【数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】

目录&#x1f60b; 任务描述 相关知识 测试说明 我的通关代码: 测试结果&#xff1a; 任务描述 本关任务&#xff1a;实现二叉排序树的基本算法。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;二叉树的创建、查找和删除算法。具体如下&#xff1a; (1)由…

Unity UGUI图片循环列表插件

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

5G学习笔记之SNPN系列之ID和广播消息

目录 1. 概述 2. SNPN ID 3. SNPN广播消息 1. 概述 SNPN&#xff1a;Stand-alone Non-Public Network&#xff0c;独立的非公共网络&#xff0c;由NPN独立运营&#xff0c;不依赖与PLMN网络。 SNPN不支持的5GS特性&#xff1a; 与EPS交互 emergency services when the UE acce…

(后序遍历 简单)leetcode 101翻转二叉树

将根结点的左右结点看作 两个树的根结点&#xff0c;后序遍历&#xff08;从叶子结点从下往上遍历&#xff09; 两个树边遍历边比较。 左节点就左右根的后序遍历 右根结点就右左根的后序遍历来写 后序遍历&#xff08;从叶子结点从下往上遍历&#xff09; /*** Definition …

通过ajax的jsonp方式实现跨域访问,并处理响应

一、场景描述 现有一个项目A&#xff0c;需要请求项目B的某个接口&#xff0c;并根据B接口响应结果A处理后续逻辑。 二、具体实现 1、前端 前端项目A发送请求&#xff0c;这里通过jsonp的方式实现跨域访问。 $.ajax({ url:http://10.10.2.256:8280/ssoCheck, //请求的u…

Goby AI 2.0 自动化编写 EXP | Mitel MiCollab 企业协作平台 npm-pwg 任意文件读取漏洞(CVE-2024-41713)

漏洞名称&#xff1a;Mitel MiCollab 企业协作平台 npm-pwg 任意文件读取漏洞(CVE-2024-41713) English Name&#xff1a;Mitel MiCollab /npm-pwg File Read Vulnerability (CVE-2024-41713) CVSS core: 6.8 漏洞描述&#xff1a; Mitel MiCollab 是加拿大 Mitel 公司推出…

现代密码学总结(上篇)

现代密码学总结 &#xff08;v.1.0.0版本&#xff09;之后会更新内容 基本说明&#xff1a; ∙ \bullet ∙如果 A A A是随机算法&#xff0c; y ← A ( x ) y\leftarrow A(x) y←A(x)表示输入为 x x x ,通过均匀选择 的随机带运行 A A A,并且将输出赋给 y y y。 ∙ \bullet …

VMware:CentOS 7.* 连不上网络

1、修改网络适配 2、修改网卡配置参数 cd /etc/sysconfig/network-scripts/ vi ifcfg-e33# 修改 ONBOOTyes 3、重启网卡 service network restart 直接虚拟机中【ping 宿主机】&#xff0c;能PING通说明centOS和宿主机网络通了&#xff0c;只要宿主机有网&#xff0c;则 Ce…

Linux —— vim 编辑器

一、什么是vim vim是一个功能强大、高度可定制的文本编辑器。以下是对vim编辑器的具体介绍&#xff1a; 历史背景&#xff1a;vim最初由Bram Moolenaar在1991年开发&#xff0c;作为vi编辑器的增强版&#xff0c;增加了许多新的特性和改进。它继承了vi的基本编辑功能和键盘快捷…

前端(async 和await)

1 async async 将 function 变为成为 async 函数 ●async 内部可以使用 await&#xff0c;也可以不使用&#xff0c;因此执行这个函数时&#xff0c;可以使用 then 和 catch 方法 ●async 函数的返回值是一个 Promise 对象 ●Promise 对象的结果由 async 函数执行的返回值决…

huggingface NLP -Transformers库

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

解决 MyBatis 中空字符串与数字比较引发的条件判断错误

问题复现 假设你在 MyBatis 的 XML 配置中使用了如下代码&#xff1a; <if test"isCollect ! null"><choose><when test"isCollect 1">AND exists(select 1 from file_table imgfile2 where task.IMAGE_SEQimgfile2.IMAGE_SEQ and im…

项目15:简易扫雷--- 《跟着小王学Python·新手》

项目15&#xff1a;简易扫雷 — 《跟着小王学Python新手》 《跟着小王学Python》 是一套精心设计的Python学习教程&#xff0c;适合各个层次的学习者。本教程从基础语法入手&#xff0c;逐步深入到高级应用&#xff0c;以实例驱动的方式&#xff0c;帮助学习者逐步掌握Python的…

Centos7上Jenkins+Docker+Git+SpringBoot自动化部署

文章目录 1.宿主机安装maven2.安装jenkins3.配置Jenkins4.Jenkins脚本自动安装JDK&#xff08;可选&#xff09; 1.宿主机安装maven wget https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz mv apache-maven-3.9.9-bin.tar.gz /usr/local…

前端-自定义Ant Design 表格(可编辑表格)

选取的的是&#xff1a;表格 Table - Ant Design 其实ant design本身就有增加和删除单列数据的封装好的表格&#xff0c;但是个人觉得那个功能繁多&#xff0c;自己实现封装也便于之后理解和二次使用。 初步效果&#xff08;舍去切换样式的功能&#xff09;&#xff1a; 突破的…

如何打造个人知识体系?

第一&#xff0c;每个人的基本情况不同。比如我有一个类别跟「设计」相关&#xff0c;这是自己的个人爱好&#xff0c;但不一定适合其他人。再比如我还有一个类别跟「广告文案」相关&#xff0c;因为里面很多表达可以借用到演讲或写作中&#xff0c;这也不适合所有人。 第二&am…

Java版-图论-最短路-Floyd算法

实现描述 网络延迟时间示例 根据上面提示&#xff0c;可以计算出&#xff0c;最大有100个点&#xff0c;最大耗时为100*wi,即最大的耗时为10000&#xff0c;任何耗时计算出来超过这个值可以理解为不可达了&#xff1b;从而得出实现代码里面的&#xff1a; int maxTime 10005…

【EthIf-04】 EthIf_CtrlIdx控制器索引

1 EthernetInterface功能spec 上层的模块访问以太网接口模块「EthernetInterface」&#xff0c;以太网接口模块通过以太网驱动程序层与多个以太网控制器交互的。 1.1 以太网控制器资源的索引 通过以太网接口模块中的以太网控制器资源的索引允许用户轻松访问多个以太网控制器…

IDEA报错:无效的源发行版、无效的目标发行版

1. 无效的源发行版 创建项目的时候&#xff0c;会遇见这个报错&#xff0c;原因就是编译的JDK版本与发布版本不一致。 解决方法&#xff1a; 1.1. 找到问题所在地 英文&#xff1a;File -> Project Structure ->Project Settings 中文&#xff1a;文件->项目结构 …