发布于2023-06-19 22:21 阅读(1799) 评论(0) 点赞(10) 收藏(2)
当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图:
后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:
如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost
当系统上线后这里需要改成公网的域名,如果这种地址非常多则非常麻烦。
基于这个问题可以采用网关来解决,如下图:
这样在前端的代码中只需要指定每个接口的相对路径,如下所示:
在前端代码的一个固定的地方在接口地址前统一加网关的地址,每个请求统一到网关,由网关将请求转发到具体的微服务。
为什么所有的请求先到网关呢?
有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的试可以根据请求路径进行路由、根据host地址进行路由等, 当微服务有多个实例时可以通过负载均衡算法进行路由,如下:
另外,网关还可以实现权限控制、限流等功能。
项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:
流程如下:
1、微服务启动,将自己注册到Nacos,Nacos记录了各微服务实例的地址。
2、网关从Nacos读取服务列表,包括服务名称、服务地址等。
3、请求到达网关,网关将请求路由到具体的微服务。
要使用网关首先搭建Nacos,Nacos有两个作用:
1、服务发现中心。
微服务将自身注册至Nacos,网关从Nacos获取微服务列表。
2、配置中心。
微服务众多,它们的配置信息也非常复杂,为了提供系统的可维护性,微服务的配置信息统一在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
<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。
可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:
本项目采用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个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。
地址:http://192.168.101.65:9001/login
用户名:minioadmin
密码: minioadmin
package com.xuecheng.media; import com.j256.simplemagic.ContentInfo; import com.j256.simplemagic.ContentInfoUtil; import io.minio.*; import io.minio.errors.*; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import java.io.*; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; /** * @author Mr.M * @version 1.0 * @description 测试minio的sdk * @date 2023/2/17 11:55 */ public class MinioTest { MinioClient minioClient = MinioClient.builder() .endpoint("http://192.168.101.65:9000") .credentials("minioadmin", "minioadmin") .build(); @Test public void test_upload() throws Exception { //通过扩展名得到媒体资源类型 mimeType //根据扩展名取出mimeType ContentInfo 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); } //删除文件 @Test public void test_delete() throws Exception { //RemoveObjectArgs RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket("testbucket").object("1.mp4").build(); //删除文件 minioClient.removeObject(removeObjectArgs); } //查询文件 从minio中下载 @Test public void test_getFile() throws Exception { GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build(); //查询远程服务获取到一个流对象 FilterInputStream inputStream = minioClient.getObject(getObjectArgs); //指定输出流 FileOutputStream outputStream = new FileOutputStream(new File("F:\\develop\\video\\1a.mp4")); IOUtils.copy(inputStream,outputStream); //校验文件的完整性对文件的内容进行md5 FileInputStream fileInputStream1 = new FileInputStream(new File("F:\\develop\\video\\1.mp4")); String source_md5 = DigestUtils.md5Hex(fileInputStream1); FileInputStream fileInputStream = new FileInputStream(new File("F:\\develop\\video\\1a.mp4")); String local_md5 = DigestUtils.md5Hex(fileInputStream); if(source_md5.equals(local_md5)){ System.out.println("下载成功"); } } }
流程:
minio:
endpoint: http://192.168.101.65:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
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; @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() { MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); return minioClient; } }
@ApiOperation("上传图片") @RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public UploadFileResultDto upload(@RequestPart("filedata")MultipartFile filedata) throws IOException { //准备上传文件的信息 UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto(); //原始文件名称 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; }
//根据扩展名获取mimeType private String getMimeType(String extension){ if(extension == null){ extension = ""; } //根据扩展名取出mimeType ContentInfo 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 */ public boolean addMediaFilesToMinIO(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); return true; } catch (Exception e) { e.printStackTrace(); log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}",bucket,objectName,e.getMessage()); } return false; } //获取文件默认存储目录路径 年/月/日 private String getDefaultFolderPath() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String folder = sdf.format(new Date()).replace("-", "/")+"/"; return folder; } //获取文件的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; } } @Override public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) { //文件名 String filename = uploadFileParamsDto.getFilename(); //先得到扩展名 String extension = filename.substring(filename.lastIndexOf(".")); //得到mimeType String mimeType = getMimeType(extension); //子目录 String defaultFolderPath = getDefaultFolderPath(); //文件的md5值 String fileMd5 = getFileMd5(new File(localFilePath)); String objectName = defaultFolderPath+fileMd5+extension; //上传文件到minio boolean 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 = new UploadFileResultDto(); 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 */ @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); //文件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); return null; } return mediaFiles; } return mediaFiles; }
在上传图片这一个业务中,如果直接在整个方法前加@Transactional
事务的话,因为上传图片涉及到网络上传,可能需要耗时较多,这样请求数据库的时间就会变长,在高并发情况下就可能给数据库造成很大的压力。那么怎么解决这个问题呢?思路就是减小事务的粒度,也就是说在原有的方法里,抽取插入数据库的代码成为独立的方法,并且在这个方法加事务,这样事务就不涉及到网络的传输,可以减少数据库的压力。但是这可能引发一个问题,那就是事务失效
。
什么是事务失效?
在spring中,事务成立的条件是代理对象
+@Transactional
,也就是说如果在一个非代理对象的方法加事务的话,那么久可能导致事务失效。在service里面的方法本质是this
方法,显然不是代理对象。那么怎样变成一个代理对象呢?
答案是将自己注入,并且将这个方法写在接口里面
@Autowired
MediaFileService currentProxy;
原文链接:https://blog.csdn.net/qq_57818853/article/details/131126301
作者:我长得真不赖
链接:http://www.javaheidong.com/blog/article/674513/8c7a6eecb304ef9936a3/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!