当前位置: 首页 > 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;如果一个大文件快上传完了网断了没有上传完成&…...

详细介绍HTTP与RPC:为什么有了HTTP,还需要RPC?

目录 一、HTTP 二、RPC 介绍 工作原理 核心功能 如何服务寻址 如何进行序列化和反序列化 如何网络传输 基于 TCP 协议的 RPC 调用 基于 HTTP 协议的 RPC 调用 实现方式 优点和缺点 使用场景 常见框架 示例 三、问题 问题一&#xff1a;是先有HTTP还是先有RPC&…...

ffmpeg 各版本号对应表格

想看看ffmpeg各个版本对应表&#xff0c; #! /bin/bashFF_PATH$1 CURRENTpwd RESULT"$CURRENT/test_version.txt"cd $FF_PATHif [ -f $RESULT ]; thenrm $RESULT fifor i in git branch -a | grep remotes/origin/release/ | grep -v HEAD | grep -v master; dogit…...

cesium 3Dtiles变量

原本有一个变亮的属性luminanceAtZenith&#xff0c;但是新版本的cesium没有这个属性了。于是 let lightColor 3.0result._customShader new this.ffCesium.Cesium.CustomShader({fragmentShaderText:void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial mate…...

如何分析Windows防火墙日志

Windows防火墙&#xff0c;也被称为Windows Defender Firewall&#xff0c;是一种内置的安全功能&#xff0c;可以主动监控和分析运行Windows操作系统的计算机上通过Windows防火墙的网络流量&#xff0c;主要目的是作为计算机和互联网或其他网络之间的屏障&#xff0c;使管理员…...

Linux下 history 命令输出时间

在 Linux 中&#xff0c;查看每条命令的执行时间。 文章目录 [toc]**1. 配置 Shell 以记录命令执行时间****1.1 Bash Shell****步骤&#xff1a;****注意事项&#xff1a;** **1.2 Zsh Shell****步骤&#xff1a;****注意事项&#xff1a;** 1. 配置 Shell 以记录命令执行时间 …...

ChatGPT/AI辅助网络安全运营之-数据解压缩

在网络安全的世界中&#xff0c;经常会遇到各种压缩的数据&#xff0c;比如zip压缩&#xff0c;比如bzip2压缩&#xff0c;gzip压缩&#xff0c;xz压缩&#xff0c;7z压缩等。网络安全运营中需要对这些不同的压缩数据进行解压缩&#xff0c;解读其本意&#xff0c;本文将探索一…...

导入 OpenCV for Android 的技巧

下载了 OpenCV for Android Sdk 以后&#xff0c;一头雾水&#xff0c;不知道从哪里下手&#xff0c;既不是jar、也不是aar&#xff0c;没关系&#xff0c;简单几步即可使用 OpenCV。 1、使用 Android Studio 打开 samples &#xff08;示例&#xff09;项目 2、同步项目&…...

云原生时代的轻量级反向代理Traefik

Traefik 是一个用于路由和管理网络流量的反向代理&#xff0c;同时也是一个支持多种协议&#xff08;HTTP、HTTPS、TCP、UDP&#xff09;的负载均衡器。它通过自动服务发现和动态配置&#xff0c;帮助开发者和运维团队轻松管理复杂的应用架构。 Traefik 的主要特点如下&#x…...

3D扫描对文博行业有哪些影响?

三维扫描技术对文博行业产生了深远的影响&#xff0c;主要体现在以下几个方面&#xff1a; 一、高精度建模与数字化保护 三维扫描技术通过高精度扫描设备&#xff0c;能够捕捉到文物的每一个细节&#xff0c;包括形状、纹理、颜色等&#xff0c;从而生成逼真的3D模型。这些模…...

linux安全管理-会话安全

