SpringCloud
Spring Cloud为开发人员提供了工具,以快速构建分布式系统中的某些常见模式(例如,配置管理,服务发现,断路器,智能路由,微代理,控制总线,一次性令牌,全局锁,领导选举,分布式会话,群集状态)。分布式系统的协调导致了样板式样,并且使用Spring Cloud开发人员可以快速支持实现这些样板的服务和应用程序。它们可以在任何分布式环境中很好地工作,包括开发人员自己的笔记本电脑,裸机数据中心以及Cloud Foundry等托管平台。
特征
Spring Cloud专注于为典型的用例和可扩展性机制(包括其他用例)提供良好的现成体验。
- 分布式/版本控制配置
- 服务注册和发现
- 路由
- 服务到服务的调用
- 负载均衡
- 断路器
- 分布式消息管理
- 全局锁
- 领导选举和集群状态
项目搭建
0.创建数据库
数据库名:db01
表名:dept
表中数据:
1.创建父工程
建一个空的maven项目
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tom</groupId>
<artifactId>springCloud</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>springcloud-api</module>
<module>springcloun-provider-dept-8001</module>
<module>springcloud-consumer-dept-80</module>
<module>springcloud-eureka-7001</module>
</modules>
<!--打包方式 pom-->
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compile.source>1.8</maven.compile.source>
<maven.compile.target>1.8</maven.compile.target>
<junit.version>4.12</junit.version>
<lombok.version>1.18.12</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springCloud依赖 Greenwich.SR3版本-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
<!--springboot启动器-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--日志测试-->
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2.api模块
新建maven项目
pom文件
<dependencies>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
Dept实体类
package com.tom.springcloud.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@NoArgsConstructor//无参
@Accessors(chain = true)//链式写法
public class Dept implements Serializable {
private Long deptno;
private String dname;
//这个数据存在哪个数据库的字段,一个服务对应一个数据库,同一份信息可能存在不同的数据库
private String db_source;
public Dept(String dname) {
this.dname = dname;
}
/*链式写法
Dept dept = new Dept();
dept.setDeptNO(11).setDname('sss');
*/
}
3.服务提供者模块
pom文件
<dependencies>
<dependency>
<groupId>com.tom</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
</dependencies>
application.yml配置文件
#端口
server:
port: 8001
#mybatis
mybatis:
type-aliases-package: com.tom.springcloud.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
cache-enabled: true
spring:
application:
name: springcloud-provider-dept
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/db01?usrUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
mapper的配置文件DeptMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tom.springcloud.dao.DeptDao">
<insert id="addDept" parameterType="Dept">
insert into dept (dname, db_source) values (#{dname},DATABASE())
</insert>
<select id="queryById" parameterType="Long" resultType="Dept">
select * from dept where deptno = #{id}
</select>
<select id="queryAll" resultType="Dept">
select * from dept
</select>
</mapper>
DeptController.java
package com.tom.springcloud.controller;
import com.tom.springcloud.pojo.Dept;
import com.tom.springcloud.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
//提供restful服务
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
@PostMapping("/dept/add")
public boolean addDept(Dept dept) {
return deptService.addDept(dept);
}
@GetMapping("/dept/get/{id}")
public Dept queryById(@PathVariable("id") Long id) {
return deptService.queryById(id);
}
@GetMapping("/dept/list")
public List<Dept> queryAll() {
return deptService.queryAll();
}
}
启动类
//启动类
@SpringBootApplication
public class DeptProvider_8001 {
public static void main(String[] args){
SpringApplication.run(DeptProvider_8001.class, args);
}
}
启动后访问测试
4.服务消费者模块
pom文件
<dependencies>
<!--实体类和web-->
<dependency>
<groupId>com.tom</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
</dependencies>
application.yml
server:
port: 80
DeptConsumerController.java
使用url请求远端服务
package com.tom.springcloud.controller;
import com.tom.springcloud.pojo.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
public class DeptConsumerController {
//直接用url请求远端服务
//使用RestTemplate...的方法
@Autowired
private RestTemplate restTemplate;//提供多种便捷访问远程http服务的方法,简单的restful服务模板
//http://localhost:8001/dept/get/1
private static final String REST_URL_PREFIX = "http://localhost:8001";
@RequestMapping("/consumer/dept/add")
public boolean add(Dept dept){
return restTemplate.postForObject(REST_URL_PREFIX+"/dept/add",dept,Boolean.class);
}
@RequestMapping("/consumer/dept/get/{id}")
public Dept get(@PathVariable("id") Long id){
return restTemplate.getForObject(REST_URL_PREFIX+"/dept/get/"+id,Dept.class);
}
@RequestMapping("/consumer/dept/list")
public List<Dept> list(){
return restTemplate.getForObject(REST_URL_PREFIX+"/dept/list",List.class);
}
}
注入RestTtemplate
package com.tom.springcloud.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ConfigBean {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
启动类
@SpringBootApplication
public class DeptConsumer_80 {
public static void main(String[] args){
SpringApplication.run(DeptConsumer_80.class,args);
}
}
5.Eureka服务注册与发现
Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。
Eureka包含两个组件:Eureka Server和Eureka Client。
Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。
在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
Eureka Server之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。综上,Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。Eureka百度百科
Eureka服务器
pom依赖
<dependencies>
<!-- eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--Gson冲突-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
</dependencies>
application.yml配置
server:
port: 7001
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false #是否向eureka注册中心注册自己
fetch-registry: false #fetch-registry如果为false,则表示自己为注册中心
service-url: #监控页面
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动类
@SpringBootApplication
@EnableEurekaServer //启动EurekaServer的注解
public class EurekaServer_7001 {
public static void main(String[] args){
SpringApplication.run(EurekaServer_7001.class,args);
}
}
启动后访问7001,还没有注册服务所以可以看到Application栏没有服务。
改造provider
在provider 服务提供者的pom文件中添加Eureka相关的包
<!--eureka依赖 一般只需要这个-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--Actuator完善监控信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.1.14.RELEASE</version>
</dependency>
<!--Gson冲突 如果包Gson冲突的错我们就导入这个包-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
在application.yml配置文件中添加eureka的配置
#Eureka配置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/
instance:
instance-id: springcloud-provider-dept8001 #修改Eureka上的默认描述信息
#info配置,点击微服务能够看到的信息
info:
app.name: tom-springcloud
company.name: www.tom.study
在启动类开启eureka相关的注解
//启动类
@SpringBootApplication
@EnableEurekaClient //在服务启动后自动注册到Eureka中!
@EnableDiscoveryClient //服务发现
public class DeptProvider_8001 {
public static void main(String[] args){
SpringApplication.run(DeptProvider_8001.class, args);
}
}
在确保启动了Eureka服务后再启动DeptProvider_8001,访问eureka页面可以看到provider的服务一进被注册进去了。
到这里eureka的服务注册已经完成了,我们也可以再添加一个能获取注册进来的微服务的消息的方法。
@GetMapping("/dept/discovery")
public Object discovery() {
//获取微服务清单
List<String> services = discoveryClient.getServices();
System.out.println("discoveryClient=>Services:" + services);
//得到具体的微服务信息
List<ServiceInstance> instances = discoveryClient.getInstances("SPRINGCLOUD-PROVIDER-DEPT");
for (ServiceInstance instance : instances) {
System.out.println(
instance.getHost()+"\t"+
instance.getPort()+"\t"+
instance.getUri()+"\t"+
instance.getServiceId()
);
}
return this.discoveryClient;
}
访问 localhost:8001/dept/discovery 如下图:
控制台输出
可以观察到已经获取到注册了的服务列表
6.Eureka集群配置
类似上面的Eureka服务器的搭建,我们可以搭建很多Eureka服务器。这样的话即使一个eureka崩了,其他的依然能够使用。
我们需要配置中关联其他的eureka
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com
client:
register-with-eureka: false #是否向eureka注册中心注册自己
fetch-registry: false #fetch-registry如果为false,则表示自己为注册中心
service-url: #关联其他的eureka
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
然后在provider配置里面修改成把服务注册到集群里面去而不是某一个
#Eureka配置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
7.Ribbon客户端负载均衡工具
Ribbon 是一个基于 HTTP 和 TCP 客户端 的负载均衡的工具。它可以 在客户端 配置 RibbonServerList(服务端列表),使用 HttpClient 或 RestTemplate 模拟 http 请求。
默认算法是轮询
改造consumer
加入ribbon的依赖和eyreka的依赖
<!--ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--eureka依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--Gson冲突-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
application.yml加入eureka的配置
server:
port: 80
eureka:
client:
register-with-eureka: false # 不向eureka注册自己
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
给RestTemplate bean 加一个@LoadBalanced注解,就能让这个RestTemplate在请求时拥有客户端负载均衡的能力
@Configuration
public class ConfigBean {
//配置负载均衡实现RestTemplate
@Bean
@LoadBalanced //Ribbon
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
改造Controller使通过Eureka注册的服务名调用服务。
@RestController
public class DeptConsumerController {
//直接用url请求远端服务
//使用RestTemplate...的方法
@Autowired
private RestTemplate restTemplate;//提供多种便捷访问远程http服务的方法,简单的restful服务模板
//通过ribbon,这里应该是一个变量,通过服务名访问
//http://localhost:8001/dept/get/1
// private static final String REST_URL_PREFIX = "http://localhost:8001";
private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER-DEPT";
@RequestMapping("/consumer/dept/add")
public boolean add(Dept dept){
return restTemplate.postForObject(REST_URL_PREFIX+"/dept/add",dept,Boolean.class);
}
@RequestMapping("/consumer/dept/get/{id}")
public Dept get(@PathVariable("id") Long id){
return restTemplate.getForObject(REST_URL_PREFIX+"/dept/get/"+id,Dept.class);
}
@RequestMapping("/consumer/dept/list")
public List<Dept> list(){
return restTemplate.getForObject(REST_URL_PREFIX+"/dept/list",List.class);
}
}
启动类添加注解
@SpringBootApplication
@EnableEurekaClient
public class DeptConsumer_80 {
public static void main(String[] args){
SpringApplication.run(DeptConsumer_80.class,args);
}
}
如果报java.lang.NoClassDefFoundError: org/apache/http/client/HttpClient错误的话导入HttpClient的包
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
测试localhost/consumer/dept/list能否调用成功
调用成功,但是由于现在只有一个服务提供方,体现不出负载均衡的作用。如果多构建几个SPRINGCLOUD-PROVIDER-DEPT的服务提供方注册到Eureka上的话,每次访问localhost/consumer/dept/list都会使用ribbon的负载均衡算法决定具体去访问哪一个,但是一个机器跑这么多服务的话内存会爆炸,谨慎模拟。
Ribbon负载均衡算法
-
RoundRobinRule: 默认轮询的方式
-
RandomRule: 随机方式
-
WeightedResponseTimeRule: 根据响应时间来分配权重的方式,响应的越快,分配的值越大。
-
BestAvailableRule: 选择并发量最小的方式
-
RetryRule: 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
-
ZoneAvoidanceRule: 根据性能和可用性来选择。
-
AvailabilityFilteringRule: 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值)
Ribbon自定义负载均衡算法
注意:官方文档中有指出,自定义的类不能放在@ComponentScan或SpringBootApplication所扫描的当前包以及子包下,否则自定义的这个配置类将会被所有的Ribbon客户端所共享。
1.可以在启动器的上一级创建一个包存放自定义算法的文件
2.仿照其他自带的负载均衡算法继承AbstractLoadBalancerRule类重写方法。
3.将自定义的负载均衡算法注入到Bean中
4.在启动类中添加Ribbon注解
@RibbonClient(name = “SPRINGCLOUD.PROVIDER-DEPT”,configuration = Rule_Test.class)
8.Feign负载均衡
feign是声明式的web service客户端,它让微服务之间的调用变得更简单了,类似controller调用service。Spring Cloud集成了Ribbon和Eureka,可在使用Feign时提供负载均衡的http客户端。
我们只需要创建一个接口并使用注解的方式来配置它。
改造consumer
添加feign的依赖
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
编写接口@FeignClient注解
DeptClientService
package com.tom.springcloud.service;
import com.tom.springcloud.pojo.Dept;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Component
@FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT")
public interface DeptClientService {
@PostMapping("/dept/add")
public boolean addDept(Dept dept);
@GetMapping("/dept/get/{id}")
public Dept queryById(@PathVariable("id") Long id);
@GetMapping("/dept/list")
public List<Dept> queryAll();
}
注意,FeignClient的value必须是在Eureka注册中心注册的服务名。还有Mapping路径必须和服务提供者的路径一致。
FeignDeptConsumerController (这个类也可以写到api模块里引用过来)。
package com.tom.springcloud.controller;
import com.tom.springcloud.pojo.Dept;
import com.tom.springcloud.service.DeptClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class FeignDeptConsumerController {
//注入接口,调用方法。
@Autowired
private DeptClientService deptClientService;
@PostMapping("/consumer/dept/add")
public boolean addDept(Dept dept){
return deptClientService.addDept(dept);
}
@GetMapping("/consumer/dept/get/{id}")
public Dept queryById(@PathVariable("id") Long id){
return deptClientService.queryById(id);
}
@GetMapping("/consumer/dept/list")
public List<Dept> queryAll(){
return deptClientService.queryAll();
}
}
配置文件
server:
port: 80
eureka:
client:
register-with-eureka: false # 不向eureka注册自己
service-url:
defaultZone: http://localhost:7001/eureka/
#feign
feign:
client:
config:
feignName:
connectTimeout: 5000 #超时
readTimeout: 3000
启动类
package com.tom.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class FeignDeptConsumer_80 {
public static void main(String[] args){
SpringApplication.run(FeignDeptConsumer_80.class,args);
}
}
注意加上@EnableFeignClients注解开启Feign。
测试,依次启动Eureka服务7001,DeptProvider8001,FeignConsumer80。
访问localhost/consumer/dept/list
Hystrix熔断器
Hystrix是什么:
在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现这一点,所有这些都可以提高系统的整体弹性。
Hystrix被设计的目标是:
- 对通过第三方客户端库访问的依赖项(通常是通过网络)的延迟和故障进行保护和控制。
- 在复杂的分布式系统中阻止级联故障。
- 快速失败,快速恢复。
- 回退,尽可能优雅地降级。
- 启用近实时监控、警报和操作控制。
Hystrix能干嘛
- 服务降级
- 服务熔断
- 服务限流
- 接近实时的监控
- …
Hystrix解决了什么问题:
正常情况下,请求是这样的:
当其中有一个系统有延迟时,它可能阻塞整个用户请求:
在高流量的情况下,一个后端依赖项的延迟可能导致所有服务器上的所有资源在数秒内饱和(PS:意味着后续再有请求将无法立即提供服务)
Hystrix如何实现其目标?
- 将对外部系统(或“依赖项”)的所有调用包装在通常在单独线程中执行的
HystrixCommand
或HystrixObservableCommand
对象中(这是命令模式的示例)。 - 超时呼叫花费的时间超过您定义的阈值。有一个默认值,但是对于大多数依赖项,您可以通过“属性”自定义设置这些超时,以使它们略高于针对每个依赖项测得的99.5个百分点的性能。
- 为每个依赖项维护一个小的线程池(或信号量);如果线程池已满,发往该依赖项的请求将立即被拒绝,而不是排队。
- 调用的结果有这么几种:成功,失败(客户端抛出的异常),超时和线程拒绝。
- 如果某个服务的错误百分比超过阈值,则使断路器跳闸,以在一段时间内手动或自动停止所有对特定服务的请求。
- 当请求失败,被拒绝,超时或短路时执行回退逻辑。
- 几乎实时监控指标和配置更改。
使用Hystrix来包装每个依赖项时,上图中所示的架构会发生变化,如下图所示:
每个依赖项相互隔离,当延迟发生时,它会被限制在资源中,并包含回退逻辑,该逻辑决定在依赖项中发生任何类型的故障时应作出何种响应
9.服务熔断
服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。
服务雪崩是什么:
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C有调用其他的微服务,这就是所谓的”扇出”,如扇出的链路上某个微服务的调用响应式过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”。
“断路器”本身是一种开关装置,当某个服务单元发生故障监控(类似熔断保险丝),向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延。乃至雪崩。
Hystrix会监控微服务见调用的状况,当失败的调用到一个阈值,缺省是5秒内20次调用失败就会启动熔断机制,熔断机制的注解是@HystrixCommand
实现步骤
1.导入依赖
<!--hystrix依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
2.启动类开启注解
@EnableCircuitBreaker
//启动类
@SpringBootApplication
@EnableEurekaClient //在服务启动后自动注册到Eureka中!
@EnableDiscoveryClient //服务发现
@EnableCircuitBreaker //开启熔断器
public class DeptProviderHystrix_8001 {
public static void main(String[] args){
SpringApplication.run(DeptProviderHystrix_8001.class, args);
}
}
3.改造controller
在你需要捕获的方法上加上注解@HystrixCommand
fallbackMethod:标记的是捕获异常时需要执行的方法,方法名称跟value值要一样,我这里是hystrixGet,在发生异常时就会执行备选方法返回一个预期的Dept对象而不是一直阻塞在这里。
@GetMapping("/dept/get/{id}")
@HystrixCommand(fallbackMethod = "hystrixGet")
public Dept queryById(@PathVariable("id") Long id) {
Dept dept = deptService.queryById(id);
if (dept==null){
throw new RuntimeException("id=>"+id+",用户名不存在或信息无法访问");
}
System.out.println(dept);
return dept;
}
//备选方法
public Dept hystrixGet(@PathVariable("id") Long id) {
return new Dept()
.setDeptno(id)
.setDname("id=>"+id+"没有对应的信息,为null--@Hystrix")
.setDb_source("没有找到数据");
}
4.测试
依次启动Eureka服务,provider,comsumer。
数据库里存放5条数据,先测试正常数据。正常服务返回正常Dept对象。
再测试会报错的数据,hystrix调用备选方法返回一个备用的Dept对象
10.服务降级
服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
实现步骤:
1.写个类实现FallbackFactory接口
package com.tom.springcloud.service;
import com.tom.springcloud.pojo.Dept;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DeptClientServiceFallbackFactory implements FallbackFactory {
public DeptClientService create(Throwable cause) {
return new DeptClientService() {
public boolean addDept(Dept dept) {
return false;
}
public Dept queryById(Long id) {
return new Dept()
.setDeptno(id)
.setDname("id=>"+id+"没有对应的信息,客户端提供的降级的信息,这个服务现在已经被关闭了。")
.setDb_source("没有数据");
}
public List<Dept> queryAll() {
return null;
}
};
}
}
实现FallbackFactory接口重写create方法,对service中每个方法返回备用方案。这里只写了一个方法queryById()来测试。
2.开启Feign结合Hystrix配置:
springcloud-consumer-dept-feign的配置文件
在配置文件里面修改:
feign:
hystrix:
enabled: true
3.测试
依次启动Eureka服务,Provider,Consumer。
测试正常数据,返回正常。
这个时候我们模拟系统其他服务负荷过高,需要关闭这个provider。
服务消费者再来访问这个服务时,这个服务被降级了,就会得到我们准备的备用数据。
11.Dashboard流监控
使用Hystrix一个最大的好处就是它会为我们自动收集每一个HystrixCommand的信息,并利用Hystrix-Dashboard通过一种高效的方式对每一个断路器的健康状态进行展示。
实现步骤:
1.新建一个hystrix监控的新模块
2.导入hystrix-dashboard相关的包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springCloud</artifactId>
<groupId>com.tom</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springcloud-consumer-dept-dashboard</artifactId>
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<!--eureka依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--Gson冲突-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<!--Hystrix依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
</dependencies>
</project>
3.配置文件
server:
port: 9001
#Eureka配置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/
4.启动类
添加@EnableHtstrixDashboard注解
package com.tom.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard //开启dashboard
public class DeptConsumerDashboard_9001 {
public static void main(String[] args){
SpringApplication.run(DeptConsumerDashboard_9001.class,args);
}
}
5.修改需要监控的服务启动类
//增加一个Servlet 固定代码,监控
@Bean
public ServletRegistrationBean hystrixMetricsStreamServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
registrationBean.addUrlMappings("/actuator/hystrix.stream");
return registrationBean;
}
6.测试
依次启动eureka,hystrixDashboard,provider。
在监控地址内输入 http://localhost:8002/actuator/hystrix.stream,然后点击 Monitor Stream 开始监控。
监控面板的各个指标的含义如下: