SpringCloud是什么
- SpringCloud是一个微服务框架,将一种分布式架构落地的方案
- 单体架构:业务所有功能集中在一个项目中开发,架构简单,但是耦合度高
- 分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。
- 将项目进行拆分后,需要考虑多个问题
- 服务拆分力度
- 服务集群地址如何维护
- 服务之间如何互相调用,也就是各个业务功能模块互相调用,需要注册中心进行维护
- 服务健康状态感知,也就是服务之间互相调用时,在一个服务出现问题时,其他服务需要感知并作出处理
- 微服务配置,需要统一在配置中心进行配置
- 用户访问,则需要通过一个统一的服务网关进行访问,由网关将请求路由到微服务集群
远程调用
- 远程调用就是指一个业务的功能需要访问另一个业务的数据,此时需要用到远程调用,而不能直接去另一个模块的数据库去取,因为每一个业务模块都独立的。
示例
订单业务
服务启动后,查询订单数据
用户业务
服务启动后,查询用户id为1的
远程调用
- 我们需要在查询到订单数据时,连带的查询用户数据库中与订单数据里的userId相匹配的人物信息,由于每个模块是独立的,所以不能直接去用户数据库中进行访问,所以可以通过模拟发送请求的方式来获取对应的用户信息。
注册RestTemplate
在OrderApplication.java中创建RestTemplate并注入Spring容器
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 创建RestTemplate并注入Spring容器
* @return
*/
public RestTemplate restTemplate(){
return new RestTemplate();
}
}在Controller中将请求回来的订单数据中的id作为参数,手动发送请求,获取对应的用户数据
public class OrderController {
private OrderService orderService;
private RestTemplate restTemplate;
public Order queryOrderByUserId( { Long orderId)
Order order = orderService.queryOrderById(orderId);
// user服务在8081端口,将查询到的ID作为参数发送请求
String url = "http://localhost:8081/user/" + order.getUserId();
//get请求用getForObject,post则用postForObject
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
// 根据id查询订单并返回
return order;
}
}结果
Eureka注册中心
上面的这种方法进行远程调用,可以看到有许多硬编码信息,这是不合理的,所以我们可以通过注册中心,管理服务。
服务提供者(被访问的服务)启动时,都会在注册中心注册自己的信息,并交给eureka保存,服务消费者(访问别人的服务)根据服务名称从注册中心获取需要的服务
如果注册中心返回消费者多个服务,则消费者利用负载均衡从服务列表中选择一个
提供者每隔30s向注册中心发送心跳请求,以保持连接,心跳不正常的则被剔除,以保证消费者获取的都是最新的信息
在Eureka架构中主要分为两类
- EurekaServer:注册中心,也就是服务端,记录服务信息,心跳监控
- EurekaClient:客户端,也就是所有的服务
- Provider:服务提供者
- Consumer:服务消费者
Eureka注册中心使用
Eureka服务搭建
新建项目模块eureka-service,引入spring-cloud-starter-netflix-eureka-server依赖,这里使用maven8版本,创建项目默认的11版本会报错
<parent>
<groupId>cn.itcast.demo</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- eureka服务器依赖 这里不用指定版本,因为其父工程的配置文件中已经指定了版本-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>添加注解。创建Main文件,并在Main文件中添加注解**@EnableEurekaServer,使用
nacos
也要添加该注解**,开启注册中心服务
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}新建application.yml文件,进行服务配置,这里eureka自己也是一个服务,所以eureka服务信息也需要配置,把自己注册到eureka
server:
port: 10086
spring:
application:
name: eurekaserver
eureka:
instance:
hostname: localhost
client:
service-url: ## eureka server的地址信息
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/配置完成之后,启动eureka服务的Main文件,访问localhost:10086
Eureka服务注册
其他服务注册,首先pom中引入eureka-client包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>application.yml配置文件中进行服务注册
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/springcloud_test?useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice
eureka:
instance:
hostname: localhost
client:
service-url: ## eureka server的地址信息(这里是服务器地址信息)
defaultZone: http://${eureka.instance.hostname}:10086/eureka/刷新localhost:10086,可以看到其他两个服务也注册上了
服务发现
现在我们进行服务间的访问,和远程调用示例实现的功能一样
修改controller中的代码,将url的访问路径由ip+端口的模式修改为服务名称
public Order queryOrderByUserId( { Long orderId)
Order order = orderService.queryOrderById(orderId);
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}在启动类OrderApplication.java的中为RestTemplate添加负载均衡注解
/**
* 创建RestTemplate并注入Spring容器
* @return
*/
public RestTemplate restTemplate(){
return new RestTemplate();
}访问该接口,也可以返回用户的信息。
负载均衡(Eureka)
上文中的RestTemplate是对httpClient的封装,类似于axios,可以通过它发送请求
在创建RestTemplate的地方添加@LoadBalance注解,那么使用RestTemplate发送的请求都会被Ribbon拦截
-
负载均衡就是在访问服务器的时候,保证访问量的一个均衡
负载均衡示例
复制一个userserver服务
右键userserver,复制一个服务,注意改变端口,启动
访问orderserver
- 访问6次userserver,可以看出,1,3,6访问8081服务,2,4,5访问8082服务


