当前位置: 首页 > news >正文

学成在线day06

上传视屏

断点续传

通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。

什么是断点续传:

引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。

断点续传流程如下图:

流程如下:

1、前端上传前先把文件分成块

2、一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传

3、各分块上传完成最后在服务端合并文件

文件分块测试

package com.xuecheng.media;import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
import java.io.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;public class TestBigFile {//文件分块@Testpublic void testChunkUpload() throws Exception{//源文件File sourceFile = new File("D:\\test\\1.mp4");//目标文件位置String ChunkFiles = "D:\\test\\chunk\\";//设置分块大小int chunkSize = 1024 * 1024 * 10;//获取文件要分块的个数Integer size =(int) Math.ceil(sourceFile.length() * 1.0 / chunkSize);System.out.println(size);//使用RandomAccessFile访问文件RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");//创建缓冲区byte[] b = new byte[1024*1024];for (Integer i = 0; i < size; i++) {//创建分块文件File chunkFile = new File(ChunkFiles+i);//判断该文件是否存在if (chunkFile.exists()) {System.out.println("分块文件已存在");//将该文件删除chunkFile.delete();}//创建临时文件boolean newFile = chunkFile.createNewFile();if (newFile){//用于记录本次读取的字节数int len = -1;//使用RandomAccessFile访问文件RandomAccessFile raf_w = new RandomAccessFile(chunkFile, "rw");while ((len = raf_read.read(b))!= -1){//将分块文件写入到输出流raf_w.write(b,0,len);if (chunkFile.length() >= chunkSize){break;}}//关闭流raf_w.close();}}raf_read.close();}//文件合并@Testpublic void testMerge() throws Exception{//源文件File sourceFile = new File("D:\\test\\1.mp4");//分块文件的文件夹File chunkFilesFolder = new File("D:\\test\\chunk");//合并文件位置File mergeFile = new File("D:\\test\\1_test.mp4");//根据文件夹获取该文件夹中的文件列表File[] files = chunkFilesFolder.listFiles();//将数组转化为list集合List<File> fileList = Arrays.asList(files);//集合排序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());}});//合并文件//创建临时文件mergeFile.createNewFile();//创建缓冲区byte[] b = new byte[1024];RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");for (File file : fileList) {//使用RandomAccessFile访问文件RandomAccessFile raf_read = new RandomAccessFile(file, "r");int len = -1;while ((len = raf_read.read(b)) != -1) {//将分块文件写入到合并文件raf_write.write(b, 0, len);}raf_read.close();}raf_write.close();//校验文件String mergeFileMd5 = DigestUtils.md5DigestAsHex(new FileInputStream(mergeFile));String sourceFileMd5 = DigestUtils.md5DigestAsHex(new FileInputStream(sourceFile));if (mergeFileMd5.equals(sourceFileMd5)){System.out.println("合并成功");}}
}

文件分块上传minio,合并文件,使用minio接口来合并文件:

    //文件上传minio@org.junit.jupiter.api.Testpublic void testUploadminio() throws Exception {
//        D:\test\chunkfor (int i = 0; i < 7; i++) {//准备数据UploadObjectArgs testbucket = UploadObjectArgs.builder().bucket("testbucket").object("chunk/"+i)//文件要存放的路径和文件名.filename("D:\\test\\chunk\\"+i).build();minioClient.uploadObject(testbucket);}}//将minio中的文件合并,minio默认文件分块大小要超过5mb@org.junit.jupiter.api.Testpublic void testMerge() throws Exception{List<ComposeSource> sources = new ArrayList<>();for (int i = 0; i < 7; i++) {ComposeSource source = ComposeSource.builder().bucket("testbucket").object("chunk/"+i).build();sources.add(source);}//        sources = (List<ComposeSource>) Stream.iterate(0, i -> ++i).limit(7).map(i -> ComposeSource.builder().bucket("testbucket").object("chunk/" + i).build());ComposeObjectArgs testbucket = ComposeObjectArgs.builder().bucket("testbucket").object("1.mp4").sources(sources).build();minioClient.composeObject(testbucket);}

文件上传相关接口

文件上传前检查

    @ApiOperation(value = "文件上传前检查文件")@PostMapping("/upload/checkfile")public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) throws Exception {RestResponse<Boolean> booleanRestResponse = mediaFileService.checkFile(fileMd5);return booleanRestResponse;}@ApiOperation(value = "分块文件上传前的检测")@PostMapping("/upload/checkchunk")public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {RestResponse<Boolean> booleanRestResponse = mediaFileService.checkChunk(fileMd5, chunk);return booleanRestResponse;}
