学成在线05-媒资管理-图片管理
[学成在线]05-媒资管理-图片管理
需求分析
媒体资源管理(Media Asset Management,MAM)系统是建立在多媒体、网络、数据库和数字存储等先进技术基础上的一个对各种媒体及内容(如视/音频资料、文本文件、图表等)进行数字化存储、管理以及应用的总体解决方案,包括数字媒体的采集、编目、管理、传输和编码转换等所有环节。其主要是满足 资源拥有者收集、保存、查找、编辑、发布各种信息的要求,为媒体资源的使用者提供访问内容的便捷方法,实现对媒体资源的高效管理,大幅度提高媒体资源的价值。
每个教学机构都可以在媒资系统管理自己的教学资源,包括:视频、教案等文件。
目前媒资管理的主要管理对象是视频、图片、文档等,包括:媒资文件的查询、文件上传、视频处理等。
媒资查询:教学机构查询自己所拥有的媒资信息。
文件上传:包括上传图片、上传文档、上传视频。
视频处理:视频上传成功,系统自动对视频进行编码处理。
文件删除:教学机构删除自己上传的媒资文件。
下图是课程编辑与发布的整体流程,通过下图可以看到媒资管理在整体流程的位置:
本模块媒资文件相关的数据表如下:
- 媒资文件表:存储文件信息,包括图片、视频、文档等。
- media_process: 待处理视频表。
- media_process_history: 视频处理历史表,记录已经处理成功的视频信息。
媒资文件与课程计划绑定关系表如下:
环境搭建
架构分析
当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图:
后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:
- 如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost
- 当系统上线后这里需要改成公网的域名,如果这种地址非常多则非常麻烦。
- 基于这个问题可以采用网关来解决,如下图:
- 这样在前端的代码中只需要指定每个接口的相对路径,如下所示
- 在前端代码的一个固定的地方在接口地址前统一加网关的地址,每个请求统一到网关,由网关将请求转发到具体的微服务。
为什么所有的请求先到网关呢?
- 有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的话可以根据请求路径进行路由、根据host地址进行路由等,当微服务有多个实例时可以通过负载均衡算法进行路由,如下:
- 另外,网关还可以实现权限控制、限流等功能。
项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:
流程如下:
1、微服务启动,将自己注册到Nacos,Nacos记录了各微服务实例的地址。
2、网关从Nacos读取服务列表,包括服务名称、服务地址等。
3、请求到达网关,网关将请求路由到具体的微服务。
要使用网关首先搭建Nacos,Nacos有两个作用:
1、服务发现中心。
微服务将自身注册至Nacos,网关从Nacos获取微服务列表。
2、配置中心。
微服务众多,它们的配置信息也非常复杂,为了提供系统的可维护性,微服务的配置信息统一在Nacos配置。
搭建Nacos
服务发现
根据上节讲解的网关的架构图,要使用网关首先搭建Nacos。
先明确概念
- Spring Cloud :是一套微服务的规范
- Spring Cloud alibaba: 阿里巴巴实现Spring Cloud的一套技术栈, 其中包括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
- 登录成功,点击左侧菜单“命名空间”进入命名空间管理界面,点击“新建命名空间”,填写命名空间的相关信息。如下图:
- 使用相同的方法再创建“测试环境”、“生产环境"的命名空间。
- 创建成功,如下图:
- 完成各服务注册到Naocs,下边将内容管理服务注册到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>
- 在内容管理模块的接口工程、系统管理模块的接口工程中添加如下依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 配置nacos的地址
// 在系统管理的接口工程的配置文件中配置如下信息:
spring:
application:
name: system-service
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
// 在内容管理的接口工程的配置文件中配置如下信息:
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
- 重启内容管理服务、系统管理服务。待微服务启动成功,进入Nacos服务查看服务列表
- 在 “开发环境” 命名空间下有两个服务这说明内容管理微服务和系统管理微服务在Nacos注册成功
- 点击其它一个微服务的“详情, 以查看微服务实例的地址。
配置中心
搭建完成Nacos服务发现中心,下边搭建Nacos为配置中心,其目的就是通过Nacos去管理项目的所有配置。
前置梳理
先将项目中的配置文件分分类:
1、每个服务特有的配置
是指该配置只在有些项目中需要配置,或者该配置在每个项目中配置的值不同。
比如:spring.application.name每个项目都需要配置但值不一样,以及有些项目需要连接数据库而有些项目不需要,有些项目需要配置消息队列而有些项目不需要。
2、服务共用的配置
是指在若干项目中配置内容相同的配置。比如:redis的配置,很多项目用的同一套redis服务所以配置也一样。
配置三要素
需要知道nacos如何去定位一个具体的配置文件,即:namespace、group、dataid.
1、通过namespace、group找到具体的环境和具体的项目。
2、通过dataid找到具体的配置文件,dataid有三部分组成
3、比如:content-service-dev.yaml配置文件, 由(content-service)-(dev). (yaml)三部分组成
- content-service:第一部分,它是在application.yaml中配置的应用名,即spring.application.name的值。
- dev:第二部分,它是环境名,通过spring.profiles.active指定,
- Yaml: 第三部分,它是配置文件的后缀,目前nacos支持properties、yaml等格式类型,本项目选择yaml格式类型。
4、大型项目, 服务的配置文件一般有三个, 根据需要使用不同的配置文件:
- 在开发环境中配置content-service-dev.yaml
- 在测试环境中配置content-service-test.yaml
- 在生产环境中配置content-service-prod.yaml
5、补充: 我们启动项目时, 也可以通过传入spring.profiles.active的参数, 决定引用哪个环境的配置文件,6、例如:传入spring.profiles.active=dev表示使用dev环境的配置文件即content-service-dev.yaml
配置特有配置
把各微服务特有的配置提取到nacos配置中心
- 下边以开发环境为例, 把对content-service服务中的content-api模块的配置文件进行提取,进入nacos,进入开发环境。nacos服务地址: http://192.168.101.65:8848/nacos/
- 添加一个配置: 输入data id、group以及配置文件内容
#微服务配置
#Data id = 服务名 + 环境名 + 配置格式
#Group = group
#上面的对应关系不能出错, 不然项目中就不能正确的定位nacos中的配置文件
spring:
application:
name: content-api # 服务名
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery: #服务注册相关配置
namespace: dev # 命名空间
group: xuecheng-plus-project # 服务分组,通用指定项目名
profiles:
active: dev # 环境名,默认为dev
- 在本地的配置文件, 添加nacos配置文件的信息, 去掉重复配置的部分
#微服务配置
spring:
application:
name: content-api # 服务名
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery: #服务注册相关配置((服务注册))
namespace: dev # 命名空间
group: xuecheng-plus-project # 服务分组,通用指定项目名
config: # 配置中心读取配置文件的相关配置(配置拉取)
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
profiles:
active: dev # 环境名,默认为dev
- 在该服务引入依赖
<!-- nacos配置拉取 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 重启服务: NacosRestTemplate.java通过Post方式与nacos服务端交互读取配置信息。
- 进到swagger文件, 测试接口, 结果正确返回, 说明数据库连接成功, 证明nacos配置文件生效
接口文档地址:
继续把content-service服务中的content-service模块的配置文件进行提取
- 在content-service模块中引入依赖
<!-- nacos配置拉取 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 在本地配置文件中配置nacos配置文件的信息
#微服务配置
spring:
application:
name: content-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.65:3306/xcplus_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: mysql
cloud:
nacos:
server-addr: 192.168.101.65:8848
config: # 配置中心读取配置文件的相关配置(配置拉取)
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
profiles:
active: dev # 环境名,默认为dev
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
- 在nacos中创建配置文件
- 把本地配置文件的重复配置注释掉
#微服务配置
spring:
application:
name: content-service
# datasource:
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://192.168.101.65:3306/xcplus_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
# username: root
# password: mysql
config: # 配置中心读取配置文件的相关配置(配置拉取)
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
profiles:
active: dev # 环境名,默认为dev
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
- 运行测试方法, 检查是否正确的拉取了nacos配置文件
- 这样配置完成以后, content-api模块和content-service模块的本地配置就迁移到了nacos的配置文件
- 但是, 目前两个模块存在重复的配置, 就是datasource选项(数据库连接参数), api接口工程依赖了service工程的jar,这里使用extension-configs扩展配置文件的方式, 让api模块引用service模块的配置文件, 就不需要重复配置了。
#微服务配置
spring:
application:
name: content-api # 服务名
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery: #服务注册相关配置(服务注册)
namespace: dev # 命名空间
group: xuecheng-plus-project # 服务分组,通用指定项目名
config: # 配置中心读取配置文件的相关配置(配置拉取)
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
extension-configs: #扩展配置文件
- data-id: content-service-${spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true
- 测试api模块是否正常工作
配置公用配置
把微服务公用的配置提取到nacos配置中心
把swagger和logging的配置提取到nacos中, 单独在xuecheng-plus-common分组下创建xuecheng-plus的公用配置, 哪个项目用引入即可。
- 在nacos中新建swagger的配置文件
- 在nacos中新建logging的配置文件
- nacos提供了shared-configs可以引入公用配置。
微服务配置
spring:
application:
name: content-api # 服务名
cloud:
nacos:
server-addr: 192.168.101.65:8848
config: # 配置中心读取配置文件的相关配置(配置拉取)
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
shared-configs: # 共享配置文件
- data-id: swagger-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev # 环境名,默认为dev
- 重启服务, 访问接口文档, 正常展示说明配置成功了
配置的优先级
到目前为止已将所有微服务的配置统一在nacos进行配置,用到的配置文件有本地的配置文件 bootstrap.yaml和nacos上的配置文件,SpringBoot读取配置文件的顺序如下:
目前项目中引入配置文件的形式有:
1、以项目应用名方式引入
2、以扩展配置文件方式引入
3、以共享配置文件方式引入
4、本地配置文件
各配置文件的优先级:项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件。
有时候我们在测试程序时直接在本地加一个配置进行测试,比如下边的例子:
- 我们想启动两个内容管理微服务,此时需要在本地指定不同的端口,
- 通过VM Options参数,在IDEA配置启动参数
- 通过-D指定参数名和参数值,参数名即在bootstrap.yml中配置的server.port。
- 由于本地配置文件的优先级最低, 所以这样的配置不会生效, 想让本地最优先,可以在nacos配置文件中配置如下即可实现
#配置本地优先
spring:
cloud:
config:
override-none: true
- 启动ContentApplication3,发现端口仍然是63043,本地的配置生效了。
- VM Options参数的作用和配置文件的作用一样的
导入配置文件
课程资料中提供了系统用的所有配置文件nacos_config_export.zip,下边将nacos_config_export.zip导入nacos。
- 进入具体的命名空间,点击“导入配置”
- 打开导入窗口, 相同的配置选择跳过配置。
- 点击“上传文件”选择资料中的nacos_config_export.zip开始导入
搭建网关
本项目使用Spring Cloud Gateway作为网关,下边创建网关工程。
- 新建一个网关工程。
- 整理工程结构
- 添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../xuecheng-plus-parent</relativePath>
</parent>
<artifactId>xuecheng-plus-gateway</artifactId>
<dependencies>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务发现中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 排除 Spring Boot 依赖的日志包冲突 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Boot 集成 log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
</project>
- 配置网关的bootstrap.yaml配置文件
#微服务配置
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
shared-configs:
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev
- 在nacos上配置网关路由策略(已经导入, 这里确认一下就行):
server:
port: 63010 # 网关端口
spring:
cloud:
gateway:
# filter:
# strip-prefix:
# enabled: true
routes: # 网关路由配置
- id: content-api # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://content-api # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/content/** # 这个是按照路径匹配,只要以/content/开头就符合要求
# filters:
# - StripPrefix=1
- id: system-api
# uri: http://127.0.0.1:8081
uri: lb://system-api
predicates:
- Path=/system/**
# filters:
# - StripPrefix=1
- id: media-api
# uri: http://127.0.0.1:8081
uri: lb://media-api
predicates:
- Path=/media/**
# filters:
# - StripPrefix=1
- id: search-service
# uri: http://127.0.0.1:8081
uri: lb://search
predicates:
- Path=/search/**
# filters:
# - StripPrefix=1
- id: auth-service
# uri: http://127.0.0.1:8081
uri: lb://auth-service
predicates:
- Path=/auth/**
# filters:
# - StripPrefix=1
- id: checkcode
# uri: http://127.0.0.1:8081
uri: lb://checkcode
predicates:
- Path=/checkcode/**
# filters:
# - StripPrefix=1
- id: learning-api
# uri: http://127.0.0.1:8081
uri: lb://learning-api
predicates:
- Path=/learning/**
# filters:
# - StripPrefix=1
- id: orders-api
# uri: http://127.0.0.1:8081
uri: lb://orders-api
predicates:
- Path=/orders/**
# filters:
# - StripPrefix=1
- 启动网关工程,查看网关是否注册到nacos中
- 通过网关工程访问微服务进行测试。
- 网关工程搭建完成即可将前端工程中的接口地址改为网关的地址
- 启动前端工程,测试之前开发内容管理模块的功能。
- 观察网关控制台,通过网关转发课程查询的日志如下:
搭建媒资工程
从课程资料中获取媒资工程 xuecheng-plus-media,拷贝到项目工程根目录。
右键pom.xml转为maven工程。
创建媒资数据库xc_media,并导入资料目录中的xcplus_media.sql
检查本地的配置文件和nacos上的media-service-dev.yaml配置文件中的配置信息
重启media-api工程只要能正常启动成功即可,访问接口文档, 稍后根据需求写接口。
分布式文件系统
概念介绍
要理解分布式文件系统首先了解什么是文件系统。
文件系统是负责管理和存储文件的系统软件,操作系统通过文件系统提供的接口去存取文件,用户通过操作系统访问磁盘上的文件。
下图指示了文件系统所处的位置:
常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等。
现在有个问题,一此短视频平台拥有大量的视频、图片,这些视频文件、图片文件该如何存储呢?如何存储可以满足互联网上海量用户的浏览。
- 今天讲的分布式文件系统就是海量用户查阅海量文件的方案。
- 一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:
- 好处:
- 一台计算机的文件系统处理能力扩充到多台计算机同时处理。
- 一台计算机挂了还有另外副本计算机提供数据。
- 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。
市面上有哪些分布式文件系统的产品呢?
- NFS
网络文件系统(NFS)是文件系统之上的一个网络抽象,来允许远程客户端以与本地文件系统类似的方式,来通过网络进行访问。虽然 NFS 不是第一个此类系统,但是它已经发展并演变成 UNIX系统中最强大最广泛使用的网络文件系统。NFS 允许在多个用户之间共享公共文件系统,并提供数据集中的优势,来最小化所需的存储空间。
特点:
1)在客户端上映射NFS服务器的驱动器。
2)客户端通过网络访问NFS服务器的硬盘完全透明。
- GFS
GFS是一个可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的苦通硬件上,可以提供容错功能,它可以以给大量的用户提供总体性能较高的服务。
1)GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成。
2)master存储了数据文件的元数据,一个文件被分成了若干块存储在多个chunkserver中。
3)用户从master中获取数据元信息,向chunkserver存储数据。
- HDFS
HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。 HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。例如客户端写入读取文件的直接操作都是分布在集群各个机器上的,没有单点性能压力。
1)HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成。
- 名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点。
3)客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据。
- 云计算厂家
阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.9999999999%(12 个 9),服务设计可用性(或业务连续性)不低于 99.995%。
官方网站:
百度对象存储BOS提供稳定、安全、高效、高可扩展的云存储服务。您可以将任意数量和形式的非结构化数据存入BOS,并对数据进行管理和处理。BOS支持标准、低频、冷和归档存储等多种存储类型,满足多场景的存储需求。
官方网站:
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个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。
数据恢复演示
下边在本机演示MinIO恢复数据的过程,在本地创建4个目录表示4个硬盘。
下载minio,下载地址在https://dl.min.io/server/minio/release/,可从课程资料找到MinIO的安装文件minio.zip解压即可使用
CMD进入有minio.exe的目录,运行下边的命令:
minio.exe server D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data1 D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data2 D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data3 D:\UserDatas\Note\16Project\xue-cheng-zai-xian\minio\data4
启动结果如下:
说明如下:
WARNING: MINIO_ACCESS_KEY and MINIO_SECRET_KEY are deprecated.
Please use MINIO_ROOT_USER and MINIO_ROOT_PASSWORD
Formatting 1st pool, 1 set(s), 4 drives per set.
WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable.
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables
1)老版本使用的MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY不推荐使用,推荐使用MINIO_ROOT_USER 和MINIO_ROOT_PASSWORD设置账号和密码。
2)pool即minio节点组成的池子,当前有一个pool和4个硬盘组成的set集合
3)因为集合是4个硬盘,大于2的硬盘损坏数据将无法恢复。
4)账号和密码默认为minioadmin、minioadmin,可以在环境变量中设置通过’MINIO_ROOT_USER’ and ‘MINIO_ROOT_PASSWORD’ 进行设置。
- 下边输入http://localhost:9000进行登录,账号和密码为:minioadmin/minioadmin
- 下一步创建bucket,桶,它相当于存储文件的目录,可以创建若干的桶。
- 点击“upload”上传文件。
- 下边去四个目录观察文件的存储情况, 我们发现上传的文件存储在了四个目录,即四个硬盘上。
- 下边测试minio的数据恢复过程:
- 首先删除一个目录。
- 删除目录后仍然可以在web控制台上传文件和下载文件。
- 稍等片刻删除的目录自动恢复。
- 删除两个目录。
- 删除两个目录也会自动恢复。
- 删除三个目录。
- 由于集合中共有4块硬盘,有大于一半的硬盘损坏数据无法恢复。
- 此时报错:We encountered an internal error, please try again. (Read failed. Insufficient number of drives online)在线驱动器数量不足。
测试Docker环境
开发阶段和生产阶段统一使用Docker下的MINIO。
- 在下发的虚拟机中已安装了MinIO的镜像和容器,执行sh /data/soft /restart.sh启动Docker下的MinIO
- 启动完成登录MinIO查看是否正常。访问http://192.168.101.65:9000
- 本项目创建两个buckets:
- mediafiles:普通文件
- video:视频文件
测试SDK
MinIO提供多个语言版本SDK的支持,下边找到java版本的文档:
- 地址:https://docs.min.io/docs/java-client-quickstart-guide.html
- 最低需求Java 1.8或更高版本:
- maven依赖如下:在media-service工程添加此依赖。
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
- 参数说明:需要三个参数才能连接到minio服务。
- 首先创建一个用于测试的bucket, 并设置访问权限
- 在xuecheng-plus-media-service工程的test下编写测试代码如下:
- 测试文件上传
/**
* @description 测试MinIO
*/
public class MinioTest {
static MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//上传文件1
@Test
public void upload() {
try {
// 上传文件的参数
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket("testbucket") // 桶名
.filename("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test.png") // 文件地址
//.object("test001.png") // 上传至根目录下
.object("001/test001.png") //上传到子目录下
.contentType("image/png") //指定文件类型
.build();
// 上传文件
minioClient.uploadObject(testbucket);
System.out.println("上传成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("上传失败");
}
}
}
- 根据文件扩展名获得媒体类型
- 引入j256依赖
<!--根据扩展名取mimetype-->
<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
- 通过com.j256.simplemagic.ContentType枚举类查看常用的mimeType(媒体类型)
- 使用j256提供的ContentInfoUtil工具类, 通过文件扩展名得到mimeType,代码如下:
/**
* @description 测试MinIO
*/
public class MinioTest {
static MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//上传文件2
@Test
public void upload2() {
// 根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();
}
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket("testbucket") // 桶名
.object("001/test001.png") //保存到子目录下
.filename("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test.png") // 文件地址
.contentType(mimeType)//默认根据扩展名确定文件内容类型,也可以指定
.build();
minioClient.uploadObject(testbucket);
System.out.println("上传成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("上传失败");
}
}
}
- 测试删除文件
/**
* @description 测试MinIO
*/
public class MinioTest {
static MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//删除文件
@Test
public void delete(){
try {
minioClient.removeObject(
RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.png").build());
System.out.println("删除成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("删除失败");
}
}
}
- 测试查询文件 (下载文件)
/**
* @description 测试MinIO
*/
public class MinioTest {
static MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//查询文件 (从minio中下载)
@Test
public void getFile() {
// 构建文件参数
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("001/test001.png").build();
try(
// 查询minio服务获取一个流对象
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
// 指定输出流
FileOutputStream outputStream = new FileOutputStream(new File("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test_copy.png"));
) {
// 把输入流拷贝到输出流, 完成文件的下载
// org.apache.commons.compress.utils.IOUtils;
IOUtils.copy(inputStream,outputStream);
// 校验文件的完整性:
// 思路: 把原始文件和下载的文件对两个文件的MD5加密值进行比较
// 1.获取原始文件的输入流
FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test.png"));
// 2.获取下载文件的输入流
FileInputStream fileInputStream2 = new FileInputStream(new File("D:\\UserDatas\\Note\\16Project\\xue-cheng-zai-xian\\minio\\test_copy.png"));
// 3.把两个文件流进行md5加密
// org.apache.commons.codec.digest.DigestUtils;
String source_md5 = DigestUtils.md5Hex(fileInputStream1);
String local_md5 = DigestUtils.md5Hex(fileInputStream2);
if(source_md5.equals(local_md5)) {
System.out.println("文件下载成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
上传图片
需求分析
课程图片是宣传课程非常重要的信息,在新增课程界面上传课程图片,也可以修改课程图片。
上传课程图片总体上包括两部分:
1、上传课程图片前端请求媒资管理服务将文件上传至分布式文件系统,并且在媒资管理数据库保存文件信息。
2、上传图片成功保存图片地址到课程基本信息表中。
1、前端进入上传图片界面
2、上传图片,请求媒资管理服务。
3、媒资管理服务将图片文件存储在MinIO。
4、媒资管理记录文件信息到数据库。
5、前端请求内容管理服务保存课程信息,在内容管理数据库保存图片地址。
涉及到的数据表有:
课程信息表中的图片字段、媒资数据库的文件表,下边主要看媒资数据库的文件表。
环境准备
video这个桶用于存放课程视频, 其他的资源放在mediafiles这个桶
在nacos中配置minio的相关信息,进入media-service-dev.yaml:
minio:
endpoint: http://192.168.101.65:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
在media-service工程编写minio的配置类:
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;
/**
* @description minio配置
*/
@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;
}
}
代码解释
- 这段代码定义了一个Spring配置类MinioConfig,用于创建并配置MinioClient对象。
- 通过@Value注解从配置文件中读取MinIO的endpoint、accessKey和secretKey。
- 使用MinioClient.builder()方法构建MinioClient实例,并设置endpoint和凭据。
- 使用@Bean注解, 将构建好的MinioClient实例作为Spring的Bean返回。
- 使用的时候, 直接注入该配置类, 就可以得到minioClient对象
接口设计
根据需求分析,下边进行接口定义,此接口定义为一个通用的上传文件接口,可以上传图片或其它文件。
/**
* @author Mr.M
* @version 1.0
* @description 媒资文件管理接口
* @date 2022/9/6 11:29
*/
@Api(value = "媒资文件管理接口", tags = "媒资文件管理接口")
@RestController
public class MediaFilesController {
@Autowired
MediaFileService mediaFileService;
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload) throws IOException {
return null;
}
}
- 请求地址:/media/upload/coursefile
- 请求格式为 multipart/form-data
- 接收一个MultipartFile类型的文件数据
- 与前端约定的文件名称是filedata
响应参数:文件上传成功后, 返回文件信息给前端,如下
{
"id": "a16da7a132559daf9e1193166b3e7f52",
"companyId": 1232141425,
"companyName": null,
"filename": "1.jpg",
"fileType": "001001",
"tags": "",
"bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
"fileId": "a16da7a132559daf9e1193166b3e7f52",
"url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
"timelength": null,
"username": null,
"createDate": "2022-09-12T21:57:18",
"changeDate": null,
"status": "1",
"remark": "",
"auditStatus": null,
"auditMind": null,
"fileSize": 248329
}
定义上传响应模型类:PO对应的是数据库中表的信息, 直接把数据封装到PO类中返回给前端也可以, 但是扩展性不强, 后续前端可能需要我们返回更多的数据, 所以我们定义一个DTO类
package com.xuecheng.media.model.dto;
import com.xuecheng.media.model.po.MediaFiles;
import lombok.Data;
/**
* 上传普通文件成功响应结果
*/
@Data
public class UploadFileResultDto extends MediaFiles {
}
service开发
实现目标: 向media_files表插入文件信息, 向minio系统上传文件
定义文件信息传输对象, 用于封装文件信息
package com.xuecheng.media.model.dto;
import lombok.Data;
/**
* 文件信息
*/
@Data
public class UploadFileParamsDto {
/**
* 文件名称
*/
private String filename;
/**
* 文件类型(文档,音频,视频)
*/
private String fileType;
/**
* 文件大小
*/
private Long fileSize;
/**
* 标签
*/
private String tags;
/**
* 上传人
*/
private String username;
/**
* 备注
*/
private String remark;
}
定义接口和实现类
/**
* @author Mr.M
* @version 1.0
* @description 媒资文件管理业务类
* @date 2022/9/10 8:55
*/
public interface MediaFileService {
/**
* 上传文件
*
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件信息
* @param localFilePath 文件磁盘路径
* @return 文件信息
*/
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath);
}
package com.xuecheng.media.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.io.FileInputStream;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/10 8:58
*/
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {
@Autowired
MediaFilesMapper mediaFilesMapper;
@Autowired
MinioClient minioClient;
//普通文件桶
@Value("${minio.bucket.files}")
private String bucket_mediafiles;
//视频文件桶
@Value("${minio.bucket.videofiles}")
private String bucket_video;
/**
* 文件上传
*
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件信息
* @param localFilePath 文件磁盘路径
* @return
*/
@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
// 1.上传文件
// 1.1获取mimeType
String filename = uploadFileParamsDto.getFilename();
String extension = filename.substring(filename.lastIndexOf("."));
String mimeType = getMimeType(extension);
// 1.2获取对象名称(根据日期生成子目录, 使用MD5值作为文件名)
String defaultFolderPath = getDefaultFolderPath();
String fileMd5 = getFileMd5(new File(localFilePath));
String objectName = defaultFolderPath + fileMd5 + extension;
// 1.3将文件上传到minio中
boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);
if(!result) {
XueChengPlusException.cast("上传文件失败");
}
// 2.保存文件信息到数据库
MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
if(mediaFiles == null) {
XueChengPlusException.cast("文件上传后保存信息失败");
}
// 3.准备返回的对象
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);
//补充其他信息
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;
}
/**
* @param localFilePath 文件地址
* @param bucket 桶
* @param objectName 对象名称
* @return void
* @description 将文件写入minIO
* @author Mr.M
* @date 2022/10/12 21:22
*/
public boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) {
try {
// 构建文件参数
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.filename(localFilePath)
.contentType(mimeType)
.build();
// 执行上传操作
minioClient.uploadObject(testbucket);
log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
System.out.println("上传成功");
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage(), e);
XueChengPlusException.cast("上传文件到文件系统失败");
}
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;
}
}
// 根据文件扩展名取出mimeType
private String getMimeType(String extension) {
if (extension == null)
extension = "";
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
//通用mimeType,字节流
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if (extensionMatch != null) {
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
}
完善接口
在接口层调用逻辑层, 完成文件的上传
/**
* @author Mr.M
* @version 1.0
* @description 媒资文件管理接口
* @date 2022/9/6 11:29
*/
@Api(value = "媒资文件管理接口", tags = "媒资文件管理接口")
@RestController
public class MediaFilesController {
@Autowired
MediaFileService mediaFileService;
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
Long companyId = 1232141425L;
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
//文件大小
uploadFileParamsDto.setFileSize(filedata.getSize());
//图片
uploadFileParamsDto.setFileType("001001");
//文件名称
uploadFileParamsDto.setFilename(filedata.getOriginalFilename());//文件名称
//文件大小
long fileSize = filedata.getSize();
uploadFileParamsDto.setFileSize(fileSize);
//创建临时文件
File tempFile = File.createTempFile("minio", "temp");
//上传的文件拷贝到临时文件
filedata.transferTo(tempFile);
//文件路径
String absolutePath = tempFile.getAbsolutePath();
//上传文件
UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, absolutePath);
return uploadFileResultDto;
}
}
接口测试
启动媒资服务, 首先使用httpclient测试
### 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="1.png"
Content-Type: application/octet-stream
< C:\Users\Lenovo\Desktop\1.png
再进行前后端联调测试
- 在新增课程、编辑课程界面上传图,保存课程信息后再次进入编辑课程界面,查看是否可以正常保存课程图片信息。
- 上图图片完成后,进入minIO管理系统,查看文件列表中是否有刚刚上传的图片信息。
事务优化
由于uploadFile方法中, 要把文件上传到minio中, 还要把文件信息保存到数据库中, 为了保持数据一致性, 要进行事务控制
目前的事务处理是在uploadFile方法上添加@Transactional注解,uploadFile方法调用前就会开启数据库事务, 方法调用结束后会提交事务, 方法调用报错后会回滚事务
/**
* 文件上传
*/
@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
// ... ...
// 1.将文件上传到minio中
boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);
// 2.保存文件信息到数据库
MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
... ...
return uploadFileResultDto;
}
虽然实现了需求, 但是并不合理, 因为上传文件是网络操作, 执行时间可能会很长,那么数据库的事务持续时间就会变长, 导致数据库链接释放很慢,最终导致数据库链接不够用。
解决方案
我们只将addMediaFilesToDb方法添加事务控制即可,uploadFile方法上的@Transactional注解去掉。
/**
* @description 将文件信息添加到文件表
*/
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
//从数据库查询文件
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
//文件不存在才需要添加到数据库
if (mediaFiles == null) {
... ...
//保存文件信息到文件表
int insert = mediaFilesMapper.insert(mediaFiles);
}
return mediaFiles;
}
代码测试
我们人为在 int insert = mediaFilesMapper.insert(mediaFiles); 下边添加一个异常代码int a=1/0;
- 测试是否事务控制。很遗憾,事务控制失败。
- 方法上已经添加了@Transactional注解为什么该方法不能被事务控制呢?
- 下边分析原因:
- 如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:
- 如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制
- 所以判断该方法是否可以事务控制, 必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解。
- 现在在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法。
- 为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。
- 我们发现在uploadFile方法中去调用addMediaFilesToDb方法不是通过代理对象去调用。
如何解决呢?
通过代理对象去调用addMediaFilesToDb方法即可解决。
- 在MediaFileService的实现类中注入MediaFileService的代理对象
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/10 8:58
*/
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {
@Autowired
MediaFileService currentProxy;
}
- 将addMediaFilesToDb方法提成接口
/**
* @author Mr.M
* @version 1.0
* @description 媒资文件管理业务类
* @date 2022/9/10 8:55
*/
public interface MediaFileService {
/**
* @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
*/
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
}
- 使用代理对象调用addMediaFilesToDb方法
- 再次测试, 事务可以正常控制。