文章目录 1 设置命令行界面超时退出2 配置终端登录失败策略3 配置 SSH 登录失败策略 1 设置命令行界面超时退出 1、检查内容 检查操作系统是否设置命令行界面超时退出。 2、配置要求 操作系统设置命令行界面超时退出。 3、配置方法 配置命令行界面超时时间&#xff0c;编辑/et…...

未来已来?AI技术革新改变我们的生活

在21世纪的今天&#xff0c;人工智能&#xff08;AI&#xff09;不再是一个遥远的概念&#xff0c;而是逐渐渗透到我们生活的方方面面。从智能家居到自动驾驶汽车&#xff0c;从个性化推荐系统到医疗诊断辅助&#xff0c;AI技术正在以惊人的速度发展&#xff0c;并深刻地影响着…...

列表上移下移功能实现

后台管理某列表需实现上移下移功能&#xff0c;并与前端展示列表排序相关。 现将开发完成过程笔记记录下来。 目录 列表增加属性 JQuery脚本 服务端 控制器 服务层 总结 列表增加属性 在循环渲染时&#xff0c;在table表格的tr上增加id和排序的属性值&#xff0c;以便传…...

[保姆式教程]使用labelimg2软件标注定向目标检测数据和格式转换

定向目标检测是一种在图像或视频中识别和定位对象的同时&#xff0c;还估计它们方向的技术。这种技术特别适用于处理有一定旋转或方向变化的对象&#xff0c;例如汽车、飞机或文本。定向目标检测器的输出是一组旋转的边界框&#xff0c;这些框精确地包围了图像中的对象&#xf…...

qt音频实战

一、Qt音频基础知识 1、QT multimedia 2、QMediaPlayer类&#xff1a;媒体播放器&#xff0c;主要用于播放歌曲、网络收音机等功能。 3、QMediaPlaylist类&#xff1a;专用于播放媒体内容的列表。 二、界面设计 三、代码 #include "mainwindow.h" #include "…...

【C++】static修饰的“静态成员函数“--静态成员在哪定义?静态成员函数的作用?

声明为static的类成员称为类的静态成员&#xff0c;用static修饰的成员变量&#xff0c;称之为静态成员变量&#xff1b;用 static修饰的成员函数&#xff0c;称之为静态成员函数。静态成员变量一定要在类外进行初始化 一、静态成员变量 1)特性 所有静态成员为所有类对象所共…...

『Linux学习笔记』linux系统有哪些方法计算文件的md5!

linux系统有哪些方法计算文件的md5&#xff01; 文章目录 一. linux系统有哪些方法计算文件的md5&#xff01;1. 使用 md5sum 命令(推荐)示例&#xff1a;输出&#xff1a;使用方法&#xff1a; 2. 使用 openssl 命令计算MD5值&#xff1a;输出&#xff1a;使用方法&#xff1…...

css vue vxe-text-ellipsis table 实现多行文本超出隐藏省略

分享 vxe-text-ellipsis table grid 多行文本溢出省略的用法 正常情况下如果需要使用文本超出隐藏&#xff0c;通过 css 就可以完成 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;但是如果需要实现多行文本溢出&#xff0c;就很难实现里&#xff0c;谷歌…...

构建现代Web应用:FastAPI、SQLModel、Vue 3与Axios的结合使用

FastAPI介绍 FastAPI是一个用于构建API的现代、快速&#xff08;高性能&#xff09;的Web框架&#xff0c;使用Python并基于标准的Python类型提示。它的关键特性包括快速性能、高效编码、减少bug、智能编辑器支持、简单易学、简短代码、健壮性以及标准化。FastAPI自动提供了交互…...

图像边界填充算法详解与Python实现

目录 图像边界填充算法详解与实现1. 基础概念1.1 边界填充的意义与应用场景1.2 常见填充策略概览2. 零填充算法(Zero Padding)2.1 理论介绍2.2 Python实现及代码详解2.3 案例分析3. 镜像填充算法(Mirror Padding)3.1 理论介绍3.2 Python实现及代码详解3.3 案例分析4. 重复填…...