service层
 /*** @description 检查文件是否存在* @param fileMd5 文件的md5* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:38*/public RestResponse<Boolean> checkFile(String fileMd5);/*** @description 检查分块是否存在* @param fileMd5  文件的md5* @param chunkIndex  分块序号* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:39*/public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex);
    /*** 检查文件是否存在* @param fileMd5 文件的md5* @return*/@Overridepublic RestResponse<Boolean> checkFile(String fileMd5) {//检查数据库中是否存在该文件MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if(mediaFiles != null){//数据库存在则检查minio中是否存在//初始化数据//存储目录String filePath = mediaFiles.getFilePath();//文件流InputStream stream = null;try {GetObjectArgs testbucket = GetObjectArgs.builder().bucket(bucket_video).object(filePath).build();//读取数据获取到输入流stream = minioClient.getObject(testbucket);//判断是否获取到流if (stream != null){return RestResponse.success(true);}} catch (Exception e) {throw new RuntimeException(e);}}return RestResponse.success(false);}/*** 检查分块是否存在* @param fileMd5  文件的md5* @param chunkIndex  分块序号* @return*/@Overridepublic 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_video).object(chunkFilePath).build());if (fileInputStream != null) {//分块已存在return RestResponse.success(true);}} catch (Exception e) {}//分块未存在return RestResponse.success(false);}//获取分块路径//得到分块文件的目录private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";}

文件上传和合并

接口层
@ApiOperation(value = "上传分块文件")@PostMapping("/upload/uploadchunk")public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {//创建临时文件,用来存储分块文件File tempFile = File.createTempFile("minio", "temp");//上传的文件拷贝到临时文件file.transferTo(tempFile);//文件路径String absolutePath = tempFile.getAbsolutePath();log.error("上传文件路径:{}",absolutePath);log.error("上传文件md5:{}",fileMd5);return mediaFileService.uploadChunk(fileMd5, chunk,absolutePath);}@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.setFilename(fileName);uploadFileParamsDto.setFileType("001002");uploadFileParamsDto.setTags("课程视频");uploadFileParamsDto.setRemark("");return mediaFileService.mergechunks(companyId, fileMd5, chunkTotal, uploadFileParamsDto);}
service层
/*** 分块上传文件* @param fileMd5 文件md5* @param chunk 分块序号* @param localChunkFilePath 文件的本地路径* @return*/public RestResponse uploadChunk(String fileMd5,int chunk,String localChunkFilePath);/*** @description 合并分块* @param companyId  机构id* @param fileMd5  文件md5* @param chunkTotal 分块总和* @param uploadFileParamsDto 文件信息* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:56*/public RestResponse mergechunks(Long companyId,String fileMd5,int chunkTotal,UploadFileParamsDto uploadFileParamsDto);

实现:

  /*** 上传分块文件到minio* @param fileMd5 文件md5* @param chunk 分块序号* @param localChunkFilePath 文件的本地路径* @return*/@Overridepublic RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {//获取文件的保存父路径String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//获取文件保存路径String chunkFilePath = chunkFileFolderPath + chunk;//获取文件的mimeTypeString mimeType = getMimeType(null);try {//上传分块文件到minioboolean b = updataFile(mimeType, bucket_video, chunkFilePath, localChunkFilePath);if (!b){log.debug("上传分块文件失败:{}",chunkFilePath);return RestResponse.validfail(false,"上传分块文件失败");}}catch (Exception e){e.printStackTrace();}log.debug("上传分块文件成功:{}",chunkFilePath);return RestResponse.success(true);}/*** @description 合并分块* @param companyId  机构id* @param fileMd5  文件md5* @param chunkTotal 分块总和* @param uploadFileParamsDto 文件信息* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:56*/@Overridepublic RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {//合并分块//获取分块文件路径
//        fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);List<ComposeSource> sources = new ArrayList<>();for (int i = 0; i < chunkTotal; i++) {ComposeSource source = ComposeSource.builder().bucket(bucket_video).object(chunkFileFolderPath+i).build();sources.add(source);}//准备合并后文件保存位置//文件名称String fileName = uploadFileParamsDto.getFilename();//文件扩展名String extName = fileName.substring(fileName.lastIndexOf("."));//合并文件路径String mergeFilePath = getFilePathByMd5(fileMd5, extName);ComposeObjectArgs testbucket = ComposeObjectArgs.builder().bucket(bucket_video).object(mergeFilePath).sources(sources).build();try {minioClient.composeObject(testbucket);} catch (Exception e) {log.error("合并文件失败",e);return RestResponse.validfail(false,"合并文件失败");}//校验文件是否完整//下载已经合并完成的文件File file = downloadFileFromMinIO(bucket_video, mergeFilePath);if(file == null){log.debug("下载合并后文件失败,mergeFilePath:{}",mergeFilePath);return RestResponse.validfail(false, "下载合并后文件失败。");}
//        try {
//            //获取合并后文件的md5
//            String mergeFile_md5 = getFileMd5(file.getAbsolutePath());
//            if (!fileMd5.equals(mergeFile_md5)){
//                //合并后文件的md5与源文件的md5不一致
//                return RestResponse.validfail(false,"文件合并校验失败");
//            }
//        } catch (Exception e) {
//            e.printStackTrace();
//        }try (InputStream newFileInputStream = new FileInputStream(file)) {//minio上文件的md5值String md5Hex = DigestUtils.md5DigestAsHex(newFileInputStream);//比较md5值,不一致则说明文件不完整if(!fileMd5.equals(md5Hex)){return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");}//文件大小uploadFileParamsDto.setFileSize(file.length());}catch (Exception e){log.debug("校验文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");}finally {if(file!=null){file.delete();}}//将文件信息保存到数据库currentProxy.addMediaFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_video,mergeFilePath);//删除分块文件
//        removeChunkFiles(chunkFileFolderPath,chunkTotal);clearChunkFiles(chunkFileFolderPath,chunkTotal);return RestResponse.success(true);}/*** 删除分块文件* @param chunkFileFolderPath 分块文件的位置* @param chunkTotal 分块文件的数量*/private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {List<DeleteObject> sources = new ArrayList<>();for (int i = 0; i < chunkTotal; i++) {DeleteObject source = new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i)));sources.add(source);}//这个方法要真正删除要遍历一下Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucket_video).objects(sources).build());results.forEach(r->{DeleteError deleteError = null;try {deleteError = r.get();} catch (Exception e) {e.printStackTrace();log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);}});}/*** 从minio中下载文件* @param bucket* @param objectName* @return*/public File downloadFileFromMinIO(String bucket,String objectName){//创建临时文件File minioFile = null;FileOutputStream  outputStream = null;try {GetObjectArgs testbucket = GetObjectArgs.builder().bucket(bucket).object(objectName).build();//读取数据获取到输入流InputStream  stream = minioClient.getObject(testbucket);//创建临时文件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) {throw new RuntimeException(e);}}}return null;}//获取分块路径//得到分块文件的目录private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";}/*** 得到合并后的文件的地址* @param fileMd5 文件id即md5值* @param fileExt 文件扩展名* @return*/private String getFilePathByMd5(String fileMd5,String fileExt){return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;}/*** @description 将文件信息添加到文件表* @param companyId  机构id* @param fileMd5  文件md5值* @param uploadFileParamsDto  上传文件的信息* @param bucket  桶* @param objectName 对象名称* @return com.xuecheng.media.model.po.MediaFiles* @author Mr.M* @date 2022/10/12 21:22*/@Transactionalpublic 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);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);if (insert < 0) {log.error("保存文件信息到数据库失败,{}",mediaFiles.toString());XueChengPlusException.cast("保存文件信息失败");}log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString());}return mediaFiles;}

面试

5.6面试
1、什么情况Spring事务会失效
1)在方法中捕获异常没有抛出去
2)非事务方法调用事务方法
3)事务方法部调用事务方法
4)@Transactional标记的方法不是public
5)抛出的异常与rollbackFor指定的异常不匹配,默认rollbackFor指定的异常为RuntimeException
6)数据库表不支持事务,比如MySQL的MyISAM
7)Spring的传播行为导致事务失效,比如:PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED
PROPAGATION REQUIRED-支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION SUPPORTS-支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION MANDATORY-支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION REQUIRES NEW-新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION NOT_SUPPORTED-以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER-以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION NESTED-如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则与
PROPAGATION REQUIRED类似的操作。

