SpringCloud详解
1、SpringCloud分布式
1.1 微服务
1.1.1 什么是微服务
简单来说,微服务就是一种将一个单一应用程序拆分为一组小型服务的方法,拆分完成后,每一个服务 都运行在独立的进程中,服务于服务之间采用轻量级的通信机制来进行沟通(Spring Cloud 中采用基于 HTTP 的 RESTful API)。
每一个服务,都是围绕具体的业务进行构建,例如一个电商系统,订单服务、支付服务、物流服务、会 员服务等等,这些拆分后的应用都是独立的应用,都可以独立的部署到生产环境中。就是在采用微服务 之后,我们的项目不再拘泥于一种语言,可以 Java、Go、Python、PHP 等等,混合使用,这在传统的 应用开发中,是无法想象的。而使用了微服务之后,我们可以根据业务上下文来选择合适的语言和构建 工具进行构建。
微服务可以理解为是 SOA 的一个传承,一个本质的区别是微服务是一个真正分布式、去中心化的,微服务的拆分比 SOA 更加彻底。
SOA: 把系统按照实际业务,拆分成刚刚好大小的、合适的、独立部署的模块,每个模块之间相互独立。
1.1.2 微服务的优势
复杂度可控
独立部署
技术选型灵活
较好的容错性
较强的可扩展性
1.1.3 使用SpringCloud的优势
Spring Cloud 可以理解为微服务这种思想在 Java 领域的一个具体落地。Spring Cloud 在发展之初,就 借鉴了微服务的思想,同时结合 Spring Boot,Spring Cloud 提供了组件的一键式启动和部署的能力, 极大的简化了微服务架构的落地。
Spring Cloud 这种框架,从设计之初,就充分考虑了分布式架构演化所需要的功能,例如服务注册、配 置中心、消息总线以及负载均衡等。这些功能都是以可插拔的形式提供出来的,这样,在分布式系统不 断演化的过程中,我们的 Spring Cloud 也可以非常方便的进化。
2.1 SpringCloud简介
2.1.1 什么是SpringCloud
Spring Cloud 是一系列框架的集合,Spring Cloud 内部包含了许多框架,这些框架互相协作,共同来 构建分布式系统。利用这些组件,可以非常方便的构建一个分布式系统。
2.1.2 核心特性
- 服务注册与发现
- 负载均衡
- 服务之间调用
- 容错、服务降级、断路器
- 消息总线
- 分布式配置中心
- 链路器
2.1.3 版本介绍
不同于其他的框架,Spring Cloud 版本名称是通过 A(Angel)、B(Brixton)、C(Camden)、 D(Dalston)、E(Edgware)、F(Finchley)... 这样来明明的,这些名字使用了伦敦地铁站的名 字,目前最新版是 H (Hoxton)版。
Spring Cloud 中,除了大的版本之外,还有一些小版本,小版本命名方式如下:
- M ,M 版是 milestone 的缩写,所以我们会看到一些版本叫 M1、M2
- RC,RC 是 Release Candidate,表示该项目处于候选状态,这是正式发版之前的一个状态,所以 我们会看到 RC1、RC2
- SR,SR 是 Service Release ,表示项目正式发布的稳定版,其实相当于 GA(Generally Available) 版。所以,我们会看到 SR1、SR2
- SNAPSHOT,这个表示快照版
3.1 SpringCloud体系
3.1.1 SpringCloud包含的组件
- Spring Cloud Netflix,这个组件,在 Spring Cloud 成立之初,立下了汗马功劳。但是, 2018 年 的断更,也是 Netflix 掉链子了。
- Spring Cloud Config,分布式配置中心,利用 Git/Svn 来集中管理项目的配置文件
- Spring Cloud Bus,消息总线,可以构建消息驱动的微服务,也可以用来做一些状态管理等
- Spring Cloud Consul,服务注册发现
- Spring Cloud Stream,基于 Redis、RabbitMQ、Kafka 实现的消息微服务
- Spring Cloud OpenFeign,提供 OpenFeign 集成到 Spring Boot 应用中的方式,主要解决微服务 之间的调用问题
- Spring Cloud Gateway,Spring Cloud 官方推出的网关服务
- Spring Cloud Cloudfoundry,利用 Cloudfoundry 集成我们的应用程序
- Spring Cloud Security,在 Zuul 代理中,为 OAuth2 客户端认证提供支持
- Spring Cloud AWS ,快速集成亚马逊云服务
- Spring Cloud Contract,一个消费者驱动的、面向 Java 的契约框架
- Spring Cloud Zookeeper,基于 Apache Zookeeper 的服务注册和发现
- Spring Cloud Data Flow,在一个结构化的平台上,组成数据微服务
- Spring Cloud Kubernetes,Spring Cloud 提供的针对 Kubernetes 的支持
- Spring Cloud Function
- Spring Cloud Task,短生命周期的微服务
3.1.2 SpringCloud与SpringBoot对应的版本关系
2、服务注册中心 Eureka
2.1 注册中心
Eureka 是 Spring Cloud 中的注册中心,类似于 Dubbo 中的 Zookeeper。那么到底什么是注册中心, 我们为什么需要注册中心?
我们首先来看一个传统的单体应用:
在单体应用中,所有的业务都集中在一个项目中,当用户从浏览器发起请求时,直接由前端发起请求给 后端,后端调用业务逻辑,给前端请求做出响应,完成一次调用。整个调用过程是一条直线,不需要服 务之间的中转,所以没有必要引入注册中心。
随着公司项目越来越大,我们会将系统进行拆分,例如一个电商项目,可以拆分为订单模块、物流模 块、支付模块、CMS 模块等等。这样,当用户发起请求时,就需要各个模块之间进行协作,这样不可避免的要进行模块之间的调用。此时,我们的系统架构就会发生变化:
在这里,大家可以看到,模块之间的调用,变得越来越复杂,而且模块之间还存在强耦合。例如 A 调用B,那么就要在 A 中写上 B 的地址,也意味着 B 的部署位置要固定,同时,如果以后 B 要进行集群化部署,A 也需要修改。
为了解决服务之间的耦合,注册中心闪亮登场。
2.2 什么是Eureka
Eureka 是 Netflix 公司提供的一款服务注册中心,Eureka 基于 REST 来实现服务的注册与发现,曾经,
Eureka 是 Spring Cloud 中最重要的核心组件之一。Spring Cloud 中封装了 Eureka,在 Eureka 的基础上,优化了一些配置,然后提供了可视化的页面,可以方便的查看服务的注册情况以及服务注册中心 集群的运行情况。
Eureka 由两部分:服务端和客户端,服务端就是注册中心,用来接收其他服务的注册,客户端则是一个 Java 客户端,用来注册,并可以实现负载均衡等功能。
从图中,我们可以看出,Eureka 中,有三个角色:
- Eureka Server:注册中心
- Eureka Provider:服务提供者
- Eureka Consumer:服务消费者
2.3 Eureka的基本搭建
Eureka 本身是使用 Java 来开发的,Spring Cloud 使用 Spring Boot 技术对 Eureka 进行了封装,所以,在 Spring Cloud 中使用 Eureka 非常方便,只需要引入 spring-cloud-starter-netflix-eureka-
server 这个依赖即可,然后就像启动一个普通的 Spring Boot 项目一样启动 Eureka 即可。
创建一个普通的 Spring Boot 项目,创建时,添加 Eureka 依赖:
- 标记该项目是一个Eureka注册中心
//标记该该项目为注册中心
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
- 配置Eureka注册中心基本信息
spring:
application:
# 给当前服务起一个名称
name: eureka
server:
# 设置该项目启动端口号
port: 1111
eureka:
client:
# 默认情况下,Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,它会有两层身份:1、注册中心;2、普通服务,即当前服务会自己把自己注册到自己上面来
# 设置为false,表示当前项目不要注册到注册中心上
register-with-eureka: false
# 表示是否Eureka Server上获取注册信息
fetch-registry: false
- 启动项目
- 如 果 在 项 目 启 动 时 , 遇 到
java.lang.TypeNotPresentException: Type javax.xml.bind.JAXBContext not present
异常,这是因为 JDK9 以上,移除了 JAXB,这个时候, 只需要我们手动引入 JAXB 即可。
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
- 注册中心访问
2.4 Eureka注册中心集群的搭建
使用了注册中心之后,所有的服务都要通过服务注册中心来进行信息交换。服务注册中心的稳定性就非 常重要了,一旦服务注册中心掉线,会影响到整个系统的稳定性。所以,在实际开发中,Eureka 一般都是以集群的形式出现的。
搭建 Eureka 集群,首先我们需要一点准备工作,修改电脑的 hosts 文件
- 修改本机的hosts文件
C:\Windows\System32\drivers\etc
在第三项Eureka的基础搭建项目上新增两个配置文件
application-a.yml
spring: application: # 给当前服务起一个名称 name: eureka server: # 设置该项目启动端口号 port: 1111 eureka: instance: # eureka主机的名称 和host文件对应 hostname: eurekaA client: # 默认情况下,Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,它会有两层身份:1、注册中心;2、普通服务,即当前服务会自己把自己注册到自己上面来 # 设置为false,表示当前项目不要注册到注册中心上 register-with-eureka: true # 表示是否Eureka Server上获取注册信息 fetch-registry: true # eurekaA服务注册到eurekaB服务上去 service-url: defaultZone: http://eurekaB:1112/eureka
application-b.yml
spring: application: # 给当前服务起一个名称 name: eureka server: # 设置该项目启动端口号 port: 1112 eureka: instance: # eureka主机的名称 和host文件对应 hostname: eurekaB client: # 默认情况下,Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,它会有两层身份:1、注册中心;2、普通服务,即当前服务会自己把自己注册到自己上面来 # 设置为false,表示当前项目不要注册到注册中心上 register-with-eureka: true # 表示是否Eureka Server上获取注册信息 fetch-registry: true # eurekaB服务注册到eurekaA服务上去 service-url: defaultZone: http://eurekaA:1111/eureka
maven打包package此项目
- 分别使用不同的配置文件去运行两个eureka程序
java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=a
java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=b
启动成功后,就可以看到,两个服务之间互相注册,共同给组成一个集群.
- eurekaA项目访问
- eurekaB项目访问
2.5 Eureka 工作细节
Eureka 本身可以分为两大部分,Eureka Server 和 Eureka Client
2.5.1 Eureka Server
Eureka Server 主要对外提供了三个功能:
服务注册,所有的服务都注册到 Eureka Server 上面来
提供注册表,注册表就是所有注册上来服务的一个列表,Eureka Client 在调用服务时,需要获取这个注册表,一般来说,这个注册表会缓存下来,如果缓存失效,则直接获取最新的注册表
同步状态,Eureka Client 通过注册、心跳等机制,和 Eureka Server 同步当前客户端的状态
2.5.2 Eureka Client
Eureka Client 主要是用来简化每一个服务和 Eureka Server 之间的交互。Eureka Client 会自动拉取、更新以及缓存 Eureka Server 中的信息,这样,即使 Eureka Server 所有节点都宕机,Eureka Client 依然能够获取到想要调用服务的地址(但是地址可能不准确)。
2.5.2.1 服务注册
服务提供者将自己注册到服务注册中心(Eureka Server),需要注意,所谓的服务提供者,只是一个业务上上的划分,本质上他就是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,他需要提供自身的一些元数据信息,例如 IP 地址、端口、名称、运行状态等等。
2.5.2.2 服务续约
Eureka Client 注册到 Eureka Server 上之后,事情没有结束,刚刚开始而已。注册成功后,默认情况下,Eureka CLient 每隔 30 秒就要向 Eureka Server 发送一条心跳消息,来告诉 Eureka Server 我还在运行。如果 Eureka Server 连续 90 秒都有没有收到 Eureka Client 的续约消息(连续三次没发送),它会认为 Eureka Client 已经掉线了,会将掉线的 Eureka Client 从当前的服务注册列表中剔除。
服务续约,有两个相关的属性(一般不建议修改):
#服务的续约时间,默认是 30 秒
eureka.instance.lease-renewal-interval-in-seconds=30
#服务失效时间,默认是 90 秒
eureka.instance.lease-expiration-duration-in-seconds=90
2.5.2.3 服务下线
当 Eureka Client 下线时,它会主动发送一条消息,告诉 Eureka Server ,我下线啦。
2.5.2.4 获取注册表信息
Eureka Client 从 Eureka Server 上获取服务的注册信息,并将其缓存在本地。本地客户端,在需要调用远程服务时,会从该信息中查找远程服务所对应的 IP 地址、端口等信息。Eureka Client 上缓存的服务注册信息会定期更新(30 秒),如果 Eureka Server 返回的注册表信息与本地缓存的注册表信息不同的话,Eureka Client 会自动处理。
这里,也涉及到两个属性,一个是是否允许获取注册表信息:
#是否允许获取注册表信息
eureka.client.fetch-registry=true
Eureka Client 上缓存的服务注册信息,定期更新的时间间隔,默认 30 秒:
#缓存的服务注册信息,定期更新的时间间隔
eureka.client.registry-fetch-interval-seconds=30
2.6 Eureka 集群原理
我们来看官方的一张 Eureka 集群架构图:
在这个集群架构中,Eureka Server 之间通过 Replicate 进行数据同步,不同的 Eureka Server 之间不区分主从节点,所有节点都是平等的。节点之间,通过置顶 serviceUrl 来互相注册,形成一个集群,进而提高节点的可用性。
在 Eureka Server 集群中,如果有某一个节点宕机,Eureka Client 会自动切换到新的 Eureka Server 上。每一个 Eureka Server 节点,都会互相同步数据。Eureka Server 的连接方式,可以是单线的,就是 A-->b-->C ,此时,A 的数据也会和 C 之间互相同步。但是一般不建议这种写法,
在我们配置serviceUrl 时,可以指定多个注册地址,即 A 可以即注册到 B 上,也可以同时注册到 C 上。
Eureka 分区:
region:地理上的不同区域
zone:具体的机房
3、服务注册与发现 Eureka
3.1 服务注册
服务注册就是把一个微服务注册到 Eureka Server 上,这样,当其他服务需要调用该服务时,只需要从 Eureka Server 上查询该服务的信息即可。
我们之前的Eureka Server
注册中心的配置
spring:
application:
# 给当前服务起一个名称
name: eureka
server:
# 设置该项目启动端口号
port: 1111
eureka:
client:
# 默认情况下,Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,它会有两层身份:1、注册中心;2、普通服务,即当前服务会自己把自己注册到自己上面来
# 设置为false,表示当前项目不要注册到注册中心上
register-with-eureka: false
# 表示是否从Eureka Server上获取注册信息
fetch-registry: false
# 因为eureka本身也是相当于一个客户端,所以这里配置eureka的服务端的地址
service-url:
defaultZone: http://localhost:1111/eureka
这里我们创建一个 provider,作为我们的服务提供者。
服务提供者
Eureka Client
注意: 这里要选择
web
依赖,不然会启动失败
#当前服务名称
spring.application.name=provider
#端口号
server.port=1113
#将该服务的client注册到 http://localhost:1111/eureka地址的server上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
首先启动 Eureka Server
再启动 Eureka Client
。
浏览器输入 http://localhost:1111,就可以查看 Eureka Client
的注册信息:
3.2 服务消费
- 这里我们创建两个provider做服务提供者(
集群部署
),一个consumer服务去消费
首先在 provider 中提供一个接口,然后创建一个新的 consumer 项目,消费这个接口。在 provider 中,提供一个 hello 接口,如下:
@RestController
public class HelloController {
//获取配置文件中的端口号
@Value("${server.port}")
int port;
@GetMapping("/hello")
public String sayHello() {
return "hello eureka:"+port;
}
}
分别启动两个 provider 服务,端口分别为1113和1115
java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1113 java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1115
接下来,创建一个 consumer 项目,consumer 项目中,去消费 provider 提供的接口。consumer 要能够获取到 provider 这个接口的地址,他就需要去 Eureka Server 中查询
创建完成后,我们首先也在 application.properties
中配置一下注册信息:
#服务名称
spring.application.name=consumer
#服务端口
server.port=1114
#将该服务注册到 http://localhost:1111/eureka地址的server上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
利用了 HttpUrlConnection 来发起的请求,使用Eureka Client 提供的 DiscoveryClient 工具,利用这个工具,我们可以根据服务名从 Eureka Server 上查询到更具服务名称获取到的该服务的详细信息
注意,DiscoveryClient 查询到的服务列表是一个集合,因为服务在部署的过程中,可能是集群化部署,集合中的每一项就是一个实例。
@RestController
public class ConsumerController {
//获取服务发现客户端的调用类
@Autowired
private DiscoveryClient discoveryClient;
//手动构建负载均衡
int count=0;
@GetMapping("/hello")
public String getHello() {
//根据【服务名称】获取服务实例,因为集群的时候配置 服务名称相同,所以获取的是一个list集合,每个集合中的元素代表一个client服务实例
//这里我们启动两个 provider 构建集群,所以获取到的实例为两个
List<ServiceInstance> provider = discoveryClient.getInstances("provider");
//因为这里的provider为的实例为两个,所以每次请求都用count对2取余,即每次请求得到0,1,0,1.... 为手动实现的线性负载均衡
ServiceInstance serviceInstance = provider.get((count++)%provider.size());
//获取服务的主机地址
String host = serviceInstance.getHost();
//获取服务的端口
int port = serviceInstance.getPort();
//动态拼接url
StringBuffer sb=new StringBuffer();
sb.append("http://")
.append(host)
.append(":" + port)
.append("/hello");
HttpURLConnection urlConnection=null;
try {
URL url =new URL(sb.toString());
urlConnection = (HttpURLConnection) url.openConnection();
if(urlConnection.getResponseCode()==200){
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String s = bufferedReader.readLine();
bufferedReader.close();
return s;
}
}catch (Exception e){
e.printStackTrace();
}
return "error";
}
}
启动之后我们访问注册中心地址:http://locahost:1111
我们发现两个服务提供者provider
和一个服务消费者consumer
已经注册到注册中心上
这时我们访问consumer
服务消费者hello接口, http://localhost:1114/hello
返回的结果: hello eureka:1113
交替出现
返回的结果: hello eureka:1115
3.3 改进请求调用和负载均衡
Http 调用,我们使用 Spring 提供的
RestTemplate
来实现。使用
Ribbon(@LoadBalanced)
来快速实现负载均衡
在consumer
服务中加入以下配置:
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
//注册RestTemplate
@Bean
//负载均衡
@LoadBalanced
RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello1")
public String getHello1() {
//在这里,我们只需要传入一个【服务名称】作为主机名即可 http://服务名称/hello
return restTemplate.getForObject("http://provider/hello", String.class);
}
}
即访问http://localhost:1114/hello1 就能实现url的请求,并实现负载均衡。
3.4 RestTemplate的使用
3.4.1 RestTemplate-GET
在provider
服务中添加如下请求
@GetMapping("/hello1")
public String sayhello1(String name) {
return "hello:"+name;
}
接下来,我们在 consumer
去访问这个接口,这个接口是一个 GET 请求,所以,访问方式,就是调用
RestTemplate 中的 GET 请求。
可以看到,在 RestTemplate 中,关于 GET 请求,一共有如下两大类方法:
这两大类方法实际上是重载的,唯一不同的,就是返回值类型。
getForObject 返回的是一个对象,这个对象就是服务端返回的具体值。
getForEntity 返回的是一个ResponseEntity,这个ResponseEntity 中除了服务端返回的具体数据外,还保留了 Http 响应头的数据。
@GetMapping("/hello2")
public void getHello2() {
String zhangsan = restTemplate.getForObject("http://provider/hello1?name={1}", String.class, "张三");
System.out.println(zhangsan);
ResponseEntity<String> lisi = restTemplate.getForEntity("http://provider/hello1?name={1}", String.class, "lisi");
String body = lisi.getBody();
System.out.println("body:" + body);
HttpStatus httpStatus=lisi.getStatusCode();
System.out.println("httpStatus:"+httpStatus);
int statusCodeValue = lisi.getStatusCodeValue();
System.out.println("statusCodeValue:"+statusCodeValue);
HttpHeaders headers = lisi.getHeaders();
Set<String> strings = headers.keySet();
System.out.println("--------header---------");
strings.forEach(s->{
System.out.println(s+":"+headers.get(s));
});
}
hello:张三
body:hello:lisi
httpStatus:200 OK
statusCodeValue:200
--------header---------
Content-Type:[text/plain;charset=UTF-8]
Content-Length:[10]
Date:[Fri, 22 Jan 2021 14:43:03 GMT]
Keep-Alive:[timeout=60]
Connection:[keep-alive]
不同的传参方式:
getForObject 和 getForEntity 分别有三个重载方法,两者的三个重载方法基本都是一致的。所以,这里,我们主要看其中一种。三个重载方法, 其实代表了三种不同的传参方式。
@GetMapping("/hello3")
public void getHello3() throws UnsupportedEncodingException {
//通过占位符传参
String r1 = restTemplate.getForObject("http://provider/hello1?name={1}", String.class, "张三");
System.out.println(r1);
//通过map传参
Map<String, Object> map = new HashMap<>();
map.put("name","李四");
String r2 = restTemplate.getForObject("http://provider/hello1?name={name}", String.class,map);
System.out.println(r2);
//参数在url中,若为中文需要转码
String url = "http://provider/hello1?name=" + URLEncoder.encode("张三", "UTF-8");
URI uri = URI.create(url);
String r3 = restTemplate.getForObject(uri, String.class);
System.out.println(r3);
}
hello:张三
hello:李四
hello:王五
3.4.2 RestTemplate-POST
首先在 provider 中提供两个 POST 接口,同时,因为 POST 请求可能需要传递 JSON,所以,这里我们创建一个普通的 Maven 项目作为 common_entity 模块,然后这个 commons 模块被 provider 和consumer 共同引用,这样我们就可以方便的传递 JSON 了。
commons 模块创建成功后,首先在 commons 模块中添加 User 对象,然后该模块分别被 provider 和consumer 引用。
然后,我们在 provider 中,提供和两个 POST 接口:
@PostMapping("/user1")
public User addUser1(User user) {
return user;
}
@PostMapping("/user2")
public User addualUser2(@RequestBody User user) {
return user;
}
这里定义了两个 User 添加的方法,两个方法代表了两种不同的传参方式。
第一种方法是以 key/value
形式来传参
第二种方法是以 JSON
形式来传参。
定义完成后,接下来,我们在 consumer 中调用这两个 POST 接口。
@GetMapping("/hello4")
public void getHello4() {
//1、以key-value形式传输
MultiValueMap<String, Object> map=new LinkedMultiValueMap<>();
map.add("username","张三");
map.add("password","123456");
map.add("age", 12);
User user = restTemplate.postForObject("http://provider/user1", map, User.class);
System.out.println(user);
//2、以json形式传输
User user1=new User();
user1.setUsername("李四");
user1.setPassword("111");
user1.setAge(11);
User user2 = restTemplate.postForObject("http://provider/user2", user1, User.class);
System.out.println(user2);
}
User{username='张三', password='123456', age=12}
User{username='李四', password='111', age=11}
post 参数到底是 key/value 形式还是 json 形式,主要看第二个参数,如果第二个参数是MultiValueMap ,则参数是以 key/value 形式来传递的,如果是一个普通对象,则参数是以 json 形式来传递的。
最后再看看一下 postForLocation 。有的时候,当我执行完一个 post 请求之后,立马要进行重定向, 一个非常常见的场景就是注册,注册是一个 post 请求,注册完成之后,立马重定向到登录页面去登录。对于这种场景,我们就可以使用 postForLocation
。
首先我们在 provider 上提供一个用户注册接口:
@Controller
public class RegisterController {
@PostMapping("/register")
public String register(User user) {
return "redirect:http://provider/loginPage?username="+user.getUsername();
}
@GetMapping("/loginPage")
@ResponseBody
public String loginPage(String username) {
return "loginpage:"+username;
}
}
注意,这里的 post 接口,响应一定是 302
,否则 postForLocation
无效。
注意,重定向的地址,一定要写成绝对路径,不要写相对路径,否则在 consumer 中调用时会出问题
在consumer接口中消费:
@GetMapping("/hello5")
public void gethello5() {
MultiValueMap<String, Object> map=new LinkedMultiValueMap<>();
map.add("username","louchen");
map.add("password","123456");
map.add("age", 18);
//得到的回调地址
URI uri = restTemplate.postForLocation("http://provider/register", map);
//http://provider/loginPage?username=louchen
System.out.println(uri);
String forObject = restTemplate.getForObject(uri, String.class);
//loginpage:louchen
System.out.println(forObject);
}
这就是 postForLocation ,调用该方法返回的是一个 Uri,这个 Uri 就是重定向的地址(里边也包含了重定向的参数),拿到 Uri 之后,就可以直接发送新的请求了。
3.4.3 RestTemplate-PUT
PUT 请求比较简单,重载的方法也比较少。
我们首先在 provider 中提供两个PUT 接口:
@PutMapping("/user1")
public void updateUser1(User user) {
System.out.println(user);
}
@PutMapping("/user2")
public void updateUser2(@RequestBody User user) {
System.out.println(user);
}
@GetMapping("/hello6")
public void hello6() {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("username", "louchen1");
map.add("password", "1234561");
map.add("age", 19);
//以key/value传值 无返回值
restTemplate.put("http://provider/user1",map);
User user1 = new User();
user1.setUsername("李四1");
user1.setPassword("1112");
user1.setAge(12);
//以json传值 无返回值
restTemplate.put("http://provider/user2",user1);
}
注意,PUT 接口传参其实和 POST 很像,也接受两种类型的参数,key/value 形式以及 JSON 形式。
3.4.4 RestTemplate-Delete
RestTemplate中的delete方法不能传入json参数,即被@RequestBody的参数
DELETE 也比较容易,我们有两种方式来传递参数,key/value 形式或者 PathVariable(参数放在路径中),首先我们在 provider 中定义两个 DELETE 方法:
@DeleteMapping("/user1")
public void deleteUser1(@RequestParam("ids") Integer[] ids){
System.out.println(Arrays.toString(ids));
}
@DeleteMapping("/user2/{id}")
public void deleteUser2(@PathVariable Integer id){
System.out.println(id);
}
然后在 consumer 中调用这两个删除的接口:
@GetMapping("/hello7")
public void hello7() {
restTemplate.delete("http://provider/user1?ids={1}","1,2,3");
restTemplate.delete("http://provider/user1?ids={1}&ids={2}",20,30);
restTemplate.delete("http://provider/user2/{1}",10);
}
3.5 客户端负载均衡
客户端负载均衡就是相对服务端负载均衡而言的。
服务端负载均衡,就是传统的 Nginx 的方式,用 Nginx 做负载均衡,我们称之为服务端负载均衡:
这种负载均衡,我们称之为服务端负载均衡,它的一个特点是,就是调用的客户端并不知道具体是哪一 个 Server 提供的服务,它也不关心,反正请求发送给 Nginx,Nginx 再将请求转发给 Tomcat,客户端只需要记着 Nginx 的地址即可。
这种负载均衡,我们称之为服务端负载均衡,它的一个特点是,就是调用的客户端并不知道具体是哪一 个 Server 提供的服务,它也不关心,反正请求发送给 Nginx,Nginx 再将请求转发给 Tomcat,客户端只需要记着 Nginx 的地址即可。
客户端负载均衡则是另外一种情形:
客户端负载均衡,就是调用的客户端本身是知道所有 Server 的详细信息的,当需要调用 Server 上的接口的时候,客户端从自身所维护的 Server 列表中,根据提前配置好的负载均衡策略,自己挑选一个Server 来调用,此时,客户端知道它所调用的是哪一个 Server。
在 RestTemplate 中,要想使用负载均衡功能,只需要给 RestTemplate 实例上添加一个@LoadBalanced
注解即可,此时,RestTemplate 就会自动具备负载均衡功能,这个负载均衡就是客户端负载均衡。
3.6 负载均衡原理
在 Spring Cloud 中,实现负载均衡非常容易,只需要添加@LoadBalanced
注解即可。只要添加了该注解,一个原本普普通通做 Rest 请求的工具 RestTemplate 就会自动具备负载均衡功能,这个是怎么实现的呢?
整体上来说,这个功能的实现就是三个核心点:
从 Eureka Client 本地缓存的服务注册信息中,选择一个可以调用的服务
根据 1 中所选择的服务,重构请求 URL 地址
将 1、2 步的功能嵌入到 RestTemplate 中
4、Consul
在 Spring Cloud 中,大部分组件都有备选方案,例如注册中心,除了常见 Eureka 之外,像
zookeeper 我们也可以直接使用在 Spring Cloud 中,还有另外一个比较重要的方案,就是 Consul。
Consul 是 HashiCorp 公司推出来的开源产品。主要提供了:服务发现、服务隔离、服务配置等功能。
相比于 Eureka 和 zookeeper,Consul 配置更加一站式,因为它内置了很多微服务常见的需求:服务发现与注册、分布式一致性协议实现、健康检查、键值对存储、多数据中心等,我们不再需要借助第三 方组件来实现这些功能。
4.1 安装
不同于 Eureka ,Consul 使用 Go 语言开发,所以,使用 Consul ,我们需要先安装软件。在 Linux 中,首先执行如下命令下载 Consul:
wget https://releases.hashicorp.com/consul/1.6.2/consul_1.6.2_linux_amd64.zip
或者去官网 https://www.consul.io/ 下载后上传到解压服务器
这里我们通过去官网下载consul然后通过ftp上传到服务器后解压 unzip consul_1.9.2_linux_amd64.zip
解压完成后,我们在当前目录下就可以看到 consul 文件,然后执行如下命令,启动 Consul
:
./consul agent -dev -ui -node=consul-dev -client=172.16.53.61
注意:这里的
-clients
地址必须为私有地址,不能为公网地址。而我们是通过公网地址访问的consul界面的需要开发
8500端口
4.2 Consul单节点使用
简单看一个注册消费的案例。
首先我们来创建一个服务提供者。就是一个普通的 Spring Boot 项目,添加如下依赖:
项目创建成功后,添加如下配置:
#应用程序名称
spring.application.name=consul-provider
server.port=2000
#consul相关配置 ui界面客户端访问的地址
spring.cloud.consul.host=47.96.141.44
spring.cloud.consul.port=8500
#consul-provider服务名称
spring.cloud.consul.discovery.service-name=consul-provider
#自定义固定ip
spring.cloud.consul.discovery.prefer-ip-address=true
#consule-provider服务的ip地址,远程的则为内网ip
#注意这里:因为要将本地的服务注册到远程的Consul服务器上,所以不能使用本地ip,需要将本地项目打包到远程服务器上,并在这配置远程服务器的内网ip
spring.cloud.consul.discovery.ip-address=172.16.53.61
在项目启动类上开启服务发现的功能:
@SpringBootApplication
@EnableDiscoveryClient
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
最后添加一个测试接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "hello";
}
}
这里因为我们的Consle服务器在阿里云,所以需要打包本地项目到远程服务器上启动
访问 Consul界面:http://47.96.141.44:8500/ui/dc1/services
4.3 Consul集群注册
为了区分集群中的哪一个 provider 提供的服务,我们修改一下 provider 中的接口:
@RestController
public class HelloController {
@Value("${server.port}")
Integer port;
@GetMapping("/hello")
public String sayHello() {
return "hello:" + port;
}
}
修改完成后,对项目进行打包。打包成功后,命令行执行如下两行命令,启动两个 provider
实例:
java -jar demo-0.0.1-SNAPSHOT.jar --server.port=2000
java -jar demo-0.0.1-SNAPSHOT.jar --server.port=2001
启动成功后,再去 consul 后台管理页面,就可以看到有两个实例了:
4.4 消费
首先创建一个消费实例,创建方式和 provider 一致。
创建成功后,添加如下配置:
# 应用名称
spring.application.name=consul-consumer
server.port=2002
#consul相关配置 ui界面客户端访问的地址
spring.cloud.consul.host=47.96.141.44
spring.cloud.consul.port=8500
#consul-provider服务名称
spring.cloud.consul.discovery.service-name=consul-consumer
#自定义固定ip
spring.cloud.consul.discovery.prefer-ip-address=true
#consule-provider服务的ip地址,远程的则为内网ip
spring.cloud.consul.discovery.ip-address=172.16.53.61
开启服务发现,并添加 RestTemplate:
@SpringBootApplication
//开启服务发现
@EnableDiscoveryClient
public class ConsulConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsulConsumerApplication.class, args);
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
提供一个服务调用的方法:
@RestController
public class HelloController {
//负载均衡
@Autowired
private LoadBalancerClient loadBalancerClient;
//服务调用
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public void sayHello() {
//根据服务名称获取调用服务的实例
ServiceInstance choose = loadBalancerClient.choose("consul-provider");
System.out.println("服务地址:"+choose.getUri());
System.out.println("服务名称:"+choose.getServiceId());
System.out.println("服务主机:"+choose.getHost());
String s = restTemplate.getForObject(choose.getUri() + "/hello", String.class);
System.out.println(s);
}
}
这里,我们通过 loadBalancerClient 实例,可以获取要调用的 ServiceInstance。获取到调用地址之后,再用 RestTemplate 去调用。
这里也需要将consul-consumer消费服务部署到远程服务器
然后,启动项目,访问消费服务地址,浏览器输入http://47.96.141.44:2002/hello ,查看请求结果,这个请求自带负载均衡功能。
通过查看日志,已经实现负载均衡功能
5、Hystrix
5.1 基本介绍
Hystrix 叫做断路器/熔断器。微服务系统中,整个系统出错的概率非常高,因为在微服务系统中,涉及到的模块太多了,每一个模块出错,都有可能导致整个服务出,当所有模块都稳定运行时,整个服务才 算是稳定运行。
我们希望当整个系统中,某一个模块无法正常工作时,能够通过我们提前配置的一些东西,来使得整个 系统正常运行,即单个模块出问题,不影响整个系统。
5.2 基本用法【注解】
首先创建一个新的 SpringBoot 模块,然后添加依赖:
项目创建成功后,添加如下配置,将 Hystrix 注册到 Eureka 上:
# 应用名称
spring.application.name=hystrix
# 应用服务 WEB 访问端口
server.port=3000
#将该服务注册到 http://localhost:1111/eureka地址的注册中心上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
然后,在项目启动类上添加如下注解,开启断路器,同时提供一个 RestTemplate
实例:
@SpringBootApplication
//开启断路器
@EnableCircuitBreaker
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@SpringCloudApplication
的注解信息如下:@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker public @interface SpringCloudApplication { }
所以启动类上的注解,也可以使用 @SpringCloudApplication
代替:
@SpringBootApplication
@SpringCloudApplication
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
这样,Hystrix 的配置就算完成了。接下来提供 Hystrix 的接口。
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;
/**
* 在这个方法中,使用restTemplate发起一次远程调用,去调用provider集群服务中的/hello接口
* 但是,在这个过程中可能会调用失败(例如调用该服务时,该服务挂掉)
* 在该方法添加的HystrixCommand注解上的fallbackMethod属性,表示当调用指定的服务接口失败的的时候,我们使用fallbackMethod指定的方法进行代替
* 这里的fallbackMethod的值为error,则表示使用error方法代替
* @return
*/
@HystrixCommand(fallbackMethod ="error")
public String sayHello() {
return restTemplate.getForObject("http://provider/hello", String.class);
}
/**
* 服务名必须和fallbackMethod中的指定的名称一致。返回值也要相同
* @return
*/
public String error() {
return "error";
}
}
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String sayhello() {
return helloService.sayHello();
}
}
要启动的服务:
Eureka注册中心 【1111端口】
Eureka集群提供者(2个服务) 【1112和1113端口】
Hystrix服务(当消费者) 【3000端口】
启动完毕后:
访问:http://localhost:1111/ 注册中心地址
通过访问:http://localhost:3000/hello 可以实现负载均衡效果
问题复现:当我们关掉一个1112端口的服务,马上再去请求 http://localhost:3000/hello
可以发现到负载均衡到1112端口的服务器上时,返回了被替换的方法内容 error,这样做的好处时,在集群的时候避免用户不因为一台服务器挂掉而出现的404错误。这也称之为容错/服务降级
。
当每隔一段时间后eureka注册中心重新同步其他服务信息,更新挂掉了的服务信息,以后就不会去请求已经挂掉的服务,也就不会进行以下的错误处理方式。
5.2.1 通过注解实现异步调用
在HelloService
类中新增一个方法:
//通过异步方式调用
@HystrixCommand(fallbackMethod ="error")
public Future<String> sayHello2() {
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://provider/hello", String.class);
}
};
}
在HelloController
类中新增一个方法:
@GetMapping("/hello3")
public void sayHello3() {
Future<String> stringFuture = helloService.sayHello2();
try {
String s = stringFuture.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
5.3 请求命令【继承】
请求命令就是以继承类的方式来替代前面的注解方式。
自定义一个 HelloCommand
:
//这里的HystrixCommand<T>泛型T为我们需要调用指定服务的返回值的类型
public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
protected HelloCommand(Setter setter, RestTemplate template) {
super(setter);
this.restTemplate=template;
}
//调用指定服务
@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://provider/hello", String.class);
}
//请求失败的回调---实现服务容错/降级
@Override
protected String getFallback() {
return "error-commands";
}
}
增加一个HelloController
中的一个请求调用方法:
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello2")
public void sayHello2() {
HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("louchen")), restTemplate);
//第一种方式:直接执行HystrixCommand中的run方法
String execute = helloCommand.execute();
System.out.println(execute);
HelloCommand helloCommand1 = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("louchen")), restTemplate);
try {
//第二种方式:放入队列执行
Future<String> queue = helloCommand1.queue();
String s = queue.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
- 注意:
- 一个HelloCommand实例只能执行一次
- 可以直接执行,也可以先入队,后执行
5.4 异常处理
就是当发起服务调用时,如果不是 provider 的原因导致请求调用失败,而是 consumer 中本身代码有问题导致的请求失败,即 consumer 中抛出了异常,这个时候,也会自动进行服务降级,只不过这个时候降级,我们还需要知道到底是哪里出异常了。
如下示例代码,如果 hello 方法中,执行时抛出异常,那么一样也会进行服务降级,进入到 error 方法中,在 error 方法中,我们可以获取到异常的详细信息:
- 注解方式:
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod ="error")
public String sayHello() {
//不是服务出现的调用异常,而是本身代码的异常
int j=1/0;
return restTemplate.getForObject("http://provider/hello", String.class);
}
//直接获取异常信息即可
public String error(Throwable t) {
return "error:"+t.getMessage();
}
}
- 继承方式
public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
protected HelloCommand(Setter setter, RestTemplate template) {
super(setter);
this.restTemplate=template;
}
@Override
protected String run() throws Exception {
//出现的错误
int j=1/0;
return restTemplate.getForObject("http://provider/hello", String.class);
}
//直接通过HystrixCommand中的getExecutionException()方法获取异常
@Override
protected String getFallback() {
return "error-commands:"+getExecutionException().getMessage();
}
}
了解:如果抛异常了,我们希望异常直接抛出,不要服务降级,那么只需要配置 忽略某一个异常即可
以注解用法为例:在
HelloService
中
//使用ignoreExceptions属性忽略指定异常,那么再出现忽略的异常就不会进行服务降级(直接进入到报错界面),就不会进入到fallbackMethod指定的方法中
@HystrixCommand(fallbackMethod ="error",ignoreExceptions = ArithmeticException.class)
public String sayHello() {
int j=1/0;
return restTemplate.getForObject("http://provider/hello", String.class);
}
5.5 请求缓存
请求缓存就是在 consumer 中调用同一个接口,如果参数相同,则可以使用之前缓存下来的数据。
首先修改 provider
中的 hello1
接口,一会用来检测缓存配置是否生效:
@GetMapping("/hello1")
public String sayhello1(String name) {
System.out.println(new Date()+">>>"+name);
return "hello:" + name;
}
注解方式
在 hystrix 的HelloService
类中,添加如下注解:
@HystrixCommand(fallbackMethod ="error2")
//这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,缓存的 value 就是方法的返回值。
@CacheResult
public String sayHello1(String name) {
return restTemplate.getForObject("http://provider/hello1?name={1}", String.class,name);
}
//方法参数必须一致
public String error2(String name) {
return "error:"+name;
}
这个配置完成后,缓存并不会生效,一般来说,我们使用缓存,都有一个缓存生命周期这样一个概念。 这里也一样,我们需要初始化 HystrixRequestContext
,初始化完成后,缓存开始生效, HystrixRequestContext
的close()
方法之后,缓存失效。
在hystrix 的HelloController
类中,添加如下方法
@GetMapping("hello4")
public void sayhello4() {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
//请求1
String s = helloService.sayHello1("张三");
//请求2
String s1 = helloService.sayHello1("张三");
ctx.close();
}
在 HystrixRequestContext
的close()
之前,缓存是有效的,close()
之后,缓存就失效了。也就是说,访问一次 hello4 接口, provider 只会被调用一次(第二次使用的缓存),如果再次调用 hello4 接口,之前缓存的数据是失效的。
默认情况下,缓存的 key 就是所调用方法的参数,如果参数有多个,就是多个参数组合起来作为缓存的key。
@HystrixCommand(fallbackMethod = "error2")
@CacheResult//这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,缓存的 value 就是方法的返回值。
public String hello3(String name,Integer age) {
return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
}
此时缓存的 key 就是 name+age,但是,如果有多个参数,但是又只想使用其中一个作为缓存的 key, 那么我们可以通过 @CacheKey
注解来解决。
@HystrixCommand(fallbackMethod = "error2")
@CacheResult//这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,缓存的 value 就是方法的返回值。
public String hello3(@CacheKey String name, Integer age) {
return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
}
上面这个配置,虽然有两个参数,但是缓存时以 name 为准。也就是说,两次请求中,只要 name 一样,即使 age 不一样,第二次请求也可以使用第一次请求缓存的结果。
另外还有一个注解叫做 @CacheRemove()
。在做数据缓存时,如果有一个数据删除的方法,我们一般除了删除数据库中的数据,还希望能够顺带删除缓存中的数据,这个时候 @CacheRemove()
就派上用场了。
@CacheRemove()
在使用时,必须指定 commandKey
属性,commandKey
其实就是缓存方法的名字,指定了 commandKey
,@CacheRemove
才能找到数据缓存在哪里了,进而才能成功删除掉数据
例如如下方法定义缓存与删除缓存:
在hystrix 的HelloService
中再增加如下方法删除缓存的方法:
@HystrixCommand
//通过commandKey为方法的名称
@CacheRemove(commandKey = "sayHello1")
public String deleteUsernameCache(String name) {
return null;
}
修改hystrix 的HelloController
方法:
@GetMapping("hello4")
public void sayhello4() {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
//请求1,数据已经缓存下来了
String s = helloService.sayHello1("张三");
//通过方法又删除了缓存
helloService.deleteUsernameCache("");
//请求2,虽然参数还是【张三】,但是缓存数据已经没了,所以这一次还是会发送请求
String s1 = helloService.sayHello1("张三");
ctx.close();
}
继承方式
在Hystrix 只需要重写 getCacheKey()
方法即可:
public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
String name;
public HelloCommand(Setter setter, RestTemplate restTemplate,String name){
super(setter);
this.name = name; this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://provider/hello2?name={1}", String.class, name);
}
@Override
protected String getCacheKey() {
return name;
}
/**
* 这个方法就是请求失败的回调
* @return
*/
@Override
protected String getFallback() {
return "error-extends:"+getExecutionException().getMessage();
}
调用时候,一定记得初始化 HystrixRequestContext
5.6 请求合并
如果 consumer 中,频繁的调用 provider 中的同一个接口,在调用时,只是参数不一样,那么这样情况下,我们就可以将多个请求合并成一个,这样可以有效提高请求发送的效率。
继承方式
首先我们在 provider 中提供一个请求合并的接口:
@RestController
public class UserController {
//假设 consumer 传过来的多个 id 的格式是 1,2,3,4....
@GetMapping("/user/{ids}")
public List<User> getUserByIds(@PathVariable String ids) {
String[] split = ids.split(",");
List<User> list=new ArrayList<>();
for (String s : split) {
list.add(new User().setId(Integer.parseInt(s)));
}
System.out.println(ids);
return list;
}
}
这个接口既可以处理合并之后的请求,也可以处理单个请求(单个请求的话,List 集合中就只有一项数据。)
然后,在 Hystrix 中,定义 UserService
:
@Service
public class UserService {
@Autowired
private RestTemplate restTemplate;
public List<User> getUsersByIds(List<Integer> ids) {
User[] forObject = restTemplate.getForObject("http://provider/user/{ids}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(forObject);
}
}
接下来定义UserBatchCommand
,相当于我们之前的 HelloCommand
:
public class UserBatchCommand extends HystrixCommand<List<User>> {
private List<Integer> ids;
private UserService userService;
public UserBatchCommand(List<Integer> ids, UserService userService) {
super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(" batchCmd")).andCommandKey(HystrixCommandKey.Factory.asKey("batchKey")));
this.ids=ids;
this.userService=userService;
}
@Override
protected List<User> run() throws Exception {
return userService.getUsersByIds(ids);
}
}
最后,定义最最关键的请求合并方法:
public class UserCollapseCommand extends HystrixCollapser<List<User>,User,Integer> {
private UserService userService;
private Integer id;
public UserCollapseCommand(UserService userService, Integer id) {
super(HystrixCollapser.Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("UserCollapseCommand")).andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(200)));
this.userService = userService;
this.id = id;
}
/**
* 请求参数
* @return
*/
@Override
public Integer getRequestArgument() {
return id;
}
/**
* 请求合并的方法
* @param collection
* @return
*/
@Override
protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Integer>> collection) {
List<Integer> ids = new ArrayList<>(collection.size());
for (CollapsedRequest<User, Integer> userIntegerCollapsedRequest : collection) {
ids.add(userIntegerCollapsedRequest.getArgument());
}
return new UserBatchCommand(ids, userService);
}
/**
* 请求结果分发
* @param users
* @param collection
*/
@Override
protected void mapResponseToRequests(List<User> users, Collection<CollapsedRequest<User, Integer>> collection) {
int count = 0;
for (CollapsedRequest<User, Integer> request : collection) {
request.setResponse(users.get(count++));
}
}
}
最后就是在HelloController
类中增加测试方法调用:
@GetMapping("/hello5")
public void sayhello5() throws ExecutionException, InterruptedException {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
UserCollapseCommand cmd1 = new UserCollapseCommand(userService, 99);
UserCollapseCommand cmd2 = new UserCollapseCommand(userService, 98);
UserCollapseCommand cmd3 = new UserCollapseCommand(userService, 97);
UserCollapseCommand cmd4 = new UserCollapseCommand(userService, 96);
Future<User> q1 = cmd1.queue();
Future<User> q2 = cmd2.queue();
Future<User> q3 = cmd3.queue();
Future<User> q4 = cmd4.queue();
User u1 = q1.get();
User u2 = q2.get();
User u3 = q3.get();
User u4 = q4.get();
System.out.println(u1);
System.out.println(u2);
System.out.println(u3);
System.out.println(u4);
ctx.close();
}
我们发现 provider只收到一次请求,而消费者把请求合并为一个请求发送。
注解方式
@Service
public class UserService {
@Autowired
private RestTemplate restTemplate;
@HystrixCollapser(batchMethod = "getUsersByIds", collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "200")})
public Future<User> getUserById(Integer id) {
return null;
}
@HystrixCommand
public List<User> getUsersByIds(List<Integer> ids) {
User[] forObject = restTemplate.getForObject("http://provider/user/{ids}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(forObject);
}
}
这里的核心是 @HystrixCollapser
注解。在这个注解中,指定批处理的方法即可。
@GetMapping("/hello6")
public void hello6() throws ExecutionException, InterruptedException {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
Future<User> q1 = userService.getUserById(99);
Future<User> q2 = userService.getUserById(98);
Future<User> q3 = userService.getUserById(97);
User u1 = q1.get();
User u2 = q2.get();
User u3 = q3.get();
System.out.println(u1);
System.out.println(u2);
System.out.println(u3);
Thread.sleep(2000);
Future<User> q4 = userService.getUserById(96);
User u4 = q4.get();
System.out.println(u4);
ctx.close();
}
6、OpenFeign
前面无论是基本调用,还是 Hystrix,我们实际上都是通过手动调用 RestTemplate 来实现远程调用
的。使用 RestTemplate 存在一个问题:繁琐,每一个请求,参数不同,请求地址不同,返回数据类型不同,其他都是一样的,所以我们希望能够对请求进行简化。
我们希望对请求进行简化,简化方案就是 OpenFeign。
一开始这个组件不叫这个名字,一开始就叫 Feign,Netflix Feign,但是 Netflix 中的组件现在已经停止开源工作,OpenFeign 是 Spring Cloud 团队在 Netflix Feign 的基础上开发出来的声明式服务调用组件。关于 OpenFeign 组件的 Issue:https://github.com/OpenFeign/feign/issues/373
6.1 简单使用
继续使用之前的 Provider。
新建一个 Spring Boot 模块,创建时,选择 OpenFeign 依赖,如下:
项目创建成功后,在 application.properties
中进行配置,使项目注册到 Eureka 上:
# 应用名称
spring.application.name=openfeign
# 应用服务 WEB 访问端口
server.port=4000
#将该服务注册到注册中心上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
通过
feign
去调用provider
中的hello接口@GetMapping("/hello") public String sayHello() { return "hello eureka:" + port; }
接下来在启动类上添加注解,开启 Feign
的支持:
@EnableFeignClients
@SpringBootApplication
public class OpenfeignApplication {
public static void main(String[] args) {
SpringApplication.run(OpenfeignApplication.class, args);
}
}
接下来,定义 HelloService
接口,去使用OpenFeign
:
/**
* 这里的FeignClient中的name=“provider”相当于请求中的服务名 http://provider/hello
*/
@FeignClient(name = "provider")
public interface HelloService {
/**
* GetMapping中的name="/hello" 相当于请求中的具体请求
* @return 方法的返回值就是 http://provider/hello 服务的返回值。这里的方法名随意
*/
@GetMapping("/hello")
public String feignHello();
}
最后调用 HelloController
中,调用HelloService
进行测试:
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello1")
public String sayHello1() {
return helloService.feignHello();
}
}
接下来,启动 OpenFeign
项目,进行测试。
6.2 参数传递
和普通参数传递的区别:
- 参数一定要绑定参数名(即通过
@RequestParam
绑定)。 - 如果通过 请求头
header
来传递参数,一定记得中文要转码。
测试的服务端接口,继续使用 provider
提供的接口。
@RestController
public class HelloController {
//获取配置文件中的端口号
@Value("${server.port}")
int port;
@GetMapping("/hello")
public String sayHello() {
return "hello eureka:" + port;
}
@GetMapping("/hello1")
public String sayhello1(String name) {
System.out.println(new Date()+">>>"+name);
return "hello:" + name;
}
@PostMapping("/user2")
public User addualUser2(@RequestBody User user) {
return user;
}
@DeleteMapping("/user2/{id}")
public void deleteUser2(@PathVariable Integer id){
System.out.println(id);
}
@GetMapping("/user3")
public void getUserByName(@RequestHeader("name") String name) throws UnsupportedEncodingException {
System.out.println(URLDecoder.decode(name,"UTF-8"));
}
}
这里,我们主要在openfeign
中添加调用接口即可:
/**
* 这里的FeignClient中的name=“provider”相当于请求中的服务名 http://provider/hello
*/
@FeignClient(name = "provider")
public interface HelloService {
/**
* GetMapping中的name="/hello" 相当于请求中的具体请求
* @return 方法的返回值就是 http://provider/hello 服务的返回值。这里的方法名随意
*/
@GetMapping("/hello")
public String sayHello();
@GetMapping("/hello1")
public String sayhello1(@RequestParam("name") String name);
@PostMapping("/user2")
public User addUser2(@RequestBody User user);
@DeleteMapping("/user2/{id}")
public void deleteUser2(@PathVariable("id") Integer id);
@GetMapping("/user3")
public void getUserByName(@RequestHeader("name") String name) throws UnsupportedEncodingException;
}
注意,凡是 key/value 形式的参数,一定要标记参数的名称。
HelloController
中调用 HelloService:
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello1")
public String sayHello1() throws UnsupportedEncodingException {
String r1 = helloService.sayhello1("张三");
System.out.println("r1>>>>>>:"+r1);
User user = new User();
user.setUsername("zs");
user.setPassword("123456");
user.setAge(11);
user.setId(1);
User r2 = helloService.addUser2(user);
System.out.println("r2>>>>>>>:"+r2.toString());
helloService.deleteUser2(2);
helloService.getUserByName(URLEncoder.encode("李四","UTF-8"));
return helloService.sayHello();
}
}
注意:放在 header 中的中文参数,一定要编码之后传递
6.3 继承特性
将 provider
和openfeign
中公共的部分提取出来,一起使用。
我们新建一个 Module
,叫做 hello-api
,注意,由于这个模块要被其他模块所依赖,所以这个模块是一个 Maven
项目,但是由于这个模块要用到 SpringMVC
的东西,因此在创建成功后,给这个模块添加一个 web 依赖,导入 SpringMVC
需要的一套东西。
项目创建成功后,首先添加依赖:
<dependency>
<groupId>org.lc</groupId>
<artifactId>entity</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
在hello-api
项目中定义公共接口
public interface IHelloService {
@GetMapping("/hello")
public String sayHello();
@GetMapping("/hello1")
public String sayhello1(@RequestParam("name") String name);
@PostMapping("/user2")
public User addUser2(@RequestBody User user);
@DeleteMapping("/user2/{id}")
public void deleteUser2(@PathVariable("id") Integer id);
@GetMapping("/user3")
public void getUserByName(@RequestHeader("name") String name) throws UnsupportedEncodingException;
}
定义完成后,接下来,在 provider
和 openfeign
中,分别引用该模块:
<dependency>
<groupId>org.lc</groupId>
<artifactId>hello-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
添加成功之后,在 provider
中实现该接口:
继承
IHelloService
的接口,若重写其中的方法,则无需再申明请求的地址,直接继承接口IHelloService
中的请求地址即可
@RestController
public class HelloController implements IHelloService {
//获取配置文件中的端口号
@Value("${server.port}")
int port;
@Override
public String sayHello() {
return "hello eureka:" + port;
}
@Override
public String sayhello1(String name) {
System.out.println(new Date()+">>>"+name);
return "hello:" + name;
}
@Override
public User addUser2(@RequestBody User user) {
return user;
}
@Override
public void deleteUser2(@PathVariable Integer id){
System.out.println(id);
}
@Override
public void getUserByName(@RequestHeader("name") String name) throws UnsupportedEncodingException {
System.out.println(URLDecoder.decode(name,"UTF-8"));
}
}
在 openfeign
中,定义接口继承自公共接口:
/**
* 这里的FeignClient中的name=“provider”相当于请求中的服务名 http://provider/hello
*/
@FeignClient(name = "provider")
public interface HelloService extends IHelloService {
}
在openfeign
中的HelloController
中的接口不变
关于继承特性:
- 使用继承特性,代码简洁明了不易出错。服务端和消费端的代码统一,一改俱改,不易出错。这是 优点也是缺点,这样会提高服务端和消费端的耦合度。
- 6.2 中所讲的参数传递,在使用了继承之后,依然不变,参数该怎么传还是怎么传。
6.4 日志
OpenFeign
中,我们可以通过配置日志,来查看整个请求的调用过程。日志级别一共分为四种:
NONE:不开启日志,默认就是这个
BASIC:记录请求方法、URL、响应状态码、执行时间
HEADERS:在 BASIC 的基础上,加载请求/响应头
FULL:在 HEADERS 基础上,再增加 body 以及请求元数据。
四种级别,可以通过 Bean 来配置:
@EnableFeignClients
@SpringBootApplication
public class OpenfeignApplication {
public static void main(String[] args) {
SpringApplication.run(OpenfeignApplication.class, args);
}
@Bean
Logger.Level loggerLevel() {
return Logger.Level.FULL;
}
}
最后,在 application.properties
中开启日志级别:
#在 org.lc.openfeign包下的HelloService类下开启debug日志级别
#也可以设置整个包下开启
logging.level.org.lc.openfeign.HelloService=debug
重启 OpenFeign
,进行测试。
6.5 数据压缩
# 开启请求的数据压缩
feign.compression.request.enabled=true
#启响应的数据压缩
feign.compression.response.enabled=true
#压缩的数据类型
feign.compression.request.mime-types=text/html,application/json
#压缩的数据下限,2048 表示当要传输的数据大于 2048 时,才会进行数据压缩
feign.compression.request.min-request-size=2048
6.6 Hystrix容错
Hystrix 中的容错、服务降级等功能,在 OpenFeign
中一样要使用。
在application.properties中开启服务降级:
#开启服务降级
feign.hystrix.enabled=true
方式一:
@Component //防止请求地址重复(只要和原请求不同即可) @RequestMapping("/lccc") public class HelloServiceFallback implements HelloService{ @Override public String sayHello() { return "error>>>sayHello"; } @Override public String sayhello1(String name) { return "error>>>sayhello1"; } @Override public User addUser2(User user) { System.out.println("error>>>>addUser2"); return null; } @Override public void deleteUser2(Integer id) { System.out.println("error>>>>deleteUser2"); } @Override public void getUserByName(String name) throws UnsupportedEncodingException { System.out.println("error>>>>getUserByName"); } }
/** * 这里的FeignClient中的name=“provider”相当于请求中的服务名 http://provider/hello */ @FeignClient(name = "provider",fallback = HelloServiceFallback.class) public interface HelloService extends IHelloService { }
方式二:通过自定义
FallbackFactory
来实现请求降级@Component public class HelloServiceFallbackFactory implements FallbackFactory<HelloService> { @Override public HelloService create(Throwable throwable) { return new HelloService() { @Override public String sayHello() { return "error>>>sayHello"; } @Override public String sayhello1(String name) { return "error>>>sayhello1"; } @Override public User addUser2(User user) { System.out.println("error>>>>addUser2"); return null; } @Override public void deleteUser2(Integer id) { System.out.println("error>>>>deleteUser2"); } @Override public void getUserByName(String name) throws UnsupportedEncodingException { System.out.println("error>>>>getUserByName"); } }; } }
/** * 这里的FeignClient中的name=“provider”相当于请求中的服务名 http://provider/hello */ @FeignClient(name = "provider",fallbackFactory = HelloServiceFallbackFactory.class) public interface HelloService extends IHelloService { }
7、Resilience4j
Resilience4j 是 Spring Cloud Greenwich 版推荐的容错解决方案,相比 Hystrix,Resilience4j 专为
Java8 以及函数式编程而设计。
Resilience4j 主要提供了如下功能:
断路器
限流
基于信号量的隔离
缓存
限时
请求重试
7.1 基本用法
首先搭建一个简单的测试环境。
创建一个maven项目,在单元测试中测试:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
7.1.1 断路器
Resilience4j 提供了很多功能,不同的功能对应不同的依赖,可以按需添加。使用断路器,则首先添加断路器的依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.13.2</version>
</dependency>
一个正常执行的例子:
@Test
public void test01(){
//方式一:获取一个CircuitBreakerRegistry实例,可以调用ofDefaults获取一个CircuitBreakerRegistry默认实例
CircuitBreakerRegistry r1=CircuitBreakerRegistry.ofDefaults();
//方式二:也可以自定义属性,通过CircuitBreakerConfig自定义一些配置
CircuitBreakerConfig config=CircuitBreakerConfig.custom()
//故障率阈值百分比,超过这个阈值,断路器就会打开(默认为50)
.failureRateThreshold(50)
//断路器保持打开的时间,在到达设置的时间之后,断路器会进入到 half open 状态
.waitDurationInOpenState(Duration.ofMillis(1000))
//当断路器处于half open 状态时环形缓冲区的大小
.ringBufferSizeInHalfOpenState(2)
//当断路器处于关闭状态时环形缓存区的大小
.ringBufferSizeInClosedState(2)
.build();
CircuitBreakerRegistry r2 = CircuitBreakerRegistry.of(config);
//创建断路器
CircuitBreaker cb1 = r1.circuitBreaker("lc1");
//通过配置的断路器注册表创建断路器
CircuitBreaker lc2 = r2.circuitBreaker("lc2",config);
//提供一个函数
CheckedFunction0<String> supplier = CircuitBreaker.decorateCheckedSupplier(cb1, () -> "hello resilience");
//拿到结果并加入之定义的值
Try<String> result = Try.of(supplier)
.map(v -> v + " hello world");
//true
System.out.println(result.isSuccess());
// hello resilience hello world
System.out.println(result.get());
}
一个出异常的断路器:
@Test
public void test02(){
CircuitBreakerConfig config=CircuitBreakerConfig.custom()
//故障率阈值百分比,超过这个阈值,断路器就会打开(默认为50)
.failureRateThreshold(50)
//断路器保持打开的时间,在到达设置的时间之后,断路器会进入到 half open 状态
.waitDurationInOpenState(Duration.ofMillis(1000))
//当断路器处于half open 状态时环形缓冲区的大小
.ringBufferSizeInHalfOpenState(2)
//当断路器处于关闭状态时环形缓存区的大小
.ringBufferSizeInClosedState(2)
.build();
CircuitBreakerRegistry r2 = CircuitBreakerRegistry.of(config);
//通过配置的断路器注册表创建断路器
CircuitBreaker cb1 = r2.circuitBreaker("lc2");
//获取断路器的一个状态 CLOSED
System.out.println(cb1.getState());
cb1.onError(0,new RuntimeException());
//获取断路器的一个状态 CLOSED
System.out.println(cb1.getState());
cb1.onError(0,new RuntimeException());
//获取断路器的一个状态 OPEN
System.out.println(cb1.getState());
//提供一个函数
CheckedFunction0<String> supplier = CircuitBreaker.decorateCheckedSupplier(cb1, () -> "hello resilience");
//拿到结果并加入之定义的值
Try<String> result = Try.of(supplier)
.map(v -> v + " hello world");
//false
System.out.println(result.isSuccess());
//报错,段断路器打开异常
System.out.println(result.get());
}
注意,由于ringBufferSizeInClosedState
的值为 2,表示当有两条数据时才会去统计故障率,所以下面的手动故障测试,至少调用两次onError
,断路器才会打开。
7.1.2 限流
RateLimiter 本身和前面的断路器很像。首先添加依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>0.13.2</version>
</dependency>
限流测试:
@Test
public void test03(){
RateLimiterConfig config = RateLimiterConfig.custom()
//阈值刷新的时间 1s
.limitRefreshPeriod(Duration.ofMillis(1000))
//阈值刷新的频次
.limitForPeriod(2)
//超时时间
.timeoutDuration(Duration.ofMillis(1000))
.build();
RateLimiter rateLimiter = RateLimiter.of("lc", config);
CheckedRunnable checkedRunnable = RateLimiter.decorateCheckedRunnable(rateLimiter, () -> {
System.out.println(new Date());
});
//上面代表我们1s钟只能处理2个请求
//若limitForPeriod(4)则 1s能处理4个请求
Try.run(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
//失败则执行
.onFailure(t -> System.out.println(t.getMessage()));
// Wed Feb 03 12:47:53 GMT+08:00 2021
// Wed Feb 03 12:47:53 GMT+08:00 2021
// Wed Feb 03 12:47:54 GMT+08:00 2021
// Wed Feb 03 12:47:54 GMT+08:00 2021
}
7.1.3 请求重试
首先加入所需的依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>0.13.2</version>
</dependency>
案例:
@Test
public void test04(){
RetryConfig config = RetryConfig.custom()
//重试次数
.maxAttempts(5)
//重试间隔
.waitDuration(Duration.ofMillis(1000))
//重试的异常
.retryExceptions(RuntimeException.class)
.build();
Retry retry = Retry.of("lc", config);
Retry.decorateRunnable(retry, new Runnable(){
int count=0;
//开启了重试功能之后,run 方法执行时,如果抛出异常,会自动触发重试功能
//如上配置 代表进行5次重试,每次重试执行一次run方法,count为3时不再抛出RuntimeException异常,所以最后方法能够正常执行。
@Override
public void run() {
if (count++ < 3) {
System.out.println("count:"+count);
throw new RuntimeException();
}
}
}).run();
}
7.2 结合微服务
Retry
、CircuitBreaker
、RateLimite
7.2.1 Retry
首先创建一个 Spring Boot 项目,创建时,添加 eureka-client 依赖,使之能够注册到 eureka 上
项目创建成功后,手动添加 Resilience4j 依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.3.1</version>
<exclusions>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>
resilience4j-spring-boot2 中包含了 Resilience4j 的所有功能,但是没有配置的功能无法使用,需要将之从依赖中剔除掉。
接下来,在 application.yml
中配置 retry
:
server:
port: 5000
spring:
application:
name: resilience4j2
eureka:
client:
service-url:
#注册到注册中心上
defaultZone: http://localhost:1111/eureka
resilience4j:
retry:
#表示Retry的优先级 数字越小优先级越高
retry-aspect-order: 339
backends:
#重试组 retryA,可以定义多个重试组
retryA:
maxRetryAttempts: 5 # 重试次数
waitDuration: 500 # 重试等待时间
exponentialBackoffMultiplier: 1.1 # 间隔乘数
retryExceptions:
- java.lang.RuntimeException
修改
provider
中的测试请求@GetMapping("/hello") public String sayHello() { System.out.println("hello eureka:" + port); int i=1/0; return "hello eureka:" + port; }
最后,创建测试的HelloController
、HelloService
:
@SpringBootApplication
public class Resilience4j2Application {
public static void main(String[] args) {
SpringApplication.run(Resilience4j2Application.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
//对应application.yml中的重试组策略名称
@Retry(name = "retryA")
public class HelloService {
@Autowired
private RestTemplate restTemplate;
public String hello() {
return restTemplate.getForObject("http://provider:1116/hello", String.class);
}
}
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello() {
return helloService.hello();
}
}
我们最后发现,在打印的错误日志中,"hello eureka:" + port
这段话在provider
中打印了5次,即进行了5次重试
7.2.2 CircuitBreaker
首先从依赖中删除排除的 CircuitBreaker
。
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.3.1</version>
<exclusions>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>
还是使用之前的
provider
中的测试接口
然后在 application.yml 中进行配置:
server:
port: 5000
spring:
application:
name: resilience4j2
eureka:
client:
service-url:
#注册到注册中心上
defaultZone: http://localhost:1111/eureka
resilience4j:
retry:
#表示Retry的优先级 数字越小优先级越高
retry-aspect-order: 339
backends:
#重试组 retryA,可以定义多个重试组
retryA:
maxRetryAttempts: 5 # 重试次数
waitDuration: 500 # 重试等待时间
exponentialBackoffMultiplier: 1.1 # 间隔乘数
retryExceptions:
- java.lang.RuntimeException
circuitbreaker:
instances:
#优先级配置
#断路器组cba,可以定义多个断路器组
cbA:
ringBufferSizeInClosedState: 5
ringBufferSizeInHalfOpenState: 3
waitInterval: 5000
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
circuit-breaker-aspect-order: 398
配置完成后,用 @CircuitBreakder
注解标记相关方法:
@Service
// 对应application.yml中的断路器组策略名称
@CircuitBreaker(name = "cbA",fallbackMethod = "error")
public class HelloService {
@Autowired
private RestTemplate restTemplate;
public String hello() {
return restTemplate.getForObject("http://provider:1116/hello", String.class);
}
public String error(Throwable throwable) {
return "error";
}
}
@CircuitBreaker
注解中的 name
属性用来指定circuitbreaker
配置,fallbackMethod
属性用来指定服务降级的方法,需要注意的是,服务降级方法中,要添加异常参数。
7.2.3 RateLimiter
RateLimiter 作为限流工具,主要在服务端使用,用来保护服务端的接口。
首先在
provider
中添加RateLimiter
依赖,并排除一些不必要的依赖<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.3.1</version> <exclusions> <exclusion> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> </exclusion> <exclusion> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-bulkhead</artifactId> </exclusion> <exclusion> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-timelimiter</artifactId> </exclusion> </exclusions> </dependency>
接下来,在
provider
的application.properties
配置文件中,去配置RateLimiter
#当前服务名称 spring.application.name=provider #端口号 server.port=1116 #将该服务的client注册到 http://localhost:1111/eureka地址的server上 eureka.client.service-url.defaultZone=http://localhost:1111/eureka # 这里配置每秒钟处理一个请求 同理也配置限流组名称为rlA resilience4j.ratelimiter.limiters.rlA.limit-for-period=1 resilience4j.ratelimiter.limiters.rlA.limit-refresh-period=1s resilience4j.ratelimiter.limiters.rlA.timeout-duration=1s
为了查看请求效果,在
provider
的HelloController
中打印每一个请求的时间:@RateLimiter(name = "rlA") @GetMapping("/hello") public String sayHello() { System.out.println(new Date()); return "hello eureka:" + port; }
这里通过
@RateLimiter
注解来标记该接口限流。配置完成后,重启provider
。
然后,在客户端模拟多个请求,查看限流效果:
@Service
//对应application.yml中的重试组策略名称
// @Retry(name = "retryA")
// 对应application.yml中的断路器组策略名称
@CircuitBreaker(name = "cbA",fallbackMethod = "error")
public class HelloService {
@Autowired
private RestTemplate restTemplate;
public String hello() {
for (int i = 0; i < 5; i++) {
restTemplate.getForObject("http://provider:1116/hello", String.class);
}
return "success";
}
public String error(Throwable throwable) {
return "error";
}
}
Wed Feb 03 16:08:57 GMT+08:00 2021
Wed Feb 03 16:08:57 GMT+08:00 2021
Wed Feb 03 16:08:58 GMT+08:00 2021
Wed Feb 03 16:08:59 GMT+08:00 2021
Wed Feb 03 16:09:00 GMT+08:00 2021
我们可以看到请求每隔1s调用了一次
8、 服务监控
微服务由于服务数量众多,所以出故障的概率很大,这种时候不能单纯的依靠人肉运维。
早期的 Spring Cloud
中,服务监控主要使用 Hystrix Dashboard
,集群数据库监控使用 Turbine
。在 Greenwich
版本中,官方建议监控工具使用 Micrometer
。
Micrometer:
- 提供了度量指标,例如 timers、counters
- 一揽子开箱即用的解决方案,例如缓存、类加载器、垃圾收集等等
建一个 Spring Boot 项目,添加 Actuator 依赖。项目创建成功后,添加如下配置,开启所有端点:
management.endpoints.web.exposure.include=*
然后就可以在浏览器查看项目的各项运行数据,但是这些数据都是 JSON
格式。
我们需要一个可视化工具来展示这些 JSON 数据。这里主要和大家介绍 Prometheus。
8.1 Prometheus(普罗米修斯)
#下载
wget https://github.com/prometheus/prometheus/releases/download/v2.16.0/prometheus-2.16.0.linux-amd64.tar.gz
#解压
tar -zxvf prometheus-2.16.0.linux-amd64.tar.gz
#切换到解压后的文件夹
cd prometheus-2.16.0.linux-amd64
查看该文件夹下的内容:
修改 prometheus.yml
配置文件,主要改两个地方,一个是数据接口,另一个是服务地址:
接下来,将 Prometheus
整合到 Spring Boot
项目中。首先加依赖
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
然后在 application.properties
配置中,添加Prometheus
配置:
management.endpoints.web.exposure.include=*
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
management.endpoint.metrics.enabled=true
#这里的服务端口要和Prometheus中监听的端口保持一致
server.port=8082
将服务部署到服务器并启动。
接下来启动 Prometheus,启动命令:
./prometheus --config.file=prometheus.yml
启动成功后,浏览器输入 http://47.96.141.44:9090 查看 Prometheus
数据信息。
8.2 Grafana
参考地址:https://grafana.com/grafana/download?platform=linux
#下载
wget https://dl.grafana.com/oss/release/grafana-7.3.7-1.x86_64.rpm
#安装
sudo yum install grafana-7.3.7-1.x86_64.rpm
#启动
systemctl start grafana-server
默认端口为3000:http://47.96.141.44:3000
初始账号和密码都为admin
,登录后需要修改密码。
我们可以在这选择数据源操作,例如添加Prometheus
服务信息
9、Zuul
9.1 服务网关
Zuul
和 Gateway
由于每一个微服务的地址都有可能发生变化,无法直接对外公布这些服务地址,基于安全以及高内聚低 耦合等设计,我们有必要将内部系统和外部系统做一个切割。
一个专门用来处理外部请求的组件,就是服务网关。
权限问题统一处理
数据剪裁和聚合
简化客户端的调用
可以针对不同的客户端提供不同的网关支持
Spring Cloud
中,网关主要有两种实现方案:
Zuul
Spring Cloud Gateway
9.2 Zuul
Zuul 是 Netflix 公司提供的网关服务
Zuul 的功能:
权限控制,可以做认证和授权监控
动态路由负载均衡
静态资源处理
Zuul 中的功能基本上都是基于过滤器来实现,它的过滤器有几种不同的类型:
- PRE
- ROUTING
- POST
- ERROR
9.3 简单使用 HelloWorld
首先创建项目,添加 Zuul 依赖。
项目创建成功后,将 zuul
注册到 eureka
上:
# 应用名称
spring.application.name=zuul
# 应用服务 WEB 访问端口
server.port=2020
#将服务注册到注册中心上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
在启动类上开启网关代理:
@SpringBootApplication
//开启网关代理
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
配置完成后,重启 Zuul
,接下来,在浏览器中,通过 Zuul
的代理就可以访问到 provider
了。
例如访问provider
中的/hello接口
:
在这个访问地址中,provider 就是要访问的服务名称,/hello 则是要访问的服务接口。
这是一个简单例子,Zuul 中的路由规则也可以自己配置。
zuul.routes.lcc.path=/lcc/**
zuul.routes.lcc.service-id=provider
访问规则:只要以
/lcc
开头的请求都映射到provider
服务上: http://localhost:2020/lcc/hello
以上两条配置文件也可以简写为:
zuul.routes.provider=/lcc/**
9.4 请求过滤
对于来自客户端的请求,可以在 Zuul
中进行预处理,例如权限判断等。定义一个简单的权限过滤器:
@Component
public class PermissionFilter extends ZuulFilter {
/**
* 过滤器类型,PRE ROUTING POST ERROR
* 这里我们使用预处理的类型 PRE
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器优先级 若有多个过滤器则值越小优先级越高
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 该过滤器是否生效
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 核心的过滤逻辑写在这里
* @return 这个方法虽然有返回值,但是这个返回值目前无所谓
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
//获取当前请求对象
HttpServletRequest request = ctx.getRequest();
String username = request.getParameter("username");
String password = request.getParameter("password");
if (!StringUtils.equals(username, "lc") || !StringUtils.equals(password, "123456")) {
//如果不满足条件则给出响应的内容
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(403);
ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8");
ctx.setResponseBody("非法请求");
}
return null;
}
}
在9.3
配置的基础上进行:
重启 Zuul,接下来,发送请求必须带上 username
和 password
参数,否则请求不通过。
http://localhost:2020/lcc/hello?username=lc&password=123456
9.5 Zuul中的其他配置
9.5.1 匹配规则
例如有两个服务,一个叫 consumer
,另一个叫 consumer-hello
,在做路由规则设置时,假如出现了如下配置:
zuul.routes.consumer=/consumer/**
zuul.routes.consumer-hello=/consumer/hello/**
此时,如果访问一个地址:http://localhost:2020/consumer/hello/123,会出现冲突。实际上,这个 地址是希望和 consumer-hello 这个服务匹配的,这个时候,只需要把配置文件改为 yml 格式就可以了(因为properites
的配置文件是无序的,而yaml
配置文件是有序的)。所以只需要把需要的请求写在前面即可
9.5.2 忽略服务
默认情况下,zuul 注册到 eureka 上之后,eureka 上的所有注册服务都会被自动代理。如果不想给某一个服务做代理,可以忽略该服务,配置如下:
zuul.ignored-services=provider
上面这个配置表示忽略 provider 服务,此时就不会自动代理 provider 服务了。
也可以忽略某一类地址:
zuul.ignored-patterns=/**/hello/**
这个表示请求路径中如果包含 hello,则不做代理。
9.5.3 前缀
也可以给路由加前缀。
zuul.prefix=/lc
这样,以后所有的请求都需要携带前缀去访问.
10、Spring Cloud Gateway
10.1 简介
特点:
- 限流
- 路径重写
- 动态路由
- 集成Spring Cloud DiscoveryClient
- 集成Hystrix断路器
Spring Cloud Gateway
和 Zuul
对比
Zuul 是 Netflix 公司的开源产品,Spring Cloud Gateway 是 Spring 家族中的产品,可以和
Spring 家族中的其他组件更好的融合
Zuul1 不支持长连接,例如 websocket。
Spring Cloud Gateway 支持限流
Spring Cloud Gateway 基于 Netty 来开发,实现了异步和非阻塞,占用资源更小,性能强于
Zuul
10.2 基本用法
Spring Cloud Gateway 支持两种不同的用法:
- 编码式
- yml配置
编码式:
首先创建 Spring Boot 项目,添加 Spring Cloud Gateway 模块:
项目创建成功后,直接配置一个 RouteLocator
这样一个 Bean
,就可以实现请求转发。
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
//将路由起名为lc,并将路径/get 指向为 http://httpbin.org请求
.route("lc",r->r.path("/get").uri("http://httpbin.org"))
.build();
}
}
这里只需要提供 RouteLocator
这个 Bean,就可以实现请求转发。配置完成后,重启项目,访问:htt p://localhost:8080/get
即访问的为http://httpbin.org
properties配置
spring.cloud.gateway.routes[0].id=lc
spring.cloud.gateway.routes[0].uri=http://httpbin.org
spring.cloud.gateway.routes[0].predicates[0]=Path=/get
yaml配置
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://httpbin.org
predicates:
- Path=/get
10.3 结合微服务
首先给 Gateway
添加依赖,将之注册到 Eureka
上。加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
加配置:
spring:
cloud:
gateway:
discovery:
locator:
#开启自动代理
enabled: true
application:
name: gateway
eureka:
client:
service-url:
#注册到注册中心上
defaultZone: http://localhost:1111/eureka
logging:
level:
org.lc.gateway: debug
接下来,就可以通过 Gateway 访问到其他注册在 Eureka 上的服务了,访问方式和 Zuul 一样。
访问provider
中的/hello
接口
http://localhost:8080/PROVIDER/hello 注意这里的PROVIDER
为大写,根据Application
名称来访问的
10.4 Predicate
通过时间匹配:
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://httpbin.org
predicates:
- Path=/get
- After=2021-02-07T18:30:00+08:00[Asia/Shanghai]
表示,请求时间在 2021-02-07日18:30:00+08:00[Asia/Shanghai] 时间之后,才会被路由。
除了 After 之外,还有两个关键字:
Before:表示在某个时间点之前进行请求转发
**Between ** :表示在两个时间点之间,两个时间点用 , 隔开
通过请求方式匹配:就是请求方法
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://httpbin.org
predicates:
- Path=/get
- Method=GET
这个配置表示只给 GET
请求进行路由。
请求路径匹配:
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://www.louchen.top
predicates:
- Path=/2020/09/04/{segment}
当访问 http://loaclhost:8080/2020/09/04/network/HTTP详解/
会被转发为:http://www.louchen.top/2020/09/04/network/HTTP详解/
通过参数进行匹配:
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://httpbin.org
predicates:
- Query=name
表示请求中一定要有 name 参数才会进行转发,否则不会进行转发。
也可以指定参数和参数的值:
例如参数的 key
为 name
,value
必须要以 java
开始:
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://httpbin.org
predicates:
- Query=name,java.*
多种匹配方式也可以组合使用:
spring:
cloud:
gateway:
routes:
- id: lc
uri: http://httpbin.org
predicates:
- Path=/get
- Query=name,java.*
- Method=GET
- After=2021-02-07T18:30:00+08:00[Asia/Shanghai]
10.5 Filter
- GatewayFilter
- GlobalFilter
AddRequestParameter
过滤器使用:
spring:
cloud:
gateway:
routes:
- id: lc
# 将请求自动转发到provider服务上,若有多个实例,则会自动实现负载均衡
uri: lb://provider
filters:
- AddRequestParameter=name,lc
predicates:
- Method=GET
当我们请求provider服务中的/hello1接口:
@GetMapping("/hello1") public String sayhello1(String name) { System.out.println(new Date()+">>>"+name); return "hello:" + name; }
访问:http://localhost:8080/hello1 会自动在请求上加上 name
=lc
的参数
11、Spring Cloud Config
11.1 基本用法
11.1.1 简介
Spring Cloud Config
是一个分布式系统配置管理的解决方案,它包含了 Client
和 Server
。配置文件放在 Server
端,通过 接口的形式提供给 Client
。
Spring Cloud Config 主要功能:
- 集中管理各个环境、各个微服务的配置文件
- 提供服务端和客户端支持
- 配置文件修改后,可以快速生效
- 配置文件通过 Git/SVn 进行管理,天然支持版本回退功能
- 支持高并发查询、也支持多种开发语言
11.1.2 创建所需的文件
准备工作主要是给 GitHub 上提交数据。
本地准备好相应的配置文件,提交到 GitHub: https://github.com/RoyalNeverG/cloudconfig.git
文件内容分别为:
client-dev
: lc=dev
client-prod
: lc=prod
client-test
: lc=test
11.1.3 ConfigServer
首先创建一个 ConfigServer
工程,创建时添加 ConfigServer
依赖:
项目创建成功后,项目启动类上添加注解,开启 config server
功能:
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
然后在配置文件中配置仓库的基本信息:
# 应用名称
spring.application.name=config-server
# 应用服务 WEB 访问端口
server.port=8081
#配置文件仓库地址
spring.cloud.config.server.git.uri=https://github.com/RoyalNeverG/cloudconfig.git
#仓库中,配置文件的目录。可能仓库中有 多个目录。
spring.cloud.config.server.git.search-paths=client1
#git的用户名
spring.cloud.config.server.git.username=421192425@qq.com
#git密码
spring.cloud.config.server.git.password=
启动项目后,就可以访问配置文件了。
路径规则的含义:
访问地址:http://localhost:8081/client1/dev/master
{ "name": "client1", "profiles": [ "dev" ], "label": "master", "version": "443dde3c001379f3da68843bb266fe63a778ff8d", "state": null, "propertySources": [ { "name": "https://github.com/RoyalNeverG/cloudconfig.git/client1/client1-dev.properties", "source": { "lc": "dev" } } ] }
访问地址:http://localhost:8081/client1-dev.properties或http://localhost:8081/master/client1-dev.properties
lc: dev
/{application}/{profile}/[{label}]
/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.yml
/{label}/{application}-{profile}.properties
- application: 表示配置文件所在的前缀,例如
client1-test.properties
中的client1
- profile:表示配置文件profile,例如
client1-test.properties
中的test
,dev,prod - label :表示git分支,参数可选,默认不写就是master分支
接下来,可以修改本地git
仓库的配置文件,并且重新提交到 GitHub
修改的: client1-dev.properties
文件后提交到仓库
lc=devupdate
此时,刷新 ConfigServer
接口,就可以及时看到最新的配置内容。
重新访问:http://localhost:8081/client1-dev.properties,可以发现本地修改的内容已经重新获取到:
lc: devupdate
原理:我们查看日志可以发现,从远端下载了一份文件保存在本地。
2021-02-08 13:45:35.163 INFO 37680 --- [nio-8081-exec-5] o.s.c.c.s.e.NativeEnvironmentRepository : Adding property source: file:/C:/Users/42119/AppData/Local/Temp/config-repo-4868007234302158719/client1/client1-dev.properties
11.1.4 ConfigCIient
项目创建成功后,resources
目录下,添加 bootstrap.properties
配置内容如下:
bootstrap.properties
比application.properties
预先加载
#下面三个配置 分别对应 config-server 中的 {application}、{profile}以及{label}占位符
spring.application.name=client1
spring.cloud.config.profile=dev
spring.cloud.config.label=master
#config server服务所在的地址
spring.cloud.config.uri=http://localhost:8081
server.port=8082
接下来创建一个HelloController
进行测试:
@RestController
public class HelloContorller {
@Value("${lc}")
String str;
@GetMapping("/hello")
public String sayHello() {
return str;
}
}
访问:http://localhost:8082/hello
结果:devupdate
11.1.5 占位符使用配置
使用占位符灵活控制查询目录。修改 config-server
配置文件:
# 应用名称
spring.application.name=config-server
# 应用服务 WEB 访问端口
server.port=8081
#配置文件仓库地址
spring.cloud.config.server.git.uri=https://github.com/RoyalNeverG/cloudconfig.git
#仓库中,配置文件的目录。可能仓库中多个目录。
spring.cloud.config.server.git.search-paths={application}
spring.cloud.config.label={label}
spring.cloud.config.profile={profile}
#git的用户名
spring.cloud.config.server.git.username=421192425@qq.com
#git密码
spring.cloud.config.server.git.password=
config-client
配置文件:#下面三个配置 分别对应 config-server 中的 {application}、{profile}以及{label}占位符 spring.application.name=client1 spring.cloud.config.profile=dev spring.cloud.config.label=master #config server服务所在的地址 spring.cloud.config.uri=http://localhost:8081 server.port=8082
{application}
占位符:表示从config-client
链接上来的 client1
的 spring.application.name
属性的值。
{profile}
占位符:表示从config-client
链接上来的 dev
的 spring.cloud.config.profile
属性的值。
{label}
占位符:表示从config-client
链接上来的master
的 spring.cloud.config.label
属性的值。
虽然在实际开发中,配置文件一般都是放在 Git 仓库中,但是,config-server
也支持将配置文件放在
classpath
下。
在 config-server
中添加如下配置:
# 表示让 config-server 从 classpath 下查找配置,而不是去 Git 仓库中查找
#若未生效则可以先删除本地下载的文件
spring.profiles.active=native
也可以在 config-server
中,添加如下配置,表示指定配置文件的位置:
spring.cloud.config.server.native.search-locations=file:/E:/properties/
11.2 配置文件加解密
11.2.1 常见加解密方案
不可逆加密:不可逆加密,就是理论上无法根据加密后的密文推算出明文。一般用在密码加密上,常见的算法如
MD5 消息摘要算法、SHA 安全散列算法。
可逆加密:根据加密后的密文推断出明文的加密方式,可逆加密一般又分为两种:
- **对称加密:**对称加密指加密的密钥和解密的密钥是一样的。常见算法des、3des、aes
- **非对称加密:**加密的密钥和解密的密钥不一样,加密的叫做公钥,可以告诉任何人,解密的叫做私 钥,只有自己知道。常见算法 RSA。
11.2.2 对称加密
首先下载不限长度的 JCE:https://www.oracle.com/java/technologies/javase-jce8-downloads.html
将下载的文件解压,解压出来的 jar
拷贝到 Java
安装目录中:C:\java\jdk1.8.0_191\jre\lib\security
然后,在 config-server 新增 bootstrap.properties
配置文件中,添加如下内容配置密钥:
#密钥
encrypt.key=louchen
然后,启动 config-server ,访问如下地址,查看密钥配置是否OK:GET请求,http://localhost:8081/encrypt/status
{
"status": "OK"
}
然后,访问:http://localhost:8081/encrypt ,注意这是一个 POST
请求,访问该地址,可以对一段明文进行加密。这里之前client1-dev.properties
中的值为lc=devupdate
,把对应的devupdate
明文加密后,重新把得到的密文赋值给lc={cipher}ee97bbac745a0778e0cdcc92a8d2615a7e752881016eb66081490caba72d0737
然后提交到 Git 仓库中
注意:加密后的密文前面需要加标识
{cipher}
,表示这是一个密文
我们重新访问 config-client
中的 http://localhost:8082/hello,发现自动解密成功。
11.2.3 非对称加密
非对称加密需要我们首先生成一个密钥对。
使用jdk
自带的工具,在命令行执行如下命令,生成 keystore
:
keytool -genkeypair -alias config-server -keyalg RSA -keystore D:\config-server.keystore
表示使用keytool生成密钥对,别名为
config-server
,算法为RSA
。生成的密钥存在D:\
盘下
这里我们输入密钥口令为:111111
若命令出现乱码则:
chcp 936
命令执行完成后,拷贝生成的 keystore
文件到config-server
的 resources
根目录下。
然后在 config-server
的 bootstrap.properties
目录中,添加如下配置:
#=================非对称密钥配置
#密钥位置
encrypt.key-store.location=config-server.keystore
#别名
encrypt.key-store.alias=config-server
encrypt.key-store.password=111111
encrypt.key-store.secret=111111
在 pom.xml
的build
节点中,添加如下配置,防止keystore
文件被过滤掉。
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.keystore</include>
</includes>
</resource>
</resources>
重启 config-server
,测试方法与对称加密一致。
请求:GET http://localhost:8081/encrypt/status 表示状态ok
{
"status": "OK"
}
请求:POST http://localhost:8081/encrypt 携带参数:dev666
得出的密文为:AQA7OOqt0LqttFO1P4UlfEnpyqSpwVrQ5Eo/LBmaPHv0jrGFkmtY2NFZqgHhM2kMmIkaCylFTFBurOKOZRDCRIdrUtjxTsOBhGPBkZwsJbpIZPASJMvMgznReTtSXpatBiTmNUsNfcwnyK4b6QC2Kzd4dHWSzdHuU9OKKmbpju1BPgsv/p8s57vKn95DhIJAjSSRNdC2hzvzXvky8v7sB4FWWSagxSZy5jAJ3yHCLJUS8OjpO3yV1a0m6eK5/tDA0kQYcs+Tf4/GsSUxSj9GQzzC4l5sh8A+sv5nayq536pCjWd06+KLoZcgywyOyEHkgyIyjXL4R3HTjmM0wfgy/2l3QNadA4/OTIqJhUcaWUmhTqlY2mAWVDhE7VaYo0jumWs=
将密文替换为 cilent1-dev.properties
中的lc=dev666
的明文然后上传到git仓库
。
lc={cipher}AQA7OOqt0LqttFO1P4UlfEnpyqSpwVrQ5Eo/LBmaPHv0jrGFkmtY2NFZqgHhM2kMmIkaCylFTFBurOKOZRDCRIdrUtjxTsOBhGPBkZwsJbpIZPASJMvMgznReTtSXpatBiTmNUsNfcwnyK4b6QC2Kzd4dHWSzdHuU9OKKmbpju1BPgsv/p8s57vKn95DhIJAjSSRNdC2hzvzXvky8v7sB4FWWSagxSZy5jAJ3yHCLJUS8OjpO3yV1a0m6eK5/tDA0kQYcs+Tf4/GsSUxSj9GQzzC4l5sh8A+sv5nayq536pCjWd06+KLoZcgywyOyEHkgyIyjXL4R3HTjmM0wfgy/2l3QNadA4/OTIqJhUcaWUmhTqlY2mAWVDhE7VaYo0jumWs=
启动config-client
中的 http://localhost:8082/hello 接口,
结果为:dev666
11.3 安全管理
防止用户直接通过访问 config-server
看到配置文件内容,我们可以用 spring security
来保护 config- server
接口。
首先在 config-server
中添加spring security
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加完依赖后,config-server
中的接口就自动被保护起来了。
默认自动生成的密码不好记,所以我们可以在config-server
中,自己配置用户名密码。
在 config-server
的bootstrap.properties
配置文件中,添加如下配置,固定用户名密码
spring.security.user.name=louchen
spring.security.user.password=123456
然后,这时config-client
还不能访问config-server
,所以需要在 config-client
的bootstrap.properties
配置文件中,添加如下配置:
spring.cloud.config.username=louchen
spring.cloud.config.password=123456
11.4 服务中心化
前面的配置都是直接在 config-client
中写死 config-server
的地址。
首先启动 Eureka
。
然后,为了让 config-server
和 config-client
都能注册到Eureka
,给它俩添加如下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
然后,分别把config-server
和config-client
都注册到eureka注册中心上,修改config-server
中的 application.properties
配置文件:
#将服务注册到注册中心上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
修改config-client
中bootstrap.properties
文件
#下面三个配置 分别对应 config-server 中的 {application}、{profile}以及{label}占位符
spring.application.name=client1
spring.cloud.config.profile=dev
spring.cloud.config.label=master
#config server服务所在的地址
#spring.cloud.config.uri=http://localhost:8081
server.port=8082
#注册到注册中心上
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
#开启服务发现
spring.cloud.config.discovery.enabled=true
#配置config-server服务(从eureka注册中心获取服务)
spring.cloud.config.discovery.service-id=config-server
spring.cloud.config.username=louchen
spring.cloud.config.password=123456
注意:加入
eureka client
之后,启动config-server
可能会报错,此时,我们重新生成一个jks
格式的密钥。
keytool -genkeypair -alias mytestkey -keyalg RSA -keypass 111111 -keystore D:\config-service.jks -storepass 111111
生成之后,拷贝到configserver
的 resources
目录下,同时修改 bootstrap.properties
配置。
#对称密钥配置
#encrypt.key=louchen
#=================非对称密钥配置
#密钥位置
encrypt.key-store.location=config-service.jks
#别名
encrypt.key-store.alias=mytestkey
encrypt.key-store.password=111111
encrypt.key-store.secret=111111
spring.security.user.name=louchen
spring.security.user.password=123456
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.keystore</include>
<include>**/*.jks</include>
</includes>
</resource>
</resources>
然后我们依次启动:eureka
、config-server
、config-client
然后访问:config-client
中的hello
接口,即可访问对应的配置文件
11.5 配置文件动态刷新
当配置文件发生变化之后,config-server
可以及时感知到变化,但是 config-client
不会及时感知到变化,默认情况下,config-client
只有重启才能加载到最新的配置文件。
首先给config-client
添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后,在bootstrap.properties
添加配置,使 refresh
端点暴露出来:
management.endpoints.web.exposure.include=refresh
最后,再给 config-client
使用了配置文件的地方加上@RefreshScope
注解,这样,当配置改变后,只需要调用 refresh 端点
,config-client
中的配置就可以自动刷新。
@RefreshScope
@RestController
public class HelloContorller {
@Value("${lc}")
String str;
@GetMapping("/hello")
public String sayHello() {
return str;
}
}
重启 config-client
,以后,只要配置文件发生变化,发送 POST 请求,调用config-cilent
的 http://localhost:8082/actuator/refresh 接口即可
然后再调用我们的http://localhost:8082/hello接口,请求的配置文件的数据就是修改后的数据。
11.6 请求失败重试
config-client
在调用config-server
时,一样也可能发生请求失败的问题,这个时候,我们可以配置一个请求重试的功能。
要给 config-client
添加重试功能,只需要添加如下依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
然后,修改bootstrap.properties
配置文件,开启失败快速响应
# 开启失败快速响应
spring.cloud.config.fail-fast=true
然后,注释掉配置文件的用户名密码,重启 config-client
,测试重试是否生效,此时加载配置文件失败,就会自动重试。也是在bootstrap.properties
中通过如下配置保证服务的可用性:
#请求重试的初始时间间隔
spring.cloud.config.retry.initial-interval=1000
#最大重试次数
spring.cloud.config.retry.max-attempts=6
#重试时间间隔乘数
spring.cloud.config.retry.multiplier=1.1
#最大间隔时间
spring.cloud.config.retry.max-interval=2000
12、Spring Cloud Bus
12.1 多个微服务配置文件批量刷新
Spring Cloud Bus
通过轻量级的消息代理连接各个微服务,可以用来广播配置文件的更改,或者管理服务监控。
这里是使用到了RabbitMQ消息队列,修改配置文件后,向
config-server
发送请求->
再由config-server
向RabbitMQ
消息队列发送请求->
再由RabbitMQ消息队列通知到各个config-cilent
首先给 config-server
和 config-client
分别加上 Spring Cloud Bus
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
同时,由于 config-server
将提供刷新接口,所以给 config-server
加上actuator
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后,给两个分别配置使之都连接到 RabbitMQ
上:
spring.rabbitmq.host=47.96.141.44
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=
然后在 config-server
中,添加开启bus-refresh
端点:
management.endpoints.web.exposure.include=bus-refresh
由于给 config-server
中的所有接口都添加了保护,所以刷新接口将无法直接访问,此时,可以通过修改 Security
配置,对端点的权限做出修改:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
//开启basic认证
.httpBasic()
.and()
.csrf().disable();
}
}
在这段配置中,开启了 HttpBasic
登录,这样,在发送刷新请求时,就可以直接通过 HttpBasic
配置认证信息了。
先启动eureka
注册中心,然后分别启动 config-server
和 config-client
我们首先访问 http://localhost:8082/hello 发现结果client1-test.properties
中的内容为 test111
然后修改配置信息提交到 GitHub,刷新 config-client
接口,查看是否有变化。
需要向config-server
发送 POST 请求:http://localhost:8081/actuator/bus-refresh
发送请求成功后,我们再次请求config-client
中的hello
接口:http://localhost:8082/hello 发现配置信息已经更新
12.2 单个微服务配置文件刷新
如果更新配置文件之后,不希望每一个微服务都去刷新配置文件,那么可以通过如下配置解决问题。
首先,给每一个 config-client
添加一个 instance-id:
eureka.instance.instance-id=${spring.application.name}:${server.port}
然后,对 config-client
进行打包。
打包完成后,通过如下命令启动两个config-client
实例:
java -jar config-cilent-0.0.1-SNAPSHOT.jar --server.port=8082
java -jar config-cilent-0.0.1-SNAPSHOT.jar --server.port=8083
修改配置文件,并且提交到 GitHub 之后,可以通过如下方式只刷新某一个微服务,例如只刷新 8082
的服务。
向client-server
发送 POST http://localhost:8081/actuator/bus-refresh/client1:8082
client1:8082
表示服务的 instance-id
(即spring.application.name=client1)
13、Spring Cloud Stream
13.1 概念
Spring Cloud Stream
用来构建消息驱动的微服务。
Spring Cloud Stream
中,提供了一个微服务和消息中间件之间的一个粘合剂,这个粘合剂叫做Binder
,Binder
负责与消息中间件进行交互。而我们开发者则通过inputs
或者 outputs
这样的消息通道与 Binder
进行交互。
13.2 简单示例
创建一个 Spring Cloud Stream
项目,添加三个依赖,web
、rabbitmq
、cloud stream
:
项目创建成功后,添加RabbitMQ
的基本配置信息:
spring.rabbitmq.host=47.96.141.44
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=
接下来,创建一个简单的消息接收器:
//@EnableBinding 表示绑定 Sink 类型消息通道 Sink是系统自定义的默认的消息通道
@EnableBinding(Sink.class)
public class MsgReceiver {
private static final Logger logger= LoggerFactory.getLogger(MsgReceiver.class);
@StreamListener(Sink.INPUT)
public void receiver(Object payload) {
logger.warn("receiver消息:"+payload);
}
}
启动 stream 项目,在控制台打印如下内容,表示创建成功。
2021-02-15 15:02:46.437 INFO 12816 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#b2f4ece:0/SimpleConnection@2f84acf7 [delegate=amqp://guest@47.96.141.44:5672/, localPort= 18952]
在 rabbitmq 后台管理页面去发送一条消息:
在项目中的我们可以看到控制台打印的内容:
2021-02-15 15:04:01.040 WARN 12816 --- [hO3JolBlSYc-w-1] org.lc.cloudstream.MsgReceiver : receiver消息:louchen
13.3 自定义消息通道
首先创建一个名为 MyChannel
的接口:
public interface MyChannel {
//注意:以下两个消息通道名称不一样,所以 输入 和 输出 通道不能直接通信,需要在properties配置文件中配置
String INPUT = "lc-input";
String OUTPUT = "lc-output";
@Output(OUTPUT)
MessageChannel output();
@Input(INPUT)
SubscribableChannel input();
}
- 注意,两个消息通道的名字是不一样的
- 从 F 版开始,默认使用通道的名称作为实例命令,所以这里的通道名字不可以相同(早期版本可以相同),这样的话,为了能够正常收发消息,需要我们在
application.properties
中做一些额外配置。#只需将不同的通道绑定到相同的名称上即可 spring.cloud.stream.bindings.lc-input.destination=lc-topic spring.cloud.stream.bindings.lc-output.destination=lc-topic
接下来,自定义一个消息接收器,用来接收自己的消息通道里的消息:
//绑定自定义的消息通道
@EnableBinding(MyChannel.class)
public class MsgReceiver2 {
private static final Logger logger= LoggerFactory.getLogger(MsgReceiver2.class);
@StreamListener(MyChannel.INPUT)
public void receiver(Object payload) {
logger.info("redeiver2消息:"+payload);
}
}
再定义一个 HelloController
进行测试:
@RestController
public class HelloController {
@Autowired
private MyChannel myChannel;
@GetMapping("/hello")
public void hello() {
//发送消息
myChannel.output().send(MessageBuilder.withPayload("hello cloud stream!").build());
}
}
我们可以看到项目控制台的打印日志:
2021-02-15 15:29:19.079 INFO 1444 --- [qqjApa1wZjU7w-1] org.lc.cloudstream.MsgReceiver2 : redeiver2消息:hello cloud stream!
13.4 消息分组
默认情况下,如果消费者是一个集群,此时,一条消息会被多次消费。通过消息分组,我们可以解决这个问题。
例如,我们启动多个实例,那么一个实例的消息发送到消息中间件,那么多个实例都会收到消息。
此时我们修改配置文件
#只需将不同的通道绑定到相同的组名称上即可
spring.cloud.stream.bindings.lc-input.group=group1
spring.cloud.stream.bindings.lc-output.group=group1
这样我们即使启动了多个实例,我们的消息也只会发送到一个实例
13.5 消息分区
通过消息分区可以实现相同特征的消息总是被同一个实例处理。只需要添加如下配置即可:
生产者端配置:
#当前消息被下标为【1】的消费者消费 (生产者)
spring.cloud.stream.bindings.lc-output.producer.partition-key-expression=1
#消费端的节点数量(生产者)
spring.cloud.stream.bindings.lc-output.producer.partition-count=2
消费者端配置:
#开启消息分区(消费者)
spring.cloud.stream.bindings.lc-input.consumer.partitioned=true
#消费者实例个数(消费者)
spring.cloud.stream.instance-count=2
#当前消费者实例的下标(消费者,动态修改)
spring.cloud.stream.instance-index=0
接下来启动两个实例:
java -jar cloud-stream-0.0.1-SNAPSHOT.jar --server.port=8080 --spring.cloud.stream.instance-index=0
java -jar cloud-stream-0.0.1-SNAPSHOT.jar --server.port=8081 --spring.cloud.stream.instance-index=1
我们请求/hello
接口发送消息发现只有 8081
端口的程序收到消息,因为 我们在配置文件中配置的消息只能被生产者端配置的实例下标为1
的消费者消费
13.6 延时任务
每天定时执行的任务,可以使用 cron
表达式,有一种比较特殊的定时任务,例如几分钟后执行,这种可以结合 Spring Cloud Stream
+RabbitMQ
来实现
这个需要首先下载一个 rabbitmq 插件:https://dl.bintray.com/rabbitmq/community-plugins/3.7.x/rabbitmq_delayed_message_exchange/rabbitmq_delayed_message_exchange-20171201-3.7.x.zip
执行如下命令:
#解压插件
unzip rabbitmq_delayed_message_exchange-20171201-3.7.x.zip
# 将解压后的文件,拷贝到 Docker 容器中
docker cp /root/springcloud/rabbitmq_delayed_message_exchange-20171201-3.7.x.ez rabbitmq02:/plugins
# 进入到容器中
docker exec -it rabbitmq02 /bin/bash
# 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 查看是否启用成功
rabbitmq-plugins list
若启动错误或者安装插件错误则可能是
rabbitmq_delayed_message_exchange
的版本和RabbitMQ
的版本不一致的问题,需要修改对应的版本才行。本案例中,用到了
rabbitmq_delayed_message_exchange 3.7
的版本和RabbitMQ 3.7.23
的版本
配置文件中,开启消息延迟功能:
#开启消息延迟功能
spring.cloud.stream.rabbit.bindings.lc-input.consumer.delayed-exchange=true
spring.cloud.stream.rabbit.bindings.lc-output.producer.delayed-exchange=true
这里还要修改通道的目标必须取
的相同的名称:
spring.cloud.stream.bindings.lc-input.destination=delay_msg
spring.cloud.stream.bindings.lc-output.destination=delay_msg
然后在消息发送时,设置消息延迟时间为 3 秒
:
@RestController
public class HelloController {
private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
@Autowired
private MyChannel myChannel;
@GetMapping("/hello")
public void hello() {
logger.warn("send msg:"+new Date());
myChannel.output(). send(MessageBuilder.withPayload("hello cloud stream!").setHeader("x-delay", 3000).build());
}
}
增加接收消息的时间:
//绑定自定义的消息通道
@EnableBinding(MyChannel.class)
public class MsgReceiver2 {
private static final Logger logger= LoggerFactory.getLogger(MsgReceiver2.class);
@StreamListener(MyChannel.INPUT)
public void receiver(Object payload) {
logger.info("redeiver message2:"+payload+new Date());
}
}
最后启动程序,查看日志:
2021-02-15 20:06:30.511 WARN 21776 --- [nio-8080-exec-1] org.lc.cloudstream.HelloController : send msg:Mon Feb 15 20:06:30 GMT+08:00 2021
2021-02-15 20:06:33.994 INFO 21776 --- [VSy4wIId7RYPw-1] org.lc.cloudstream.MsgReceiver2 : redeiver message2:hello cloud stream!Mon Feb 15 20:06:33 GMT+08:00 2021
我们可以发现,发送消息和接收消息中级间隔了3秒。
14、Spring Cloud Sleuth 链路追踪
14.1 简介
在这种大规模的分布式系统中,一个完整的系统是由很多种不同的服务来共同支撑的。不同的系统可能 分布在上千台服务器上,横跨多个数据中心。一旦系统出问题,此时问题的定位就比较麻烦。
分布式链路追踪:
在微服务环境下,一次客户端请求,可能会引起数十次、上百次服务端服务之间的调用。一旦请求出问 题了,我们需要考虑很多东西:
- 如何快速定位问题
- 如果快速确定此次客户端调用,都涉及到哪些服务
- 到底是哪一个服务出问题了
要解决这些问题,就涉及到分布式链路追踪。
分布式链路追踪系统主要用来跟踪服务调用记录的,一般来说,一个分布式链路追踪系统,有三个部分:
- 数据收集
- 数据存储
- 数据展示
Spring Cloud Sleuth
是 Spring Cloud
提供的一套分布式链路追踪系统。
trace:从请求到达系统开始,到给请求做出响应,这样一个过程成为 trace
span: 每 次 调 用 服 务 时 , 埋 入 的 一 个 调 用 记 录 , 成 为 span
annotation:相当于 span 的语法,描述 span 所处的状态。
14.2 简单应用
首先创建一个项目,引入 Spring Cloud Sleuth
接下来创建一个 HelloController
,打印日志测试:
@RestController
public class HelloController {
private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
@GetMapping("/hello1")
public String hello1() {
logger.info("hello1 logs...");
return "hello1";
}
}
启动应用,请求 /hello1
接口,结果如下:
2021-02-16 13:05:10.195 INFO [sleuth,acdce5ad2a3102b0,acdce5ad2a3102b0,true] 16228 --- [nio-8080-exec-1] org.lc.sleuth.HelloController : hello1 logs...
这个就是 Spring Cloud Sleuth
的输出。
再定义两个接口,在 hello2
中调用hello3
,形成调用链:
@SpringBootApplication
public class SleuthApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
public class HelloController {
private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello1")
public String hello1() {
logger.info("hello1 logs...");
return "hello1";
}
@GetMapping("/hello2")
public String hello2() {
logger.info("hello2 logs...");
return restTemplate.getForObject("http://localhost:8080/hello3", String.class);
}
@GetMapping("/hello3")
public String hello3() {
logger.info("hello3 logs...");
return "hello3";
}
}
此时,访问 hello2,会先调用 hello3,拿到返回结果,会给 hello2。
2021-02-16 13:13:47.473 INFO [sleuth,0425c43079621665,0425c43079621665,true] 25372 --- [nio-8080-exec-1] org.lc.sleuth.HelloController : hello2 logs... 2021-02-16 13:13:47.574 INFO [sleuth,0425c43079621665,73c7d57f237adf05,true] 25372 --- [nio-8080-exec-2] org.lc.sleuth.HelloController : hello3 logs...
第一个值为当前程序的名称:sleuth
这里是一个调用链,所以trance相同:0425c43079621665
一个调用链中多个span调用请求组成,所以 span不同,分别为:0425c43079621665
,73c7d57f237adf05
一个 trace
由多个span
组成,一个trace
相当于就是一个调用链,而一个 span
则是这个链中的每一次调用过程。
14.3 异步任务信息收集
14.3.1 异步任务
Spring Cloud Sleuth
中也可以收集到异步任务中的信息。
开启异步任务:
@SpringBootApplication
//开启异步任务
@EnableAsync
public class SleuthApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
创建一个 HelloService
,提供一个异步任务方法:
@Service
public class HelloService {
private static final Logger logger= LoggerFactory.getLogger(HelloService.class);
@Async
public String asyncHello() {
logger.info("asyncHelloMethod logs...");
return "asyncHelloMethod...";
}
}
在HelloController中添加如下方法:
@GetMapping("/hello4")
public String hello4() {
logger.info("hello4 logs...");
return helloService.asyncHello();
}
启动项目进行测试,发现 Sleuth
也打印出日志了,在异步任务中,异步任务是单独的 spanid
。
2021-02-16 13:33:45.561 INFO [sleuth,5a6d545fc36d1e60,5a6d545fc36d1e60,true] 23064 --- [nio-8080-exec-1] org.lc.sleuth.HelloController : hello4 logs... 2021-02-16 13:33:45.569 INFO [sleuth,5a6d545fc36d1e60,7c9ef0385ca20312,true] 23064 --- [ task-1] org.lc.sleuth.HelloService : asyncHelloMethod logs...
14.3.2 定时任务
Spring Cloud Sleuth
也可以收集定时任务的信息。
首先开启定时任务支持:
@SpringBootApplication
//开启异步任务
@EnableAsync
//开启定时任务支持
@EnableScheduling
public class SleuthApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
然后在HelloSerivce
中,添加定时任务,去调用 asyncHello
方法。
@Service
public class HelloService {
private static final Logger logger= LoggerFactory.getLogger(HelloService.class);
@Async
public String asyncHello() {
logger.info("asyncHelloMethod logs...");
return "asyncHelloMethod...";
}
@Scheduled(cron = "0/10 * * * * ?")
public void schMethod() {
logger.info("start logs...");
asyncHello();
logger.info("end logs....");
}
}
在定时任务中,每一次定时任务都会产生一个新的 Trace
,并且在调用过程中,SpanId
都是一致的,这个和普通的调用不一样。
14.4 Zipkin
14.4.1 简介
Zipkin
本身是一个由 Twitter
公司开源的分布式追踪系统。
Zipkin
分为 server
端和 client
端,server
用来展示数据,client
用来收集+上报数据。
14.4.2 Zipkin安装
es 安装命令:
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.1.0
可视化工具有三种安装方式:
直接下载软件安装
通过 Docker 安装
安装 Chrome/Firefox 插件
这里采用第 3 种方式。
RabbitMQ 安装
Zipkin 安装:
docker run -d -p 9411:9411 --name zipkin -e ES_HOSTS=47.96.141.44 -e STORAGE_TYPE=elasticsearch -e ES_HTTP_LOGGING=BASIC -e RABBIT_URI=amqp://guest:密码@47.96.141.44:5672 openzipkin/zipkin
ES_HOSTS
:es 的地址STORAGE_TYPE
:数据存储方式RABBIT_URI
:要连接的 Rabbit 的地址
14.4.3 简单使用
首先来创建一个 Zipkin
项目,添加web、sleuth、zipkin、rabbitmq、stream
:
项目创建好之后,配置 zipkin
和 rabbitmq
:
# 应用名称
spring.application.name=zinkin01
# 应用服务 WEB 访问端口
server.port=8080
#开启链路追踪
spring.sleuth.web.client.enabled=true
#配置采样比例,默认为0.1 (即不是所有消息都收集,这里的比例为10%)
spring.sleuth.sampler.probability=1
#zipkin地址
spring.zipkin.base-url=http://47.96.141.44:9411
#开启zipkin
spring.zipkin.enabled=true
#追踪消息的发送类型
spring.zipkin.sender.type=rabbit
#rabbitmq配置
spring.rabbitmq.host=192.168.91.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=
接下来提供一个测试的 HelloController
:
@RestController
public class HelloController {
private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
@GetMapping("/hello")
public String hello1(String name) {
logger.info("zipkin01 hello1 logs:"+name+"...");
return "zipkin01-hello1:"+name;
}
}
然后再创建一个 zipkin02
,和 zipkin01
的配置基本一致。
# 应用名称
spring.application.name=zipkin02
# 应用服务 WEB 访问端口
server.port=8081
#开启链路追踪
spring.sleuth.web.client.enabled=true
#配置采样比例,默认为0.1 (即不是所有消息都收集,这里的比例为10%)
spring.sleuth.sampler.probability=1
#zipkin地址
spring.zipkin.base-url=http://47.96.141.44:9411
#开启zipkin
spring.zipkin.enabled=true
#追踪消息的发送类型
spring.zipkin.sender.type=rabbit
#rabbitmq配置
spring.rabbitmq.host=192.168.91.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=
@SpringBootApplication
public class Zipkin02Application {
public static void main(String[] args) {
SpringApplication.run(Zipkin02Application.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
这里我们使用使用zipkin02
项目中的hello请求
去调用zipkin01
项目中的hello请求
@RestController
public class HelloContorller {
private static final Logger logger= LoggerFactory.getLogger(HelloContorller.class);
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public String hello1() {
logger.info("zipkin02 hello1 logs...");
return restTemplate.getForObject("http://localhost:8080/hello?name={1}", String.class, "louchen");
}
}
我们访问zipkin
可视化界面:
http://47.96.141.44:9411/zipkin
这里的调用过程是:
接口中的日志通过Stream
发送到RabbitMQ
,再由RabbitMQ
将消息发送到ElasticSearch
,我们的zipkin
客户端会通过从服务器上的zipkin
服务端从ElasticSearch
读取数据从而实现数据的展示。
15、Spring Cloud Alibaba
15.1 简介
Spring Cloud Alibaba
是阿里巴巴提供的一套微服务开发一站式解决方案。主要提供的功能:
- 服务限流降级
- 服务注册与发现
- 分布式配置中心
- 消息驱动
- 分布式事务
====>以下组件需要和阿里云绑定
- 阿里云对象存储
- 阿里云短信
提供的组件:
- Sentinel
- Nacos
优势:
- 中文文档
- 没有另起炉灶,可以方便的集成到现有项目中
- 阿里本身在高并发、高性能上的经验,让我们有理由相信这些组件足够可靠。
15.2 Nacos
15.2.1 Nacos简介
Nacos
主要提供了服务发现、服务配置以及服务管理。相当于SpringCloud中的eureka和consul
基本特性:
- 服务发现
- 动态配置
- 动态DNS服务
- 服务及数据管理
15.2.2 安装
- Docker安装
- 下载源码自己编译安装/下载编译好的安装包
首先下载安装包https://github.com/alibaba/nacos/releases/download/1.2.0-beta.1/nacos-server-1.2.0-beta.1.tar.gz
然后解压,注意,系统一定要配置好 jdk,测试一下java 和 javac 两个命令要存在。
如果 win,直接在 bin 目录下双击startup.cmd
启动。
如果Linux,bin 目录下执行 sh startup.sh -m standalone
#解压
tar -zxvf nacos-server-1.2.0-beta.1.tar.gz
#切换到bin目录启动
sh startup.sh -m standalone
有以下内容表示启动成功:
nacos is starting with standalone nacos is starting,you can check the /root/springcloud/nacos/logs/start.out
Nacos
启动成功后,浏览器输入:http://47.96.141.44:8848/nacos 就能看到启动页面。
如果有登录页面,登录的默认用户名/密码都是 nacos
。
15.2.3 Nacos作配置中心
首先在服务端配置,点击配置管理->配置列表->+
这里主要配置三个东西,Data ID
、Group
以及要配置的内容。
Data Id
的格式是:${prefix}-${spring.profile.active}.${file-extension}
${prefix}
的值,默认为spring.application.name
的值${spring.profile.active}
表示项目当前所处的和环境${file-extension}
表示配置文件的扩展名
首先在nacos上新建一个配置文件:
Data ID: nacos.properties
Group:DEFAULT_GROUP
内容:name=lc
配置完成后,新建 Spring Boot
项目,加入 Nacos
依赖
然后,新建 bootstrap.properties
配置文件,配置nacos
信息:
#nacos地址
spring.cloud.nacos.server-addr=47.96.141.44:8848
#配置文件扩展名
spring.cloud.nacos.config.file-extension=properties
在application.properties
配置文件中,配置应用程序信息
# 应用名称
spring.application.name=nacos
# 应用服务 WEB 访问端口
server.port=8080
提供一个测试 HelloController
:
//配置文件动态刷新
@RefreshScope
@RestController
public class HelloController {
@Value("${name}")
public String name;
@GetMapping("/hello")
public String hello() {
return name;
}
}
这时访问 hello接口
:http://localhost:8080/hello
结果:lc
这时修改nacos
中的配置文件中 name=louchen
保存发布即可。
我们再次访问 hello接口
:http://localhost:8080/hello
结果:louchen
nacos
配置文件中的更改的内容已经生效。
15.2.4 Nacos作注册中心
Nacos
做注册中心,可以代替Eureka
创建 Spring Boot
项目,添加依赖:
生产者配置:
添加配置:
# 应用名称
spring.application.name=nacos01
# 应用服务 WEB 访问端口
server.port=8080
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
spring.cloud.nacos.discovery.server-addr=47.96.141.44:8848
然后再提供一个测试 HelloController
:
@RestController
public class HelloController {
@Value("${server.port}")
String port;
@GetMapping("/hello")
public String hello() {
return "port:"+port;
}
}
再将项目打包,启动两个实例:
java -jar nacos01-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar nacos01-0.0.1-SNAPSHOT.jar --server.port=8081
我们查看nacos界面:
查看详情:
消费者配置:
依赖和生产者配置一致
@SpringBootApplication
public class Nacos02Application {
public static void main(String[] args) {
SpringApplication.run(Nacos02Application.class, args);
}
//使用负载均衡
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
application.properties
文件配置:
# 应用名称
spring.application.name=nacos02
# 应用服务 WEB 访问端口
server.port=8082
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
spring.cloud.nacos.discovery.server-addr=47.96.141.44:8848
增加HelloController
配置:
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public String hello() {
return restTemplate.getForObject("http://nacos01/hello", String.class);
}
}
访问消费者接口:http://localhost:8082/hello
结果为:port:8080
和 port:8081
交替执行,实现负载均衡
nacos界面:
15.3 Sentinel
15.3.1 简介
- 使用场景丰富
- 有完备的实时监控
- 广泛的开源生态
Sentinel
整体上可以分为两个核心部分:
- 核心库
- 控制台
15.3.2 安装
首先下载控制台 jar
,这是一个 Spring Boot
工程,下载好之后,直接用 Spring Boot
启动命令启动它即可。
下载地址:https://github.com/alibaba/Sentinel/releases/download/1.8.1/sentinel-dashboard-1.8.1.jar
启动:jar -jar sentinel-dashboard-1.8.1.jar
默认用户名密码为:sentinel
15.3.3 应用
创建一个新的 Spring Boot 工程,添加 Sentinel 依赖。
项目创建成功后,配置sentinel
控制台地址:
# 应用名称
spring.application.name=sentinel
# 应用服务 WEB 访问端口
server.port=8081
# Sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
再创建一个测试 HelloController
。
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello sentinel";
}
}
启动项目访问hello接口
然后可以看到在Sentinel的可视化界面看到请求的调用。
这里我们还可以设置/hello接口的流控规则
,每秒处理5个请求,其余的请求等待超时时间为1s
测试设置的流控规则是否生效:
@Test
void contextLoads() {
RestTemplate restTemplate = new RestTemplate();
//模拟连续发生20次请求
for (int i = 0; i < 20; i++) {
String forObject = restTemplate.getForObject("http://localhost:8081/hello", String.class);
System.out.println(forObject+":"+new Date());
}
}
查看日志结果可以发现,每秒处理了5
个请求:
hello sentinel:Wed Feb 17 18:07:42 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:42 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:42 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:42 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:42 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:43 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:43 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:43 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:43 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:43 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:44 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:44 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:44 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:44 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:44 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:45 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:45 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:45 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:45 GMT+08:00 2021
hello sentinel:Wed Feb 17 18:07:45 GMT+08:00 2021
15.3.4 在Nacos中配置流控规则
首先需要将Nacos
作为配置中心,需要将Nacos Config
依赖加到Sentinel
项目中
注意:这里的<spring-cloud-alibaba.version>2.2.0.RELEASE</spring-cloud-alibaba.version> 版本如果升级到2.2.2则会报错
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.7.1</version>
</dependency>
bootstrap.properties
配置:
#配置nacos地址
spring.cloud.nacos.config.server-addr=47.96.141.44:8848
application.properties
配置:
# 应用名称
spring.application.name=sentinel
# 应用服务 WEB 访问端口
server.port=8081
# Sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
#配置在nacos上的数据源信息
spring.cloud.sentinel.datasource.ds.nacos.server-addr=47.96.141.44:8848
spring.cloud.sentinel.datasource.ds.nacos.data-id=sentinel
spring.cloud.sentinel.datasource.ds.nacos.group-id=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds.nacos.rule-type=flow
然后在nacos
中配置流控规则文件:
然后访问http://localhost:8081/hello接口
然后再访问Sentinel
地址:http://localhost:8080,我们可以发现/hello
接口的规则已经生成