中兴机顶盒B860AV1.1刷机固件升级和教程「适用4/8G版」

准备工作&#xff1a; TTL 线&#xff08;CH340G 按系统版本找到要对应驱动&#xff09;下载 putty 软件拆开电视盒接好 TTL 线&#xff08;2、5、6 针脚对应GND、RX、TX&#xff09;在资源管理器的端口选项下找到 CH340G&#xff0c;记住端口号&#xff08;如 COM4&#xff0…...

JVM 性能调优 -- CMS 垃圾回收器 GC 日志分析【Full GC】

前言&#xff1a; 上一篇我们分析了 Minor GC 的发生过程&#xff0c;因为 GC 日志没有按我们预估的思路进行打印&#xff0c;其中打印了 CMS 垃圾回收器的部分日志&#xff0c;本篇我们就来分析一下 CMS 垃圾收集日志。 JVM 系列文章传送门 初识 JVM&#xff08;Java 虚拟机…...

重塑视频新语言,让每一帧都焕发新生——Video-Retalking,开启数字人沉浸式交流新纪元!

模型简介 Video-Retalking 模型是一种基于深度学习的视频再谈话技术&#xff0c;它通过分析视频中的音频和图像信息&#xff0c;实现视频角色口型、表情乃至肢体动作的精准控制与合成。这一技术的实现依赖于强大的技术架构和核心算法&#xff0c;特别是生成对抗网络&#xff0…...

C#中面试的常见问题001

1、c#访问修饰符有哪些 public&#xff1a;公共访问级别&#xff0c;成员可以被任何其他代码访问。private&#xff1a;私有访问级别&#xff0c;成员只能在定义它的类内部访问。protected&#xff1a;受保护的访问级别&#xff0c;成员可以被定义它的类及其子类访问。interna…...

webGis 气象站点数据解析渲染

1.站点数据说明 1.1 数据来源 站点数据来源多样。 1.2数据传输 实现前端的展示&#xff0c;数据传输的方式有&#xff1a; json等等 1.2数据格式 let arr [{ lat:1,//经纬度 lng:1, value:2//值 },{},...] 1.3站点数据转格点数据 turf.interpolate克里金插值qgis等ID…...

Vue3+Typescript+Axios+.NetCore实现导出Excel文件功能

前端代码 //导出Excel const exportMaintenanceOrderSettlementItemExcelClick async () > {let url ${VITE_APP_API_URL}/api/app/maintenance/settlement-service-item/${currentMaintenanceOrderId.value}/${currentMaintenanceOrderSettlementRow.value.id};let file…...

专属主机服务器和ECS服务器有什么区别?

‌专属主机服务器和ECS服务器的主要区别在于资源隔离、计费方式、管理权限等方面。‌ 资源隔离 ‌专属主机服务器‌&#xff1a;用户可以独享整台物理服务器资源&#xff0c;与其他租户的服务器物理隔离。这意味着用户不需要与其他租户共享物理资源&#xff0c;可以获取服务器…...

MySQL索引与分区:性能优化的关键

在开发过程中&#xff0c;随着数据量的不断增长&#xff0c;MySQL 查询的性能问题会逐渐显现。特别是在大数据量下&#xff0c;查询变得越来越慢&#xff0c;甚至可能导致系统崩溃。为了优化查询&#xff0c;MySQL 提供了 分区&#xff08;Partitioning&#xff09; 和 索引&am…...

VUE项目部署服务器之后刷新页面异常

情况&#xff1a; vue项目在本地完美运行&#xff0c;经过npm run build之后把dist目录上传到服务后。只有访问文件跟目录可以运行&#xff0c;但刷新之后会找不到相应的页面。 网上都说是hository路由的问题导致&#xff0c;需要修改成hash模式。如果不想修改为hash模式&…...

【实验13】使用预训练ResNet18进行CIFAR10分类