4、断点续传是怎么做的?
我们是基于分块上传的模式实现断点续传的需求,当文件上传一部分断网后前边已经上传过的不再上传。
1)前端对文件分块。
2)前端使用多线程一块一块上传,上传前给服务端发一个消息校验该分块是否上传,如果已上传则不再上传。
3)等所有分块上传完毕,服务端合并所有分块,校验文件的完整性。
因为分块全部上传到了服务器,服务器将所有分块按顺序进行合并,就是写每个分块文件内容按顺序依次写入一个
文件中。使用字节流去读写文件。
4)前端给服务传了一个md5值,服务端合并文件后计算合并后文件的md5是否和前端传的一样,如果一样则说文
件完整,如果不一样说明可能由于网络丢包导致文件不完整,这时上传失败需要重新上传。
5、分块文件清理问题?
上传一个文件进行分块上传,上传一半不传了,之前上传到miio的分块文件要清理吗?怎么做的?
1、在数据库中有一张文件表记录minio中存储的文件信息。
2、文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成。
3、当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未
上传完成则删除minio中没有上传成功的文件目录。

视频处理

视频编码

视频上传成功后需要对视频进行转码处理。

什么是视频编码?查阅百度百科如下:

详情参考 :视频编码_百度百科

首先我们要分清文件格式和编码格式:

文件格式:是指.mp4、.avi、.rmvb等 这些不同扩展名的视频文件的文件格式 ,视频文件的内容主要包括视频和音频,其文件格式是按照一 定的编码格式去编码,并且按照该文件所规定的封装格式将视频、音频、字幕等信息封装在一起,播放器会根据它们的封装格式去提取出编码,然后由播放器解码,最终播放音视频。

音视频编码格式:通过音视频的压缩技术,将视频格式转换成另一种视频格式,通过视频编码实现流媒体的传输。比如:一个.avi的视频文件原来的编码是a,通过编码后编码格式变为b,音频原来为c,通过编码后变为d。

音视频编码格式各类繁多,主要有几下几类:

MPEG系列

(由ISO[国际标准组织机构]下属的MPEG[运动图象专家组]开发 )视频编码方面主要是Mpeg1(vcd用的就是它)、Mpeg2(DVD使用)、Mpeg4(的DVDRIP使用的都是它的变种,如:divx,xvid等)、Mpeg4 AVC(正热门);音频编码方面主要是MPEG Audio Layer 1/2、MPEG Audio Layer 3(大名鼎鼎的mp3)、MPEG-2 AAC 、MPEG-4 AAC等等。注意:DVD音频没有采用Mpeg的。

H.26X系列

(由ITU[国际电传视讯联盟]主导,侧重网络传输,注意:只是视频编码)

包括H.261、H.262、H.263、H.263+、H.263++、H.264(就是MPEG4 AVC-合作的结晶)

目前最常用的编码标准是视频H.264,音频AAC。

提问:

H.264是编码格式还是文件格式?

mp4是编码格式还是文件格式?

FFmpeg 的基本使用

我们将视频录制完成后,使用视频编码软件对视频进行编码,本项目 使用FFmpeg对视频进行编码 。

FFmpeg被许多开源项目采用,QQ影音、暴风影音、VLC等。

下载:FFmpeg Download FFmpeg

请从常用工具软件目录找到ffmpeg.exe,并将ffmpeg.exe加入环境变量path中。

测试是否正常:cmd运行 ffmpeg -version

安装成功,作下简单测试

将一个.avi文件转成mp4、mp3、gif等。

比如我们将nacos.avi文件转成mp4,运行如下命令:

D:\soft\ffmpeg\ffmpeg.exe -i 1.avi 1.mp4

可以将ffmpeg.exe配置到环境变量path中,进入视频目录直接运行:ffmpeg.exe -i 1.avi 1.mp4

转成mp3:ffmpeg -i nacos.avi nacos.mp3

转成gif:ffmpeg -i nacos.avi nacos.gif

官方文档(英文):ffmpeg Documentation

视频处理工具类

将课程资料的工具类中的util拷贝至base工程。

其中Mp4VideoUtil类是用于将视频转为mp4格式,是我们项目要使用的工具类。

