废话不说先上图
拉了一步就是指定sql语句取抢锁,就是我们的starttask方法。这是一个乐观锁。
1、任务调度中心广播作业分片。
2、执行器收到广播作业分片,从数据库读取待处理任务,读取未处理及处理失败的任务。
3、执行器更新任务为处理中,根据任务内容从MinIO下载要处理的文件。
4、执行器启动多线程去处理任务。
5、任务处理完成,上传处理后的视频到MinIO。
6、将更新任务处理结果,如果视频处理完成除了更新任务处理结果以外还要将文件的访问地址更新至任务处理表及文件表中,最后将任务完成记录写入历史表。
具体实现
(1)添加待处理任务
首先添加视频后需要将处理的任务添加到表中 这里需要进行事务处理
@Transactional public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){ //从数据库查询文件 MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5); if (mediaFiles == null) { mediaFiles = new MediaFiles(); //拷贝基本信息 并增加其他属性信息 BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles); mediaFiles.setId(fileMd5);//为什么设置id是因为我们id在数据库上没有设置自增,因为要设计成MD5形式 mediaFiles.setFileId(fileMd5); mediaFiles.setCompanyId(companyId); mediaFiles.setUrl("/" + bucket + "/" + objectName); mediaFiles.setBucket(bucket); mediaFiles.setFilePath(objectName); //mediaFiles.setCreateDate(LocalDateTime.now()); mediaFiles.setAuditStatus("002003"); mediaFiles.setStatus("1"); //保存文件信息到文件表 int insert = mediaFilesMapper.insert(mediaFiles); //int i = 1/0; if (insert < 0) { log.error("保存文件信息到数据库失败,{}",mediaFiles.toString()); XueChengPlusException.cast("保存文件信息失败"); } log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString()); } //添加到任务处理列表,写在这里主要是因为我们要用事务实现添加视频成功后直接写两个数据表中 addWaitingTask(mediaFiles); log.debug("保存文件信息到数据库成功,{}", mediaFiles.toString()); return mediaFiles; } /** * 添加待处理任务 * @param mediaFiles */ private void addWaitingTask(MediaFiles mediaFiles){ String filename = mediaFiles.getFilename(); String extension = filename.substring(filename.lastIndexOf(".")); String mimeType = getMimeType(extension); if("video/x-msvideo".equals(mimeType)){ //如果我们之后有多种视频自愿我们可以添加配置 然后前面的字符串换成配置中的字符串 MediaProcess mediaProcess = new MediaProcess(); BeanUtils.copyProperties(mediaFiles,mediaProcess); mediaProcess.setStatus("1");//未处理 mediaProcess.setFailCount(0);//失败次数默认为0 mediaProcess.setUrl(null);//最终互联网要播放视频地url mediaProcessMapper.insert(mediaProcess) } }
(2)查询待处理任务
然后我们需要实现查询未处理视频的查询操作需要自定义mapper并且自定义service实现(代码中的where t.id " + "%#{shardToal} = #{shardIndex}是选择特定执行器进行执行,(t.status = '1' or t.status = '3')这个是状态为未转码和转码失败的两个状态,t.fail_count < 3失败次数大于三次我们不再进行转码,limit #{count}限制每个执行器处理的数量)
@Select("select * from media_process t where t.id " + "%#{shardToal} = #{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);
@Override public List<MediaProcess> getMediaProcessList(int shardIndex, int shardTotal, int count) { List<MediaProcess> mediaProcesses = mediaProcessMapper.selectListByShardIndex(shardTotal, shardIndex, count); return mediaProcesses; }
(3)开始执行任务
/** * 但是上面这样仍然可能产生同一个虚拟机(就是执行器)修改同一条数据(视频)的情况,因为我们可能 * 有的虚拟机(执行器)直接挂掉了,这时候我们的执行器数目自动减一,然后我们编号重新变化, * 这时候我们可能同一个虚拟机处理了同一个任务,这时候就需要我们下面的方法,使用乐观锁进行更新。 */
我们应该使用锁解决上面问题,使用·synchronized不行因为这是解决一个虚拟机中多个线程的锁的问题,现在我们要使用分布式锁(实现分布式环境下所有虚拟机中的线程去同步执行就需要让多个虚拟机去共用一个锁,虚拟机可以分布式部署,锁也可以分布式部署)来解决问题。
分布式锁的实现在另一篇博客中有写,这里直接使用乐观锁,下面是代码实现。
1、定义mapper
Java |
2、service方法
Java |
(4)更新任务状态,注意这个是我们第五部执行完成之后要执行的代码,因为第五部比较冗余放在最后。这个方法接收一个参数(第五部产生的status) 然后根据这个参数对相关的数据表进行更新。
任务处理完成需要更新任务处理结果,任务执行成功更新视频的URL、及任务处理结果,将待处理任务记录删除,同时向历史任务表添加记录。
在MediaFileProcessService接口添加方法
Java |
service接口方法实现如下:
Java |
(5)视频处理
视频采用并发处理,每个视频使用一个线程去处理,每次处理的视频数量不要超过cpu核心数。
所有视频处理完成结束本次执行,为防止代码异常出现无限期等待则添加超时设置,到达超时时间还没有处理完成仍结束任务。
定义任务类VideoTask 如下:
Java package com.xuecheng.media.service.jobhandler; import com.xuecheng.base.utils.Mp4VideoUtil; import com.xuecheng.media.model.po.MediaProcess; import com.xuecheng.media.service.MediaFileProcessService; import com.xuecheng.media.service.MediaFileService; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.annotation.XxlJob; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.util.List; import java.util.concurrent.*; /** * 任务处理类 */ @Slf4j @Component public class VideoTask { @Autowired MediaFileProcessService mediaFileProcessService; @Autowired MediaFileService mediaFileService; //ffmpeg的路径 @Value("${videoprocess.ffmpegpath}") private String ffmpegpath; /** * 视频处理任务 */ @XxlJob("videoJobHandler") public void videoJobHandler() throws Exception { // 分片参数 int shardIndex = XxlJobHelper.getShardIndex();//执行器的序号,从0开始 int shardTotal = XxlJobHelper.getShardTotal();//执行器总数 //确定cpu的核心数 int processors = Runtime.getRuntime().availableProcessors(); //查询待处理的任务 List<MediaProcess> mediaProcessList = mediaFileProcessService.getMediaProcessList(shardIndex, shardTotal, processors); //任务数量 int size = mediaProcessList.size(); log.debug("取到视频处理任务数:"+size); if(size<=0){ return; } //创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(size); //使用的计数器 使用计数器是将这一个任务所用线程如果没有完成先阻塞只有全部完成才能够方形 CountDownLatch countDownLatch = new CountDownLatch(size); mediaProcessList.forEach(mediaProcess -> { //将任务加入线程池 executorService.execute(()->{ try { //任务id Long taskId = mediaProcess.getId(); //文件id就是md5 String fileId = mediaProcess.getFileId(); //开启任务 boolean b = mediaFileProcessService.startTask(taskId); if (!b) { log.debug("抢占任务失败,任务id:{}", taskId); return; } //桶 String bucket = mediaProcess.getBucket(); //objectName String objectName = mediaProcess.getFilePath(); //下载minio视频到本地 File file = mediaFileService.downloadFileFromMinIO(bucket, objectName); if (file == null) { log.debug("下载视频出错,任务id:{},bucket:{},objectName:{}", taskId, bucket, objectName); //保存任务处理失败的结果 mediaFileProcessService.saveProcessFinishStatus(taskId, "3", fileId, null, "下载视频到本地失败"); return; } //源avi视频的路径 String video_path = file.getAbsolutePath(); //转换后mp4文件的名称 String mp4_name = fileId + ".mp4"; //转换后mp4文件的路径 //先创建一个临时文件,作为转换后的文件 File mp4File = null; try { mp4File = File.createTempFile("minio", ".mp4"); } catch (IOException e) { log.debug("创建临时文件异常,{}", e.getMessage()); //保存任务处理失败的结果 mediaFileProcessService.saveProcessFinishStatus(taskId, "3", fileId, null, "创建临时文件异常"); return; } String mp4_path = mp4File.getAbsolutePath(); //创建工具类对象 Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpegpath, video_path, mp4_name, mp4_path); //开始视频转换,成功将返回success,失败返回失败原因 String result = videoUtil.generateMp4(); if (!result.equals("success")) { log.debug("视频转码失败,原因:{},bucket:{},objectName:{},", result, bucket, objectName); mediaFileProcessService.saveProcessFinishStatus(taskId, "3", fileId, null, result); return; } //上传到minio String filePath = getFilePath(fileId, ".mp4"); boolean b1 = mediaFileService.addMediaFilesToMinIO(mp4File.getAbsolutePath(), "video/mp4", bucket, filePath); if (!b1) { log.debug("上传mp4到minio失败,taskid:{}", taskId); mediaFileProcessService.saveProcessFinishStatus(taskId, "3", fileId, null, "上传mp4到minio失败"); return; } //访问url String url = "/" + bucket + "/" + filePath; //更新任务状态为成功 mediaFileProcessService.saveProcessFinishStatus(taskId, "2", fileId, url, null); }finally { //计算器减去1 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; } } |
工具类如下
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 = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 String video_path = "D:\\BaiduNetdiskDownload\\test1.avi"; String mp4_name = "test1.mp4"; String mp4folder_path = "D:/BaiduNetdiskDownload/Movies/test1/"; 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 ); commend.add(mp4folder_path ); 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); Boolean check_video_time = this.check_video_time(video_path, mp4folder_path); if(!check_video_time){ return outstring; }else{ return "success"; } } public static void main(String[] args) throws IOException { //ffmpeg的路径 String ffmpeg_path = "D:\\explorer\\ffmpeg\\ffmpeg.exe";//ffmpeg的安装位置 //源avi视频的路径 String video_path = "G:\\Day2-04.课程查询-接口测试.avi"; //转换后mp4文件的名称 String mp4_name = null; //转换后mp4文件的路径 String mp4_path = "G:\\nacos.mp4"; //创建工具类对象 Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path); //开始视频转换,成功将返回success String s = videoUtil.generateMp4(); System.out.println(s); } }