1、媒资管理需求分析
2、为什么要用网关
当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图:
后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:
如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost
当系统上线后这里需要改成公网的域名,如果这种地址非常多则非常麻烦。
基于这个问题可以采用网关来解决,如下图:
这样在前端的代码中只需要指定每个接口的相对路径,如下所示:
在前端代码的一个固定的地方在接口地址前统一加网关的地址,每个请求统一到网关,由网关将请求转发到具体的微服务。
为什么所有的请求先到网关呢?
有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的试可以根据请求路径进行路由、根据host地址进行路由等, 当微服务有多个实例时可以通过负载均衡算法进行路由,如下:
另外,网关还可以实现权限控制、限流等功能。
项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:
流程如下:
1、微服务启动,将自己注册到Nacos,Nacos记录了各微服务实例的地址。
2、网关从Nacos读取服务列表,包括服务名称、服务地址等。
3、请求到达网关,网关将请求路由到具体的微服务。
要使用网关首先搭建Nacos,Nacos有两个作用:
1、服务发现中心。
微服务将自身注册至Nacos,网关从Nacos获取微服务列表。
2、配置中心。
微服务众多,它们的配置信息也非常复杂,为了提供系统的可维护性,微服务的配置信息统一在Nacos配置。
3、Nacos
Spring Cloud :一套规范
Spring Cloud alibaba: nacos服务注册中心,配置中心
根据上节讲解的网关的架构图,要使用网关首先搭建Nacos。
首先搭建Nacos服务发现中心。
在搭建Nacos服务发现中心之前需要搞清楚两个概念:namespace和group
namespace:用于区分环境、比如:开发环境、测试环境、生产环境。
group:用于区分项目,比如:xuecheng-plus项目、xuecheng2.0项目
首先在nacos配置namespace:
登录Centos,启动Naocs,使用sh /data/soft/restart.sh将自动启动Nacos。
访问:
http://192.168.101.65:8848/nacos/
账号密码:nacos/nacos
相关配置
- 在xuecheng-plus-parent中添加依赖管理
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency>
2)在内容管理模块的接口工程、系统管理模块的接口工程中添加如下依赖
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
3)配置nacos的地址
在系统管理的接口工程的配置文件中配置如下信息:
YAML
#微服务配置
spring:
application:
name: system-service
cloud:
nacos:
server-addr:192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
在内容管理的接口工程的配置文件中配置如下信息:
YAML
spring:
application:
name: content-api
cloud:
nacos:
server-addr:192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
配置优先级
到目前为止已将所有微服务的配置统一在nacos进行配置,用到的配置文件有本地的配置文件 bootstrap.yaml和nacos上的配置文件,SpringBoot读取配置文件 的顺序如下:
引入配置文件的形式有:
1、以项目应用名方式引入
2、以扩展配置文件方式引入
3、以共享配置文件 方式引入
4、本地配置文件
各配置文件 的优先级:项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件。
有时候我们在测试程序时直接在本地加一个配置进行测试,比如下边的例子:
我们想启动两个内容管理微服务,此时需要在本地指定不同的端口,通过VM Options参数,在IDEA配置启动参数
通过-D指定参数名和参数值,参数名即在bootstrap.yml中配置的server.port。
启动ContentApplication2,发现端口仍然是63040,这说明本地的配置没有生效。
这时我们想让本地最优先,可以在nacos配置文件 中配置如下即可实现:
#配置本地优先
spring:
cloud:
config:
override-none:true
再次启动ContentApplication2,端口为63041。
4、分布式文件系统
可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:
5、MinIO分布式文件系统
介绍
本项目采用MinIO构建分布式文件系统,MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。
它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
官网:https://min.io
中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/
MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问。
去中心化有什么好处?
在大数据领域,通常的设计理念都是无中心和分布式。Minio分布式模式可以帮助你搭建一个高可用的对象存储服务,你可以使用这些存储设备,而不用考虑其真实物理位置。
它将分布在不同服务器上的多块硬盘组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。如下图:
Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。
6、Linux下MinIO登录
地址
:http://192.168.101.65:9001/login
用户名:minioadmin
密码: minioadmin
7、java在MinIO上传文件和下载文件
packagecom.xuecheng.media;importcom.j256.simplemagic.ContentInfo;importcom.j256.simplemagic.ContentInfoUtil;importio.minio.*;importio.minio.errors.*;importorg.apache.commons.codec.digest.DigestUtils;importorg.apache.commons.io.IOUtils;importorg.junit.jupiter.api.Test;importorg.springframework.http.MediaType;importjava.io.*;importjava.security.InvalidKeyException;importjava.security.NoSuchAlgorithmException;/**
* @author Mr.M
* @version 1.0
* @description 测试minio的sdk
* @date 2023/2/17 11:55
*/publicclassMinioTest{MinioClient minioClient =MinioClient.builder().endpoint("http://192.168.101.65:9000").credentials("minioadmin","minioadmin").build();@Testpublicvoidtest_upload()throwsException{//通过扩展名得到媒体资源类型 mimeType//根据扩展名取出mimeTypeContentInfo extensionMatch =ContentInfoUtil.findExtensionMatch(".mp4");String mimeType =MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();}//上传文件的参数信息UploadObjectArgs uploadObjectArgs =UploadObjectArgs.builder().bucket("testbucket")//桶.filename("F:\\develop\\video\\1.mp4")//指定本地文件路径// .object("1.mp4")//对象名 在桶下存储该文件.object("test/01/1.mp4")//对象名 放在子目录下.contentType(mimeType)//设置媒体文件类型.build();//上传文件
minioClient.uploadObject(uploadObjectArgs);}//删除文件@Testpublicvoidtest_delete()throwsException{//RemoveObjectArgsRemoveObjectArgs removeObjectArgs =RemoveObjectArgs.builder().bucket("testbucket").object("1.mp4").build();//删除文件
minioClient.removeObject(removeObjectArgs);}//查询文件 从minio中下载@Testpublicvoidtest_getFile()throwsException{GetObjectArgs getObjectArgs =GetObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build();//查询远程服务获取到一个流对象FilterInputStream inputStream = minioClient.getObject(getObjectArgs);//指定输出流FileOutputStream outputStream =newFileOutputStream(newFile("F:\\develop\\video\\1a.mp4"));IOUtils.copy(inputStream,outputStream);//校验文件的完整性对文件的内容进行md5FileInputStream fileInputStream1 =newFileInputStream(newFile("F:\\develop\\video\\1.mp4"));String source_md5 =DigestUtils.md5Hex(fileInputStream1);FileInputStream fileInputStream =newFileInputStream(newFile("F:\\develop\\video\\1a.mp4"));String local_md5 =DigestUtils.md5Hex(fileInputStream);if(source_md5.equals(local_md5)){System.out.println("下载成功");}}}
8、上传图片
流程:
9、上传图片接口
yml
minio:
endpoint: http://192.168.101.65:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
配置类
packagecom.xuecheng.media.config;importio.minio.MinioClient;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassMinioConfig{@Value("${minio.endpoint}")privateString endpoint;@Value("${minio.accessKey}")privateString accessKey;@Value("${minio.secretKey}")privateString secretKey;@BeanpublicMinioClientminioClient(){MinioClient minioClient =MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();return minioClient;}}
API
@ApiOperation("上传图片")@RequestMapping(value ="/upload/coursefile",consumes =MediaType.MULTIPART_FORM_DATA_VALUE)publicUploadFileResultDtoupload(@RequestPart("filedata")MultipartFile filedata)throwsIOException{//准备上传文件的信息UploadFileParamsDto uploadFileParamsDto =newUploadFileParamsDto();//原始文件名称
uploadFileParamsDto.setFilename(filedata.getOriginalFilename());//文件大小
uploadFileParamsDto.setFileSize(filedata.getSize());//文件类型
uploadFileParamsDto.setFileType("001001");//创建一个临时文件File tempFile =File.createTempFile("minio",".temp");
filedata.transferTo(tempFile);Long companyId =1232141425L;//文件路径String localFilePath = tempFile.getAbsolutePath();//调用service上传图片UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);return uploadFileResultDto;}
Service
//根据扩展名获取mimeTypeprivateStringgetMimeType(String extension){if(extension ==null){
extension ="";}//根据扩展名取出mimeTypeContentInfo extensionMatch =ContentInfoUtil.findExtensionMatch(extension);String mimeType =MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();}return mimeType;}/**
* 将文件上传到minio
* @param localFilePath 文件本地路径
* @param mimeType 媒体类型
* @param bucket 桶
* @param objectName 对象名
* @return
*/publicbooleanaddMediaFilesToMinIO(String localFilePath,String mimeType,String bucket,String objectName){try{UploadObjectArgs uploadObjectArgs =UploadObjectArgs.builder().bucket(bucket)//桶.filename(localFilePath)//指定本地文件路径.object(objectName)//对象名 放在子目录下.contentType(mimeType)//设置媒体文件类型.build();//上传文件
minioClient.uploadObject(uploadObjectArgs);
log.debug("上传文件到minio成功,bucket:{},objectName:{},错误信息:{}",bucket,objectName);returntrue;}catch(Exception e){
e.printStackTrace();
log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}",bucket,objectName,e.getMessage());}returnfalse;}//获取文件默认存储目录路径 年/月/日privateStringgetDefaultFolderPath(){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");String folder = sdf.format(newDate()).replace("-","/")+"/";return folder;}//获取文件的md5privateStringgetFileMd5(File file){try(FileInputStream fileInputStream =newFileInputStream(file)){String fileMd5 =DigestUtils.md5Hex(fileInputStream);return fileMd5;}catch(Exception e){
e.printStackTrace();returnnull;}}@OverridepublicUploadFileResultDtouploadFile(Long companyId,UploadFileParamsDto uploadFileParamsDto,String localFilePath){//文件名String filename = uploadFileParamsDto.getFilename();//先得到扩展名String extension = filename.substring(filename.lastIndexOf("."));//得到mimeTypeString mimeType =getMimeType(extension);//子目录String defaultFolderPath =getDefaultFolderPath();//文件的md5值String fileMd5 =getFileMd5(newFile(localFilePath));String objectName = defaultFolderPath+fileMd5+extension;//上传文件到minioboolean result =addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);if(!result){XueChengPlusException.cast("上传文件失败");}//入库文件信息MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);if(mediaFiles==null){XueChengPlusException.cast("文件上传后保存信息失败");}//准备返回的对象UploadFileResultDto uploadFileResultDto =newUploadFileResultDto();BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);return uploadFileResultDto;}/**
* @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
*/@TransactionalpublicMediaFilesaddMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){//将文件信息保存到数据库MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if(mediaFiles ==null){
mediaFiles =newMediaFiles();BeanUtils.copyProperties(uploadFileParamsDto,mediaFiles);//文件id
mediaFiles.setId(fileMd5);//机构id
mediaFiles.setCompanyId(companyId);//桶
mediaFiles.setBucket(bucket);//file_path
mediaFiles.setFilePath(objectName);//file_id
mediaFiles.setFileId(fileMd5);//url
mediaFiles.setUrl("/"+bucket+"/"+objectName);//上传时间
mediaFiles.setCreateDate(LocalDateTime.now());//状态
mediaFiles.setStatus("1");//审核状态
mediaFiles.setAuditStatus("002003");//插入数据库int insert = mediaFilesMapper.insert(mediaFiles);if(insert<=0){
log.debug("向数据库保存文件失败,bucket:{},objectName:{}",bucket,objectName);returnnull;}return mediaFiles;}return mediaFiles;}
10、事务优化(重点)
在上传图片这一个业务中,如果直接在整个方法前加
@Transactional
事务的话,因为上传图片涉及到网络上传,可能需要耗时较多,这样请求数据库的时间就会变长,在高并发情况下就可能给数据库造成很大的压力。那么怎么解决这个问题呢?思路就是减小事务的粒度,也就是说在原有的方法里,抽取插入数据库的代码成为独立的方法,并且在这个方法加事务,这样事务就不涉及到网络的传输,可以减少数据库的压力。但是这可能引发一个问题,那就是
事务失效
。
什么是事务失效?
在spring中,事务成立的条件是
代理对象
,也就是说如果在一个非代理对象的方法加事务的话,那么久可能导致事务失效。在service里面的方法本质是@Transactional
方法,显然不是代理对象。那么怎样变成一个代理对象呢?this
答案是将自己注入,并且将这个方法写在接口里面
@AutowiredMediaFileService currentProxy;
版权归原作者 杭州下小雨~ 所有, 如有侵权,请联系我们删除。