对Mp4VideoUtil类需要学习使用方法,下边代码将一个avi视频转为mp4视频,如下:

    public static void main(String[] args) throws IOException {//ffmpeg的路径String ffmpeg_path = "D:\\yingyong\\tool\\ffmpeg\\ffmpeg.exe";//ffmpeg的安装位置//源avi视频的路径String video_path = "D:\\test\\1.avi";//转换后mp4文件的名称String mp4_name = "test1.mp4";//转换后mp4文件的路径String mp4_path = "D:\\test\\1a.mp4";//创建工具类对象Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);//开始视频转换,成功将返回successString s = videoUtil.generateMp4();System.out.println(s);}

执行main方法,最终在控制台输出 success 表示执行成功。

分布式任务处理

什么是分布式任务调度

对一个视频的转码可以理解为一个任务的执行,如果视频的数量比较多,如何去高效处理一批任务呢?

1、多线程

多线程是充分利用单机的资源。

2、分布式加多线程

充分利用多台计算机,每台计算机使用多线程处理。

方案2可扩展性更强。

方案2是一种分布式任务调度的处理方案。

什么是分布式任务调度?

我们可以先思考一下下面业务场景的解决方案:

每隔24小时执行数据备份任务。

12306网站会根据车次不同,设置几个时间点分批次放票。

某财务系统需要在每天上午10点前结算前一天的账单数据,统计汇总。

商品成功发货后,需要向客户发送短信提醒。

类似的场景还有很多,我们该如何实现?

多线程方式实现:

学过多线程的同学,可能会想到,我们可以开启一个线程,每sleep一段时间,就去检查是否已到预期执行时间。

以下代码简单实现了任务调度的功能:

public static void main(String[] args) {    //任务执行间隔时间final long timeInterval = 1000;Runnable runnable = new Runnable() {public void run() {while (true) {//TODO:somethingtry {Thread.sleep(timeInterval);} catch (InterruptedException e) {e.printStackTrace();}}}};Thread thread = new Thread(runnable);thread.start();
}

上面的代码实现了按一定的间隔时间执行任务调度的功能。

Jdk也为我们提供了相关支持,如Timer、ScheduledExecutor,下边我们了解下。

Timer方式实现

public static void main(String[] args){  Timer timer = new Timer();  timer.schedule(new TimerTask(){@Override  public void run() {  //TODO:something}  }, 1000, 2000);  //1秒后开始调度,每2秒执行一次
}

Timer 的优点在于简单易用,每个Timer对应一个线程,因此可以同时启动多个Timer并行执行多个任务,同一个Timer中的任务是串行执行。

ScheduledExecutor方式实现

public static void main(String [] agrs){ScheduledExecutorService service = Executors.newScheduledThreadPool(10);service.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {//TODO:somethingSystem.out.println("todo something");}}, 1,2, TimeUnit.SECONDS);
}

Java 5 推出了基于线程池设计的 ScheduledExecutor,其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。

Timer 和 ScheduledExecutor 都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。比如,设置每月第一天凌晨1点执行任务、复杂调度任务的管理、任务间传递数据等等。

第三方Quartz方式实现,项目地址:https://github.com/quartz-scheduler/quartz

Quartz 是一个功能强大的任务调度框架,它可以满足更多更复杂的调度需求,Quartz 设计的核心类包括 Scheduler, Job 以及 Trigger。其中,Job 负责定义需要执行的任务,Trigger 负责设置调度策略,Scheduler 将二者组装在一起,并触发任务开始执行。Quartz支持简单的按时间间隔调度、还支持按日历调度方式,通过设置CronTrigger表达式(包括:秒、分、时、日、月、周、年)进行任务调度。

下边是一个例子代码

public static void main(String [] agrs) throws SchedulerException {//创建一个SchedulerSchedulerFactory schedulerFactory = new StdSchedulerFactory();Scheduler scheduler = schedulerFactory.getScheduler();//创建JobDetailJobBuilder jobDetailBuilder = JobBuilder.newJob(MyJob.class);jobDetailBuilder.withIdentity("jobName","jobGroupName");JobDetail jobDetail = jobDetailBuilder.build();//创建触发的CronTrigger 支持按日历调度CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("triggerName", "triggerGroupName").startNow().withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")).build();scheduler.scheduleJob(jobDetail,trigger);scheduler.start();
}public class MyJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext){System.out.println("todo something");}
}

通过以上内容我们学习了什么是任务调度,任务调度所解决的问题,以及任务调度的多种实现方式。

任务调度顾名思义,就是对任务的调度,它是指系统为了完成特定业务,基于给定时间点,给定时间间隔或者给定执行次数自动执行任务。

什么是分布式任务调度?

通常任务调度的程序是集成在应用中的,比如:优惠卷服务中包括了定时发放优惠卷的的调度程序,结算服务中包括了定期生成报表的任务调度程序,由于采用分布式架构,一个服务往往会部署多个冗余实例来运行我们的业务,在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度,如下图:

分布式调度要实现的目标:

不管是任务调度程序集成在应用程序中,还是单独构建的任务调度系统,如果采用分布式调度任务的方式就相当于将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力:

1、并行任务调度

并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。

如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。

2、高可用

若某一个实例宕机,不影响其他实例来执行任务。

3、弹性扩容

当集群中增加实例就可以提高并执行任务的处理效率。

4、任务管理与监测

对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。

5、避免任务重复执行

当任务调度以集群方式部署,同一个任务调度可能会执行多次,比如在上面提到的电商系统中到点发优惠券的例子,就会发放多次优惠券,对公司造成很多损失,所以我们需要控制相同的任务在多个运行实例上只执行一次。

XXL-JOB介绍

XXL-JOB配置详情:https://mx67xggunk5.feishu.cn/wiki/SiQAwJ99MiP7w9kZWaicTacKn0g

相关文章:

学成在线day06

上传视屏 断点续传 通常视频文件都比较大&#xff0c;所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制&#xff0c;但是客户的网络环境质量、电脑硬件环境等参差不齐&#xff0c;如果一个大文件快上传完了网断了没有上传完成&…...

Mac安装及合规无限使用Beyond Compare

文章目录 Beyond CompareBeyond Compare简介Beyond Compare安装Beyond Compare到期后继续免费使用 Beyond Compare Beyond Compare简介 Beyond Compare 是一款由 Scooter Software 开发的文件和文件夹比较工具。它主要用于对比两个文件或文件夹之间的差异&#xff0c;并支持文…...

【青牛科技】2K02 电动工具专用调速电路芯片描述

概述&#xff1a; 2K02 是电动工具专用调速电路。内置稳压电路&#xff0c;温度系数好&#xff0c;可以调节输出频率以及占空比的振荡输出&#xff0c;广泛的应用于小型电钻&#xff0c;割草机等工具。 主要特点&#xff1a; ● 电源电压范围宽 ● 占空比可调 ● 温度系数好 …...

基于SpringBoot实现的民宿管理系统(代码+论文)

&#x1f389;博主介绍&#xff1a;Java领域优质创作者&#xff0c;阿里云博客专家&#xff0c;计算机毕设实战导师。专注Java项目实战、毕设定制/协助 &#x1f4e2;主要服务内容&#xff1a;选题定题、开题报告、任务书、程序开发、项目定制、论文辅导 &#x1f496;精彩专栏…...

安装QT6.8(MSVC MinGW)+QT webengine+QT5.15.2

本篇主要针对只使用过QT5的qmake&#xff0c;没有用过MSVC&#xff0c;VS的老同学。 建议一部分一部分安装&#xff0c;全部勾选安装遇到问题会中断&#xff0c;前功尽弃。 我自己需要的是QT5&#xff0c;编出的软件用在公司设备上。 QT6&#xff1a;建议也安装学习&#xf…...

MinIO常见操作及Python实现对象的增删改查

MinIO常见操作 MinIO是一个高性能的开源对象存储服务&#xff0c;它兼容Amazon S3云存储服务API。在MinIO中&#xff0c;常见的操作包括&#xff1a; 存储桶操作&#xff1a; 创建、列出、获取信息、删除存储桶。 对象操作&#xff1a; 上传、下载、列出、删除对象。 权限管理&…...

网络编程中的字节序函数htonl()、htons()、ntohl()和ntohs()

目录 1&#xff0c;网络字节序和主机字节序 2. 函数的具体作用 2.1,htonl&#xff08;Host to Network Long&#xff09; 2.2,htons&#xff08;Host to Network Short) 2.3,ntohl&#xff08;Network to Host Long&#xff09; 2.4,ntohs&#xff08;Network to Host Sho…...

【dvwa靶场:File Upload系列】File Upload低-中-高级别,通关啦

目录 一、low级别,直接上传木马文件 1.1、准备一个木马文件 1.2、直接上传木马文件 1.3、访问木马链接 1.4、连接蚁剑 二、medium级别&#xff1a;抓包文件缀名 2.1、准备一个木马文件&#xff0c;修改后缀名为图片的后缀名 2.2、上传文件&#xff0c;打开burpSuite&…...

RHCE NFS

RHCE NFS 1.11. 2 NFS 主机名格式1.3 NFS 服务器配置1.3.1 /etc/exports 配置文件1.3.1.1 导出条目1.3.1.2 默认选项1.3.1.3 默认和覆盖选项 1.4 启动 NFS 服务器1.5 练习1.5.1 配置 NFS 服务器和客户端挂载1.5.2 配置autofs自动挂载&#xff08;需要时才挂载&#xff09; 1.6 …...

【数据结构】ArrayList与顺序表

ArrayList与顺序表 1.线性表2.顺序表2.1 接口的实现 3. ArrayList简介4. ArrayList使用4.2 ArrayList常见操作4.3 ArrayList的遍历4.4 ArrayList的扩容机制 5. ArrayList的具体使用5.1 杨辉三角5.2 简单的洗牌算法 6. ArrayList的问题及思考 【本节目标】 线性表顺序表ArrayLis…...

互联网基础

TCP/IP协议&#xff08;协议组&#xff09; 分层名称TCP/IP协议应用层HTTP,FTP,mDNS,WebSocket,OSC...传输层TCP&#xff0c;UDP网络层IP链路层&#xff08;网络接口层&#xff09;Ethernet&#xff0c;Wi-Fi... 链路层&#xff08;网络接口层&#xff09; 链路层的主要作用…...

ffmpeg.js视频播放(转换)

chrome 临时设置SharedArrayBuffer "C:\Program Files\Google\Chrome\Application\chrome.exe" --enable-featuresSharedArrayBuffer 引用的js及相关文件 ffmpeg.min.js ffmpeg.min.js.map ffmpeg-core.js ffmpeg-core.wasm ffmpeg-core.worker.js 以上几个现…...

后端 Java发送邮件 JavaMail 模版 20241128测试可用

配置授权码 依赖 <dependency><groupId>javax.mail</groupId><artifactId>javax.mail-api</artifactId><version>1.5.5</version> </dependency> <dependency><groupId>com.sun.mail</groupId><artifa…...

电脑中的vcruntime140_1.dll文件有问题要怎么解决?一键修复vcruntime140_1.dll

遇到“vcruntime140_1.dll无法继续执行代码”的错误通常表明电脑中的vcruntime140_1.dll文件有问题。这个文件属于Visual C Redistributable&#xff0c;对很多程序的运行至关重要。本文将提供几个步骤&#xff0c;帮助你迅速修复这一错误&#xff0c;使电脑恢复正常工作状态。…...

探索 Vue 3.0中Treeshaking特性?

Vue 3.0 中的 Tree Shaking 特性 Tree Shaking 是一种优化技术,旨在通过静态分析代码,去除未使用的模块或函数,从而减小最终的打包文件大小。在 Vue 3.0 中,由于其模块化设计和代码按需引入的特性,Vue 的 Tree Shaking 特性得到了增强,能够有效地去除那些在生产环境中未…...

Paddle Inference部署推理(十)

十&#xff1a;Paddle Inference推理 &#xff08;python&#xff09;API详解 9. 启用内存优化 API定义如下&#xff1a; # 开启内存 / 显存复用&#xff0c;具体降低内存效果取决于模型结构 # 参数&#xff1a;None # 返回&#xff1a;None paddle.inference.Config.enable…...

万能门店小程序管理系统 doPageGetFormList SQL注入漏洞复现

0x01 产品简介 万能门店小程序管理系统是一款功能强大的工具,旨在为各行业商家提供线上线下融合的全方位解决方案。是一个集成了会员管理和会员营销两大核心功能的综合性平台。它支持多行业使用,通过后台一键切换版本,满足不同行业商家的个性化需求。该系统采用轻量后台,搭…...

全面+彻底解决VMware安装后没有VMnet1和VMnet8的问题

目录 1、摘要 &#xff08;1&#xff09;问题 &#xff08;2&#xff09;所用工具 ① Everything软件 ② CCleaner软件 2、问题的检查与确认 3、解决过程 &#xff08;1&#xff09;卸载已经安装的VMware &#xff08;2&#xff09;设置services.mcs&#xff1a;服务自…...

什么是堆?

堆&#xff08;Heap&#xff09;&#xff1a;堆可以看做是一颗用数组实现的二叉树&#xff0c;所以它没有使用父指针或者子指针。堆根据“堆属性”来排序&#xff0c;“堆属性”决定了树中节点的位置。 堆的特性 1.堆是完全二叉树&#xff0c;除了树的最后一层节点不需要是满的…...

微距动物和植物摄影后期森系风格Lr调色教程,手机滤镜PS+Lightroom预设下载!

调色教程 微距动物和植物摄影后期采用森系风格的 Lightroom 调色&#xff0c;将微距下的动植物世界打造成充满自然气息和梦幻感的画面。这种调色风格旨在突出动植物的细腻之美&#xff0c;同时营造出宁静、清新的森林氛围。 预设信息 调色风格&#xff1a;森系风格预设适合类…...

Qt6.8安卓Android开发环境配置

时隔多年&#xff0c;重拾QtCreator下Android开发。发现Qt6下安卓开发环境配置变简单不少&#xff01;只需三步即可在QtCreator下进行Android开发&#xff1a; 一、使用Qt Mantenance Tool进行Android模块的安装&#xff1a; 如果感觉安装网速较慢&#xff0c;可以查看本人另外…...

RK3568部署yolo8记录

本教程记录自己一下在RK3568上部署yolo8的步骤 板端驱动 在板端&#xff0c;首先查看rknpu驱动是否安装、存在。若键入下面的命令有返回则&#xff0c;证明驱动已安装。 dmesg | grep -i rknpu 瑞芯微官方说&#xff0c;驱动版本最好大于0.9.2。但是我看有的博主说&#xff…...

数据可视化复习2-绘制折线图+条形图(叠加条形图,并列条形图,水平条形图)+ 饼状图 + 直方图

目录 目录 一、绘制折线图 1.使用pyplot 2.使用numpy ​编辑 3.使用DataFrame ​编辑 二、绘制条形图&#xff08;柱状图&#xff09; 1.简单条形图 2.绘制叠加条形图 3.绘制并列条形图 4.水平条形图 ​编辑 三、绘制饼状图 四、绘制散点图和直方图 1.散点图 2…...

JavaScript原生深拷贝方法 structuredClone使用

structuredClone 简介 structuredClone 是现代浏览器提供的原生 JavaScript 方法&#xff0c;用于深拷贝对象。它可以处理各种复杂数据结构&#xff0c;包括嵌套对象、数组、Date、Map、Set 等&#xff0c;且支持循环引用。 语法 const clone structuredClone(value);value:…...

SpringBoot无法使用jkd8问题

1. 解决SpringBoot无法使用jdk8问题 创建一个高 jkd 版本&#xff0c;如 jkd21 在创建项目后&#xff0c;将 pom.xml中的 jdk 版本改为8&#xff0c;找到下图所在位置修改即可。 此外将 SpringBoot 的版本修改为 2 开头的 如2.7.4 &#xff0c;然后 刷新 Maven 项目即可。 在 …...

使用 Jina Embeddings v2 在 Elasticsearch 中进行后期分块

作者&#xff1a;来自 Elastic Gustavo Llermaly 在 Elasticsearch 中使用 Jina Embeddings v2 模型并探索长上下文嵌入模型的优缺点。 在本文中&#xff0c;我们将配置和使用 jina-embeddings-v2&#xff0c;这是第一个开源 8K 上下文长度嵌入模型&#xff0c;首先使用 semant…...

QT简易项目 数据库可视化界面 数据库编程SQLITE QT5.12.3环境 C++实现

案例需求&#xff1a; 完成数据库插入&#xff0c;删除&#xff0c;修改&#xff0c;查看操作。 分为 插入&#xff0c;删除&#xff0c;修改&#xff0c;查看&#xff0c;查询 几个模块。 代码&#xff1a; widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget…...

python json.dump()和json.dumps()的区别

用人话总结一下 json.dump()是针对文件的json和python的转换 json.dumps()主要是针对内容数据 json.dumps(obj, skipkeysFalse, ensure_asciiTrue, check_circularTrue, allow_nanTrue, clsNone, indentNone, separatorsNone, encoding“utf-8”, defaultNone, sort_keysFalse…...

网络流学习笔记

注&#xff1a;笔者是蒟蒻&#xff0c;所以本文几乎是干货&#xff0c;枯燥无味甚至可能会引人不适&#xff0c;请读者谨慎阅读。 为了笔者快爆掉的肝点个赞好吗&#xff1f;&#xff1f;&#xff1f; Part.1 网络流基础定义 一个有向带权图 G ( V , E ) G(V,E) G(V,E) 是…...

Mybatis PLUS查询对List使用OR模糊查询

Mybatis PLUS查询对List使用OR模糊查询 1、版本2、代码3、效果 1、版本 Mybatis PLUS版本&#xff1a;3.5.7 注意&#xff1a;版本3.1.2及以下是需要return的 因当前为高版本&#xff0c;代码中已将 return 注释。 2、代码 QueryWrapper<Object> queryWrapper new Que…...

哪个网站专业做安防/什么网站都能打开的浏览器

gstreamer及相关插件编译完成后&#xff0c;会输出gst-inspect可执行文件&#xff0c;相关信息如下&#xff1a; drwxrwxr-x 12 yingc yingc 4096 6月 15 16:56 glib-2.42.0/ drwxrwxr-x 10 yingc yingc 4096 6月 15 17:05 gst-libav-1.4.5/ drwxrwxr-x 14 yingc yingc …...

中国洛阳网/重庆seo排名技术

基于对生物进化机制的模仿&#xff0c;共产生进化算法的四种典型模型&#xff1a; ①遗传算法 Genetic Algorithm,GA ②进化规划 Evolutionary Programming,EP ③遗传规划 Genetic Programming,GP ④进化策略 Evolution Strategy, E 一、遗传算法&#xff08;GA&#xff09; 遗…...

wordpress lbs插件/网络营销方法和手段

转自于&#xff1a;http://www.cnblogs.com/shanno/p/3958298.html?utm_sourcetuicool 数据分布是分布式存储系统的一个重要部分&#xff0c;数据分布算法至少要考虑以下三个因素&#xff1a; 1) 故障域隔离。同份数据的不同副本分布在不同的故障域&#xff0c;降低数据损坏的…...

网站开发基础知识/百度网站大全首页

以下是一些开发中会经常用到的宏,简单的进行了整理,为了今后可以更加方便的使用,从而提升开发的效率,不为此搭进去更多时间. 也希望有大家可以补充,从而使其更加强加! /*** 开发常用宏相关*/#ifndef FYHCommonMacros_h #define FYHCommonMacros_h//开发的时候打印&#xff0c;…...

wordpress怎么改登陆/西安seo网站关键词优化

由于汇编语言针对特定机器&#xff0c;因此给出大端/小端机器下的2种不同代码 实现思路 使用shlb依次移出每一位到CF使用setb将CF的值转移到cl将rcx左移/循环右移8位&#xff0c;视机器不同而不同重复上述操作8次 64位小端机器 char* chr2bin(uint8_t ch, char buf[8*sizeof…...

合肥网站建设ijwww/搜索引擎优化的主要工作有

原文地址: The Javascript Runtime Environment 原文作者: Jamie Uttariello 译者语&#xff1a; 本文是在学习的过程中发现的一篇讲述JS机制比较明了的文章&#xff0c;因此尝试翻译了一下。 不是专业的&#xff0c;因此难免有偏颇&#xff0c;欢迎交流指正。 复制代码 通过本…...