当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图:
后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:
如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是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;