结论
- 配置LoadBalance注解,则开启负载均衡,默认负载均衡规则为轮询,除轮询外,还有一下负载均衡规则可以配置
负载均衡规则配置
在应用文件中配置
在OrderApplication中配置
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 创建RestTemplate并注入Spring容器
* @return
*/
public RestTemplate restTemplate(){
return new RestTemplate();
}
// 配置随机规则,规则接口是IRule
public IRule randomRule(){
return new RandomRule();
}
}重启order服务后,重新访问6次可以看出,4,6在8082端口访问,1,2,3,5在8081端口访问
配置文件配置
在order-service服务的配置文件application.yml中添加以下配置
userservice: #对userservice开启以下负载均衡规则
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机负载均衡策略这种方式也开启了随机负载均衡策略
不同
- 第一种方式是对所有的服务请求都开启了随机负载均衡
- 第二种方式可以指定相应的微服务。
负载均衡加载方式
Ribbon默认加载方式为懒加载,第一次访问时才会去创建负载均衡的这个东西,所以第一次访问时,时间会很长
饥饿加载则会在项目启动的时候创建,降低第一次访问的耗时。
饥饿加载的配置在配置文件application..yml中配置
# 指定服务配置负载均衡
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机负载均衡策略
# 配置加载方式
ribbon:
eager-load:
enabled: true # 开启懒加载
clients: userservice #使用这种加载方式的服务
Nacos注册中心
服务启动
安装完Nacos之后,到bin文件夹下,打开cmd,运行下面命令即可启动Nacos
.\startup.cmd -m standalone
服务使用
在父工程pom.xml中添加alibaba的依赖,Nacos是属于阿里巴巴的
<!-- Nacos管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>将子工程中引入的eureka依赖删除
子工程的pom文件中引入nacos客户端依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>修改application.yml配置文件,配置nacos服务的地址信息
spring:
cloud:
nacos:
server-addr: 10.130.15.112:8848 #nacos服务器地址
application:
name: coupon # 在 Spring Boot 应用程序中,如果不指定 spring.application.name 属性,应用程序将无法在注册中心(如 Eureka、nacos 等)进行注册。spring.application.name 用于标识应用程序的名称,这是注册中心用来区分不同服务的关键属性。之后启动服务,并打开Nacos可视化界面,可以看到服务都已被注册,并可正常访问
Nacos服务分级存储模型
一级是服务,二级是集群(一般指地域),三级是实例
一个userserver服务,有三个实例,两个在第一个集群中,一个在第二个集群中。
设置实例的集群属性:在application.yml中添加spring.cloud.nacos.discovery.cluster-name属性即可。将userservice的集群先设置成HZ,启动8082的实例,在改变集群为BJ,启动8081端口的实例
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
cluster-name: HZ #集群名称为杭州在Nacos上点击服务的详情,可以看到对应集群的服务
Nacos负载均衡
Nacos默认的负载均衡策略也为轮询负载均衡,当我们配置orderservice集群为HZ,并启动服务,希望负载均衡策略优先考虑属于同一集群的服务,这时需要去配置负载均衡的策略了。
在orderservice下配置负载均衡规则
userservice: #针对哪个服务
ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机负载均衡策略
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 同集群负载均衡再次启动orderservice服务,并访问,可以看到访问的都为同一集群的服务,当同一集群的服务都关掉时,访问其他集群的服务,并会爆出警告
Nacos权重
当属于同一集群的服务性能不同时,我们希望更多的请求访问性能越好的,此时需要配置属于同一集群的服务的权重来实现,如下所示
权重值的设置一般在0-1之间,权重越高,被访问的频率越高,权重设置为0则完全不会被访问
Nacos环境隔离
上面说过Nacos分级模型一级是服务,二级是集群(一般指地域),三级是实例,除此之外,在公司内部,还会有不同的环境,比如开发环境,测试环境等等,此时对于不同环境的服务,需要进行环境隔离,设置命名空间,除此之外,同一命名空间内部,关联交强的可以设置同一组内。
设置命名空间,首先需要在Nacos上新建一个命名空间
在orderservice的application.yml中进行配置,将其放到dev环境中
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
cluster-name: HZ
namespace: 49430540-2aa3-46a2-9438-e4d53806fb57 # 命名空间ID此时再次启动服务,由于有环境隔离,所以是访问不到usersevice服务的
Nacos配置管理服务
通过在Nacos平台新建配置文件,让服务进行读取,对于一些通用的配置,可以不用在每个微服务下都进行配置,并且直接在Nacos更改配置,可以实现配置热更新,不用重启微服务(之前更改微服务的配置文件,需要重启微服务)
新建配置之后,就可以去微服务进行配置加载Nacos配置文件
微服务获取配置的流程
项目启动 ===> 读取本地配置文件 ===> 创建Spring容器 ===> 加载bean
在新建了Nacos配置后,我们需要微服务加载该配置,所以流程需要更改。
项目启动 ===> 读取Nacos配置文件 ===> 读取本地配置文件 ===> 创建Spring容器 ===> 加载bean
由于Nacos地址信息都在本地配置文件中,读取Nacos配置文件,必须得要先知道Nacos地址信息,所以我们需要新建一个bootstrap.yml文件,该文件为引导文件,优先级高于本地配置文件applicatioon.yml,在bootstrap配置文件中进行Nacos配置
微服务中加载Nacos配置管理服务
在pom文件中引入Nacos配置管理依赖
<!-- Nacos配置管理依赖--> |
resources目录下新建bootstrap.yml文件并对Nacos进行配置
服务名称,环境名称,后缀名都是和配置列表的名称一一对应的。
spring:
application:
name: userservice #服务名称
profiles:
active: dev #环境名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
config:
file-extension: yaml #后缀名在Nacos下配置的配置文件如下,也就是日期格式化的配置。
pattern:
dateformat: MM-dd HH:mm:ss:SSS
测试配置
为了测试这个配置是否生效,可以通过写一个接口来测试,配置文件中的属性通过@Value读取注入,访问now接口时,会返回一个格式化日期
public class UserController {
private String dateformat;
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}访问localhost:8081/user/now
新版本配置方式
- 在
Spring Cloud Alibaba 2.2.5
版本及以上,Spring Boot 2.4
及以上版本中,配置中心的配置方式发生了一些变化。你需要在配置文件中添加spring.config.import
属性来指定 Nacos 配置中心。
添加nacos配置依赖同上
在application.yml添加spring.config.import
属性
spring: |
使用方式同上,使用@Value注解读取配置
Nacos配置热更新实现
在注入配置属性的类上方加入@RefreshScope注解
- 增加@RefreshScope后,更改了Nacos配置文件后,不用重启微服务即可更新
|
新建类实现
- 在SpringBoot文件中,有讲到,读取配置文件可以通过新建一个类,并通过@ConfigurationProperties注解实现读取,通过这种方式读取配置文件不需要添加其他东西即可实现热更新
Nacos配置共享
- 服务加载读取Nacos的时候,主要读取两个文件,一个是 [服务名称]_[环境名称].[后缀名],例如userservice-dev.yaml,另一个是[服务名称].[后缀名],例如userservice.yaml
- 当不同环境的服务拥有相同的配置时,可以将该配置以第二种方式命名来实现
配置共享实现
- Nacos下新建userservice.yaml
由于使用@Value读取配置文件的方式在未配置该属性的时候会报错,所以使用新建配置Pattern类的方式来实现
public class Pattern {
private String dateformat;
private String name;
}在controller中,通过自动注入来进行属性的读取
public class UserController {
private UserService userService;
private Pattern pattern;
public ArrayList<String> profile() {
ArrayList<String> list = new ArrayList<>();
list.add(pattern.toString());
return list;
}
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern.getDateformat()));
}
}将8081服务的userservice的环境设置为dev,8082端口的userservice服务设置为test环境,启动服务并访问
访问结果如下
- 可以看到对于共享的属性,不管哪个环境都可以访问到,dev环境的配置只有dev环境的可以访问到。
Nacos集群配置
在上面启动Nacos时的命令为.\startup.cmd -m standalone,因为Nacos是默认集群模式启动,所以单例模式下需要加上-m standalone。
集群架构
- Nacos节点的负载均衡使用nginx实现
- 这里的Nacos三个节点应该是部署在三台不同的服务器上的,这里由于条件有限,所以使用复制粘贴的方式,使用不同的端口模拟Naco服务节点。三个Nacos服务节点的端口分别为8845,8856,8847
- 使用Mysql数据库集群取代Nacos的内置数据库
Nacos集群配置流程
准备
新建nacos数据库,并创建多个mysql表
,首先将nacos=>config目录下配置文件cluster.config.example,更名为cluster.config,并在该文件中添加三个Nacos节点的地址,高版本Nacos不要配置连续的地址,不然会被错
127.0.0.1:8843
127.0.0.1:8845
127.0.0.1:8847修改application.properties文件,将Mysql的配置取消注释
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
启动
将Nacos复制三份,模拟三个nacos节点,并在各自的application.properties文件中修改各自的端口号
server.port=8843
server.port=8845
server.port=8847
分别在cmd启动三个Nacos节点
startup.cmd
配置Nginx反向代理,下载Nginx后,进入config-nginx.config,添加以下代码
http {
include mime.types;
default_type application/octet-stream;
#新增的 配置集群地址
upstream nacos-cluster {
server 127.0.0.1:8843;
server 127.0.0.1:8845;
server 127.0.0.1:8847;
}
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
#新增的 使用/nacos代理集群地址,并负责负载均衡
location /nacos {
proxy_pass http://nacos-cluster;
}
}
}启动Nginx
start nginx.exe
由于设置了反向代理,直接访问localhost/nacos,就可以访问nacos,80端口默认省略
服务中配置nacos端口则也是80,通过80访问到nginx,nginx通过负载均衡随机分配到nacos服务器上
总结
使用nginx会有负载均衡效果,所以nginx会在三个nacos下做一个负载均衡的效果,默认轮询负载均衡
在Nacos新建的服务配置,命名空间等都会存入对应的表中,但是现在新建服务配置会失败,可能和nacos,mysql版本对应有关系。存入数据库中,其他Nacos也可以访问该配置。
微服务中配置的nacos地址为nginx地址
正向代理和反向代理
正向代理
用户 ===> 代理服务器 ===> 服务地址
反向代理,用户是不感知服务被代理的
用户 ===> 反向代理地址 <=== (服务地址1,服务地址2,服务地址3)
Feign
- Feign为一个http客户端,之前我们使用的是RestTemplate,现在使用Feign来进行发送请求
相比于RestTemplate的不同
之前使用RestTemplate发送请求,参数复杂时难以维护
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
使用Feign进行http请求(微服务之间进行请求)
引入依赖,
<!-- Feign依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 引入负载均衡依赖,不然会报错 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>添加注解,在启动文件中添加注释@EnableFeignClients
// 开启Feign客户端,可以指定需要扫描的package,例如:@EnableFeignClients(basePackages = "com.qr.mall.member.feign"),不指定则扫描整个项目中的所有Feign客户端接口。可能会导致命名冲突。
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}client文件夹下新建UserClient(要远程调用user服务,就写UserClient)接口文件,编写Feign客户端,客户端需要声明以下信息,和Restful规则一样
- 服务名称
- 请求方式
- 请求路径
- 请求参数
- 返回值类型
//远程调用客户端服务名称
public interface UserClient {
// 想要调用的服务的签名,也就是他的mapping和方法的第一行复制过来即可
User findById(; Long id)
}Controller中使用
public class OrderController {
private OrderService orderService;
private UserClient userClient;
// feign调用
public Order queryOrderByUserId( { Long orderId)
Order order = orderService.queryOrderById(orderId);
User user = userClient.findById(order.getUserId());
order.setUser(user);
// 根据id查询订单并返回
return order;
}这样也可以正常调用,使用Feign使代码看起来更优雅
Feign自定义配置
- 日志打印自定义的两种配置方式,两种配置都可以规定该配置是针对于某个服务或者全局的
- 配置文件中配置
- 自定义类,通过在启动类的@EnableFeignClients注解中添加该类的类名来加载配置的日志类
Feign性能优化
更换Feign底层http客户端
Feign底层使用的http客户端为URLConnection,该客户端不支持连接池,所以可以将其更换为httpClient或者OKHttp
更换为httpClient客户端
- 导包
<!-- 引入httpClient依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>配置Feign
feign:
httpclient:
enabled: true # 开启httpclient
max-connections: 200 # 最大连接数
max-connections-per-route: 50 #单个路径的最大连接数
Feign配置抽取
- 当我们有多个模块都需要引入用户信息,都需要写一个client,POJO类,都是重复的配置,所以可以将其抽取为一个单独的模块,通过在配置页面中引入坐标来加载需要的配置
配置抽取实现
新建module,将一些公共部分进行抽取,像POJO类,Client文件
在服务的配置文件中引入feign-api坐标
<!-- 对应的封装feign-api -->
<dependency>
<groupId>cn.qr.feign</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>将微服务中引入feign-api那些文件的地方进行引入路径的修改
由于Spring扫描路径默认为启动类所在的包,例如order-service,spring默认扫描的包为cn.qr.order,而feign-api中clients文件夹下的UserClient路径为cn.qr.feign,所以此时启动order-service服务会报错,找不到这个类,所以需要在orderservice的启动文件的@EnableFeignClients注解后,增加要加载客户端类
// 开启Feign客户端
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}服务可以正常启动,并且其他微服务也可以复用该api配置
网关Gateway
- 网关的作用
- 对用户进行身份认证,权限校验
- 将用户请求路由到微服务,并实现负载均衡
- 对用户请求进行限流
搭建网关服务
新建module gateway
目录结构如图所示
将Main文件配置为SpringBoot启动类,或者直接新建一个SpringBoot的模块(配置就直接配好了),并在pom配置文件中引入网关依赖和Nacos服务发现依赖,引入nacos依赖是因为网关服务也需要注册到nacos。
<dependencies>
<!-- 网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- nacos服务发现依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>在application.yml中进行配置
server:
port: 10010 # 网关端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由id
uri: lb://userservice # uri指向的服务 lb代表负载均衡 userservice是服务名
predicates:
- Path=/user/** # 断言,路径相匹配的进行路由,判断是否以/user开头
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**启动服务后,通过localhost:10010/user/1 ,localhost:10010/order/1 进行访问,通过网关进行访问
路由服务流程
网关过滤器
- 对请求和响应的结果进行过滤,可以添加请求头,响应头等等操作
使用
网关中对order服务添加过滤器
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由id
uri: lb://userservice # uri指向的服务 lb代表负载均衡 userservice是服务名
predicates:
- Path=/user/** # 断言,路径相匹配的进行路由,判断是否以/user开头
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
filters:
- AddRequestHeader=token, blue # 过滤器,添加请求头在controller中添加参数@RequestHeader(value = “token” , required = false) String token并打印,这个token必须和配置文件中逗号前的名称相同
public Order queryOrderByUserId( { Long orderId , String token)
System.out.println("token = " + token);
Order order = orderService.queryOrderById(orderId);
User user = userClient.findById(order.getUserId());
order.setUser(user);
// 根据id查询订单并返回
return order;
}通过配置defaultFilters属性,与routes在同一层级下,可以配置对所有服务都生效的过滤器
全局过滤器
- 相比于gatefilter,全局过滤器是通过自己写代码配的,而网关过滤器是通过配置文件的方式实现的
全局过滤器实现
网关服务中新建一个过滤器文件,这里新建名为AuthorizeFilter的过滤器
//过滤器的优先级,数字越小,优先级越高
//注入到Spring中
public class AuthorizeFilter implements GlobalFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/**
* @param exchange 请求上下文,里面可以获取Request Response等信息
* @param chain 过滤器链,可以通过chain继续向下执行,传递给下一个过滤器
* @return {@code Mono<Void>}返回表示当前过滤业务结束
*/
// 获取请求参数,是query参数中的第一个参数authorization是否为admin
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
// 获取authorization参数
String authorization = queryParams.getFirst("authorization");
// 判断authorization值是否为admin
if("admin".equals(authorization)){
// 放行
return chain.filter(exchange);
}
// 不是admin,设置状态码为401,UNAUTHORIZED代表的就是401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 直接结束
return exchange.getResponse().setComplete();
}
}重启网关后,正常访问localhost:10010/order/101,报401,访问localhost:10010/order/101?authorization=admin才会返回数据
过滤器执行顺序
网关跨域问题处理
application.yml中添加以下配置代码
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由id
uri: lb://userservice # uri指向的服务 lb代表负载均衡 userservice是服务名
predicates:
- Path=/user/** # 断言,路径相匹配的进行路由,判断是否以/user开头
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
filters:
- AddRequestHeader=token, blue # 过滤器,添加请求头
# 跨域
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*" # 允许所有域名
allowedMethods: # 允许的请求方式
- GET
- POST
- PUT
- DELETE
allowedHeaders:
- "*"
allowCredentials: true # 是否允许携带cookie
maxAge: 1800 # 跨域时间
add-to-simple-url-handler-mapping: true # option请求被拦截问题
Docker
- Docker将开发中的应用,依赖,函数库,配置一起打包,形成可移植的镜像
- Docker应用 运行在容器中,使用沙箱机制,相互隔离
- Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的linux内核,因此可以在任意Linux操作系统上运行,Linux内核版本不能低于3.10
- 镜像:Docker将应用程序等东西打包在一起称为镜像
- Dockerhub:Docker镜像的托管平台,这样的平台称为Docker Registry
Docker服务命令
安装Dokcer |
Docker命令
构建镜像 |
Dockers容器命令
docker ps # 查看所有运行的容器及状态 |
mysql容器
启动mysql,--restart=always自动启动 -d后台运行 -p端口映射 -e环境变量设置,设置登录密码. -v代表挂载 将mysql容器内部的/var/log/mysql /var/lib/mysql /etc/mysql挂载到宿主机上的/mydata/mysql/log /mydata/mysql/data /mydata/mysql/conf位置,以后更改不用进容器内部修改,这些文件主要是日志,数据以及配置文件,最后的mysql代表启动哪个mysql版本 |
redis容器
由于redis容器中/etc/redis并没有redis.conf文件,所以会导致在宿主机中将redis.conf当为目录,而不是文件,因此需要提前创建文件 |
Tomcat命令
启动一个名为 "my-tomcat" 的 Tomcat 容器,并将容器内 8080 端口映射到主机的 8080 端口 |
nginx容器
docker run: 执行Docker容器运行命令。 |
EacticSearch容器
mkdir -p /mydata/elasticsearch/config |
权限问题
文件挂载后启动服务,没有权限时,使用以下命令 |
Kibana容器(EacticSearch的可视化界面)
ELASTICSEARCH_HOST=http://192.168.202.1:9200: 代表EacticSearch服务地址和端口 |
后端服务
root文件夹下,构建当前文件夹下的Dockerfile |
数据卷Volume
- 容器与容器中的数据耦合度过高,像nginx中的index.html文件,不能使用vi编辑,很不方便,所以数据卷就是解决这个问题,数据卷将容器中的数据映射到宿主机上的某个位置。两个数据双向绑定。除此之外,多个容器之间的数据共享也可以通过这个实现了,只需要全部指向同一个数据卷即可。
数据卷命令
docker volume [command] |
不使用数据卷
- 其实我们可以自己创建目录,在运行容器的时候使用-v进行挂载,相比于数据卷,我们可以自定义目录位置,比较方便
- 区别
- 数据卷自动化管理,不用操心目录的管理
- 自己创建目录自己可以随意创建目录位置,但是需要目录管理
Dockerfile自定义镜像
镜像时分层的,每一层称为layer
- BaseImage层:包含基本的函数库,环境变量,文件系统
- Entrypoint层:入口,是镜像中应用启动的命令
- 其他:在BaseImage基础上添加依赖,安装程序,完成整个应用的安装和配置
Docker基本操作,可以看出,我们可以通过对Dockerfile文件进行构建来生成镜像
Dockerfile文件,类似于命令行文件,下面为一个Dockerfile文件,自定义一个Java项目的镜像
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar使用步骤
构建,通过docker build构建Dockerfile文件,-t代表文件的标签,也就是名字,.代表Dockerfile所在的文件目录
docker build -t java:1.0 .
构建完成之后,镜像就被加载到本地了,就可以运行容器
在构建Java项目时,Dockerfile文件每次都要配置环境变量,很麻烦,所以java:8-alpine基础镜像就包含了上面的环境配置步骤。
# 指定基础镜像
FROM java:8-alpine
COPY ./docker-demo.jar /tmp/app.jar
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
DockerCompose
基于Compose文件帮我们快速的部署分布式应用,无需手动一个个创建容器和运行容器
compose文件是一个文本文件,通过指令定义容器中的每个容器如何运行,也就是将docker run命令拆分为yml格式运行
ll命令查看文件的权限,通过chmod命令增加权限
chmod +x /usr/local/bin/docker-compose #增加执行权限
DockerCompse的使用
安装DockerCompose
自动补全命令,
补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-composeDockerCompose文件
version: "3.2"
services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"之前学过的微服务,通过maven都打成jar包,将其制作为镜像
FROM java:8-alpine |
由于各个微服务之间互相引用,之前都是使用localhost进行访问的,可能各个服务不再同一台机器上,所以需要改成服务名称访问
文件上传到虚拟机上后,执行命令
docker-compose up -d
Docker私有镜像仓库
- 本地私有仓库部署
MQ(MessageQueue)
- 异步调用常见实现就是事件驱动模式,
- 当使用同步调用,调用A事件后,需要执行B,C,D事件会导致如果再有后续事件添加不方便,耦合性较高,一个事件失败会导致剩余事件阻塞。
- 异步调用,将事件交给一个中间人broker,所有事件都订阅broker,当A事件结束了,就返回给用户结果,通知broker,broker发送通知说A事件结束,订阅了broker的事件就会执行自己。一个事件执行失败不会影响其他事件的执行。
- MQ就是事件驱动架构中的中间人,MQ常见的实现方式
- RabbitMQ
- ActiveMQ
- RocketMQ
- kafka
RabbitMQ
图示:
概念
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,对queue,exchange等资源的逻辑分组
安装
找到mq镜像文件
在虚拟机中加载镜像
docker load -i mq.tar
运行MQ容器
docker run \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq \
--hostname mq1 \
-p 15672:15672 \ (浏览器访问端口)
-p 5672:5672 \ (用户连接端口)
-d \
rabbitmq:3-management
常见消息模型
HeloWorld案例
publisher服务中
建立连接
ConnectionFactory factory = new ConnectionFactory();
设置连接参数
factory.setHost("192.168.202.1"); //虚拟机地址
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("root");建立连接
Connection connection = factory.newConnection();
创建通道
Channel channel = connection.createChannel();
创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());关闭通道和连接
channel.close();
connection.close();
consumer服务中
建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.202.1");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
Connection connection = factory.newConnection();创建通道
Channel channel = connection.createChannel();
创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);订阅消息:当queueName队列里有消息,后面的回调函数就会立即执行
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
SpringAMQP
- AMQP(Advanced Message Queuing Protocol),高级消息队列协议
- SpringAMQP基于AMQP协议的规范API,提供模版来接收和发送消息,包含两部分,spring-amqp是基础抽象,spring-rabbit是底层默认的实现。
- 由于rabbitmq的实现的代码过于复杂,所以出现了SpringAMQP
使用
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>配置连接,在publisher服务中编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 192.168.202.1 # RabbitMQ server IP
port: 5672 # RabbitMQ server port
username: root # RabbitMQ server username
password: root # RabbitMQ server password
virtual-host: / # RabbitMQ server virtual host编写publisher测试,通过调用RabbitTemplate中的convertAndSend发送消息
private RabbitTemplate rabbitTemplate;
String queueName = "simple.queue";
String message = "hello spring amqp";
rabbitTemplate.convertAndSend(queueName, message);consumer服务中除了在application.yml中新增连接配置外,还需要编写接收类来接收消息
public class SpringRabbitListener {
public void simpleQueueListener(String msg) {
System.out.println("simple.queue receive msg: " + msg);
}
}
常见消息模型(续)
Work Queue
主要是为了提高消息处理的速度,避免消息堆积,加入队列中一共50条消息,两个消费者各处理25条,比单个消费者处理50条消息速度要快。
实现直接在消费者服务中定义两个消费者即可
对于处理消息快的消费者,则可以多分担一点,但是rabbit中有个默认的机制:消息预取,当消息到达队列中时,消费者所属的通道就会拿一个过来排着,所以如果不进行配置,则不同处理速度的消费者所处理的消息数量是一样的。我们可以在application.yml中进行如下配置,就不会出现消息堆积。
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # The number of messages that can be processed at the same time
Publisher/Subscribe
发布订阅模式允许将同一消息发送给多个消费者,实现方式是加入了交换机
exchange负责消息路由,而不是存储,路由失败则消息丢失。常见的exchange类型包括
- fanout:广播,将接收到的消息路由到每一个跟其绑定的queue
- Direct:路由,将接受的消息根据规则路由到指定的Queue。每一个Queue都与Exchange设置一个BindingKey,发布者发送消息时指定一个RoutingKey,Exchange将消息路由到RoutingKey与BindingKey相等的Queue
- Topic:话题
fanoutExchange
在consumer服务创建一个配置类,添加@configuration注解,并声明FanoutExchange,Queue和绑定关系对象Binding,代码如下
public class FanoutConfig {
// 声明交换机
public FanoutExchange fanoutExchange() {
return new FanoutExchange("fanout.exchange");
}
// 声明队列
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
// 绑定队列到交换机
public Binding fanoutBinding1() {
return BindingBuilder.
bind(fanoutQueue1()).
to(fanoutExchange());
// return new Binding("fanout.queue1", Binding.DestinationType.QUEUE, "fanout.exchange", "", null);
}
// 声明队列
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
// 绑定队列到交换机
public Binding fanoutBinding2() {
return BindingBuilder.
bind(fanoutQueue2()).
to(fanoutExchange());
}
}在consumer中监听
public class SpringRabbitListener {
public void simpleQueueListener(String msg) {
System.out.println("fanout.queue receive msg: " + msg);
}
public void simpleQueue2Listener(String msg) {
System.out.println("fanout.queue2 receive msg: " + msg);
}
}publisher中发送消息到交换机
public void testSendFanoutExchange() {
String exchangeName = "fanout.exchange";
String message = "hello fanout exchange";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
DirectExchange
在消息监听类中利用@RabbitListener声明Exchange,Queue,RoutingKey(使用注解方式声明,是上面的使用Bean声明的简化版本,所以之前的单独的队列,也可以使用Bean进行声明)
public class SpringRabbitListener {
public void listenerDirectQueue1(String msg) {
System.out.println("direct.queue1 receive msg: " + msg);
}
public void listenerDirectQueue2(String msg) {
System.out.println("direct.queue2 receive msg: " + msg);
}
}publisher
public void testSendDirectExchange() {
String exchangeName = "direct.exchange";
String message = "hello direct exchange";
rabbitTemplate.convertAndSend(exchangeName, "red", message); //red为指定的RoutingKey
}
TopicExchange,和directKey非常相似,区别在于routingKey必须是多个单词的列表,并以点分割,例如beijing.food,beijing.people,shanxi.food,shanxi.people,在指定bindingKey时,可以使用通配符,例如beijing.#,#.food
消息转换器
发送Map,List数据时,底层会将数据序列化,导致数据安全性和速度受到影响,所以,我们需要消息转换器,将其转换为json数据进行传输
在父工程中引入依赖(publisher服务和consumer服务都需要)
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>在Publisher服务和Consumer服务的启动类中添加以下代码
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}之后只需要发布者发送什么类型,接受者在接收时声明对应类型即可。
ElasticSearch
Elasticsearch
是一个分布式的、基于**RESTful API
** 的搜索和分析引擎与
Mysql
中名词对应关系索引 (
Index
) —–database
类型(
Type
) (新版本已经废弃,之后的文档都是直接存在索引下的) ——Table
文档(
Document
) ——Table
中一条条的数据属性 —– 某一列的属性
倒排索引机制: 用来快速查找包含某个词的所有文档.
- 按词汇进行索引,快速查找包含该词的所有文档。
- 优点是对搜索效率极高,尤其是当我们查询某个词或一组词时,能够快速定位所有相关文档。
- 缺点是需要额外的空间存储索引表。
倒排索引使用: 有4条文档, 1–小米手机, 2–华为手机, 3– 小米手环, 4–华为小米充电器. 倒排索引在存储时,会将其拆分成词条进行如下存储, 当搜索华为手机时,会命中两个词条, 华为和手机, 对应1,2,4三个文档ID, 文档命中数多的排在前面. 命中数少的排在后面.
表头: 词条(Term) 文档ID
小米 1,3,4
手机 1,2
华为 2,4
手环 3
充电器 4_cat
- GET
/_cat/nodes
:查看所有节点 - GET
/_cat/health
: 查看es健康状况 - GET
/_cat/master
: 查看主节点的信息 - GET
/_cat/indices
: 查看所有索引, 相当于show databases
- GET
保存一个文档(相当于添加一条数据):
PUT
/customer/external/1
:第一次发送是新建操作, 再次使用同一ID发送就变为更新操作, PUT方法必须指定ID, 一般用来更新操作.POST
/customer/external/1
: 不指定ID会自动生成唯一ID, 指定ID就会修改ID对应的这条数据. 一般用来新增操作// 在customer索引下的external类型下保存一号数据如下
{
"name":"jone"
}
查询数据
- GET
/customer/external/1
: 查询 customer索引下的external类型下的1号数据
- GET
更新数据
POST
/customer/external/1
/_update : 带_update
,的请求会对比原来的数据, 与原来一样, 数据不会更新,version
,seq_no
也不会变{
// post使用_update, 要更新的数据必须在doc下
"doc": {
name: "sam"
}
}POST/PUT
/customer/external/1
: 不带update, 每次发送请求都会执行更新操作, ,version
,seq_no
都会更新
删除文档&索引
- DELETE /
customer/external/1
: - DELETE
/customer
- DELETE /
bulk批量API: 批量导入数据
POST
/customer/external/_bulk
// 两行为一组, 第一行为需要执行的操作.第二行为请求的数据,删除操作时只有一行,指定ID即可
{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"Jane Doe"}语法格式
POST /索引/类型/_bulk
{action:{metadata}}
{requestBody}
{action:{metadata}}
{requestBody}