目录 1 数据处理 1.1 数据集介绍 1.2数据处理与划分 2 模型构建- Pytorch高层API中的Resnet18 3 模型训练 4 模型评价 5 比较“使用预训练模型”和“不使用预训练模型”的效果&#xff1a; 6 模型预测 7 完整代码 8 参考链接 1 数据处理 1.1 数据集介绍 数据规模&…...

如何将 GitHub 私有仓库(private)转换为公共仓库(public)

文章目录 如何将 GitHub 私有仓库转换为公共仓库步骤 1: 登录 GitHub步骤 2: 导航到目标仓库步骤 3: 访问仓库设置步骤 4: 更改仓库可见性步骤 5: 确认更改步骤 6: 验证更改注意事项 如何将 GitHub 私有仓库转换为公共仓库 在软件开发领域&#xff0c;GitHub 是一个广受欢迎的…...

进制的问题

蓝桥2015某题 计算数字x在进制p 下的各位数字之和 ​ int calc(int x,int p) {int res0;while(x){resx%p;//取当前位累加x/p;//去掉最低位}return res; }​...

【配置】如何下载和配置Android studio?

下载Android Studio 1、下载链接 https://developer.android.google.cn/studio?hlzh-cn​​​​​​​​​​​​​​​​​​​​ 注意&#xff1a;下载的时候要关闭代理服务器 2、安装软件 根据提示进行安装 3、配置proxy 这里建议配置代理而不是配置国内镜像源 所以…...

CA系统(file.h---申请认证的处理)

#pragma once #ifndef FILEMANAGER_H #define FILEMANAGER_H #include <string> namespace F_ile {// 读取文件&#xff0c;返回文件内容bool readFilename(const std::string& filePath);bool readFilePubilcpath(const std::string& filePath);bool getNameFro…...

Redis开发04:Redis的INFO信息解析

命令解释redis_versionRedis 的版本号&#xff0c;这里是 3.2.100。redis_git_sha1Redis 使用的 Git SHA1 校验值&#xff0c;表示当前代码的版本。redis_git_dirty如果 Redis 当前运行的代码是脏版本&#xff08;未提交的修改&#xff09;&#xff0c;该值为 1&#xff0c;否则…...

《Learn Three.js》学习(2)构建Three.js基本组件

前言&#xff1a; 本章将了解内容包括Three中的主要组件&#xff1b;THERE.SCENE对象的作用&#xff1b;几何图形和格网如何关联&#xff1b;区别正射/透视投影摄像机 基础理论知识&#xff1a; Three.scene&#xff08;场景图&#xff09;保存所有对象、光源和渲染所需的其他…...

VLLM 格式化LLM输出

文章目录 前言guided_jsonguided_choiceguided_regexguided_grammar总结 前言 vllm OpenAI Compatible Server 提供了格式化LLM输出的能力&#xff0c;默认的格式化解码后端应该是outlines 目前提供了四个参数来控制格式化输出&#xff0c;分别是&#xff1a; guided_json: …...

Java篇——Java通过JNA调用c++库时传参含有结构体时数据错乱的解决办法

Java通过JNA调用c库时传参含有结构体时&#xff0c;只继承Structure是不够的&#xff0c;还需要实现Structure.ByValue&#xff0c;或者强制指定结构体字节对齐。示例如下&#xff1a; 1、c库中的结构体定义&#xff1a; 2、java中结构体定义&#xff1a; 3、java中调用 如果没…...

sql分类

SQL&#xff08;Structured Query Language&#xff09;是一种用于管理和操作关系数据库管理系统&#xff08;RDBMS&#xff09;的编程语言。SQL 可以分为几个主要类别&#xff0c;每个类别都有其特定的用途和功能。以下是 SQL 的主要分类&#xff1a; 1. 数据定义语言&#x…...

LayaBox1.8.4实现战争迷雾效果

实现思路&#xff1a; 和Unity实现思路一样&#xff0c;可看我写的下面的一篇文章 战争迷雾FogOfWar---Unity中实现-CSDN博客 根据碰撞点可以计算出需要透明的位置&#xff0c;怎样计算如下&#xff1a; 根据迷雾mesh的长宽和纵向横向的的像素数可以得出&#xff0c;每个小方…...

Python打包元数据困境:约束的重要性

在Python社区中&#xff0c;一项旨在建立新的通用锁文件标准的努力正在展开&#xff0c;这一努力主要在Python讨论论坛上进行。此倡议凸显了创建一个让所有人都满意的标准化方案的难度。不同Python打包工具对锁文件应有的形态和用途有着略微不同的理解。然而&#xff0c;在这些…...

第29天 MCU入门

目录 MCU介绍 MCU的组成与作用 电子产品项目开发流程 硬件开发流程 常用元器件初步了解 硬件原理图与PCB板 常见电源符号和名称 电阻 电阻的分类 贴片电阻的封装说明&#xff1a; 色环电阻的计算 贴片电阻阻值计算 上拉电阻与下拉电阻 电容 电容的读数 二极管 LED 灯电路 钳位作…...

三分钟快速掌握——Linux【vim】的使用及操作方法

一、vim的使用 vim是一个文本编辑器 非常小巧轻便 1.1如何进入vim编辑器 方法一&#xff1a; 首先使用touch 1.c 创建一个源文件 然后使用vim 1.c进入 方法二&#xff1a; 直接使用指令 vim 2.c 会直接创建一个2.c的源文件 退出时记得保存&#xff08;使用wq或者x&am…...

安达发|制造业APS智能优化排产软件的四类制造模型解决方案

在制造业中&#xff0c;APS&#xff08;高级计划和排程系统&#xff09;智能优化排产软件的应用越来越广泛。它通过集成先进的算法和模型&#xff0c;帮助企业提高生产效率、降低成本并提升客户满意度。针对不同类型的生产需求&#xff0c;APS软件提供了四类制造模型解决方案&a…...

屏幕分辨率|尺寸|颜色深度指纹修改

一、前端通过window.screen接口获取屏幕分辨率 尺寸 颜色深度&#xff0c;横屏竖屏信息。 二、window.screen c接口实现&#xff1a; 1、third_party\blink\renderer\core\frame\screen.idl // https://drafts.csswg.org/cssom-view/#the-screen-interface[ExposedWindow ] …...

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

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

记录一次 k8s 节点内存不足的排查过程

背景&#xff1a;前端服务一直报404&#xff0c;查看k8s日志&#xff0c;没发现报错&#xff0c;但是发现pods多次重启。 排查过程&#xff1a; 查看pods日志&#xff0c;发现日志进不去。 kubectrl logs -f -n weave pod-name --tail 100查看pod describe kubectl describ…...

方差分析、相关分析、回归分析

第一章&#xff1a;方差分析 1.1 方差分析概述 作用: 找出关键影响因素&#xff0c;并进行对比分析&#xff0c;选择最佳组合方案。影响因素: 控制因素&#xff08;人为可控&#xff09;和随机因素&#xff08;人为难控&#xff09;。控制变量的不同水平: 控制变量的不同取值…...

【JavaEE初阶 — 网络原理】初识网络原理

目录 1. 网络发展史 1.1 独立模式 1.2 网络互连 1.2.1 网络互联的背景 1.2.2 网络互联的定义 1.3 局域网LAN 1.4 广域网WAN 2. 网络通信基础 2.1 IP地址 2.2 端口号 2.3 认识协议 2.4 五元组 2.5 协议分层 2.5.1 分…...

算法的NPU终端移植:深入探讨与实践指南

目录 ​编辑 引言 算法选择 模型压缩 权重剪枝 量化 知识蒸馏 硬件适配 指令集适配 内存管理 并行计算 性能测试 速度测试 精度测试 功耗测试 案例分析 图像识别算法的NPU移植案例 结论 引言 在人工智能技术的浪潮中&#xff0c;神经网络处理器&#xff08;…...