SpringCloud上
一、简介
概述
springcloud 是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。
分布式是指将不同的业务分布在不同的地方,分布式主要是针对于业务(微服务)来说,多个微服务合起来构成一个分布式系统。
分布式系统中某一个业务即微服务,可以用集群的方式实现,即将同一个微服务部署在多台服务器上,避免单点故障。
分布式系统中,A服务需要调用B服务,B服务在多台机器中都存在,A调用任意一个服务器均可完成功能。
集群指的是将几台服务器集中在一起,实现同一业务。
服务注册与发现:Eureka
服务负载与调用:Ribbon、feign
服务熔断降级:Hystrix
服务网关:zuul
服务分布式配置:springcloud config
服务开发:springboot
springboot是以数字作为版本,springcloud是以字母作为版本
dubbo做服务调用,dubbo框架里有服务提供者、服务消费者、注册中心、监控中心、负载均衡。
以前通过dubbo做服务调用,zookeeper做注册中心。
现在是用springcloud,里面集成了微服务分布式的许多落地技术,是许多分布式技术的集合体,可以这么理解,比如集成了Eureka作为注册中心等。这是springcloud和dubbo的区别。
Eureka目前停更不停用,可以用zookeeper、Consul、Nacos代替
springcloud集成的分布式技术
服务注册中心:
- Eureka
- zookeeper
- consul
- nacos
服务调用:
- Ribbon
- LoadBalancer
- Feign(不使用了)
- openFeign
服务熔断降级:
- Hystrix
- resilience4j
- AlibabaSentinel
服务网关:
- zuul
- gateway
服务配置:
- config
- apolo
- Nacos
服务总线:
- Bus
- nacos
二、示例项目构建
步骤
建父工程
首先建立一个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.atguigu.springcloud</groupId> <artifactId>cloud2020</artifactId> <version>1.0-SNAPSHOT</version> <!--表示此pom文件的工程是一个总的父工程--> <packaging>pom</packaging> <!--统一管理jar包和版本号--> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <junit.version>4.12</junit.version> <log4j.version>1.2.17</log4j.version> <lombok.version>1.16.18</lombok.version> <mysql.version>8.0.18</mysql.version> <druid.verison>1.1.16</druid.verison> <mybatis.spring.boot.verison>1.3.0</mybatis.spring.boot.verison> </properties> <!-- <dependencyManagement>这个标签一般都是用在父工程 --> <!--子模块继承之后,提供的作用:锁定版本+子module不用再写groupId和version--> <dependencyManagement> <dependencies> <!--spring boot 2.2.2--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud Hoxton.SR1--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR1</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud alibaba 2.1.0.RELEASE--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!-- MySql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- Druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.verison}</version> </dependency> <!-- mybatis-springboot整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.spring.boot.verison}</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <!--junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <!-- log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> </dependencies> </dependencyManagement> </project>
maven中
<DependencyManagement>
和<Dependencies>
的区别maven使用
<DependencyManagement>
元素来提供一种管理依赖版本号的方式通常会在一个组织或者项目的最顶层的父pom文件中看到
<DependencyManagement>
元素这样写了,在子项目pom文件中,就不用显示地列出版本号
maven会沿着父子层次向上走,直到找到一个拥有
<DependencyManagement>
元素的项目,然后它就会使用这个<DependencyManagement>
元素中指定的版本号。好处:
如果有多个子项目都引用同一个依赖,则可以避免在每个子项目的pom文件中都声明一个版本号,这样当想jar包升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要一个一个子项目的修改。另外如果某个子项目需要此依赖另外一个版本,只需要声明version即可。
注意:
<DependencyManagement>
只是确定规范,并不实现依赖,因此子项目中需要显示地声明需要用的依赖。如果子项目中不声明依赖,是不会从父项目中继承下来的,只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom.
如果子项目中指定了版本号,那么会使用子项目中指定的版本。
父工程创建完成后,执行mvn:install将父工程发布到仓库,方便子工程继承
构建子工程微服务提供者
建module
改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"> <!--父项目是管理依赖和依赖版本的。--> <!--和springboot项目有关的依赖和版本都是在父项目中来定义和声明的。--> <parent> <artifactId>cloud2020</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <!--因为父项目中有<DependencyManagement>标签,所以子项目不需要写groupId和version--> <!--gav--> <artifactId>cloud-provider-payment8001</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <!--如果没写版本,从父层面找,找到了就直接用,全局统一--> </dependency> <!--mysql-connector-java--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
写yml配置文件
server: port: 8001 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource ## 当前数据源操作类型 driver-class-name: com.mysql.cj.jdbc.Driver ## mysql驱动类 url: jdbc:mysql://localhost:3306/springcloud?serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.atguigu.springcloud.entities
写主启动类
@SpringBootApplication public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } }
写业务
controller
@RestController @Slf4j public class PaymentController { @Resource private PaymentService paymentService; @PostMapping(value = "/payment/create") public CommonResult create(@RequestBody Payment payment) { int result = paymentService.create(payment); log.info("****插入结果:" + result); if (result > 0) { return new CommonResult(200, "插入数据成功", result); } else { return new CommonResult(444, "插入数据失败", null); } } @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { Payment payment = paymentService.getPaymentById(id); log.info("****查找结果:" + payment); if (payment != null) { return new CommonResult(200, "查找数据成功", payment); } else { return new CommonResult(444, "没有对应记录,查询id:" + id, null); } } }
service
public interface PaymentService { // 直接和dao接口中的写的一样,这是一个技巧 public int create(Payment payment); public Payment getPaymentById(Long id); }
@Service public class PaymentServiceImpl implements PaymentService { @Resource private PaymentDao paymentDao; @Override public int create(Payment payment) { return paymentDao.create(payment); } @Override public Payment getPaymentById(Long id) { return paymentDao.getPaymentById(id); } }
dao
@Mapper public interface PaymentDao { public int create(Payment payment); public Payment getPaymentById(@Param("id") Long id); }
<?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.atguigu.springcloud.dao.PaymentDao"> <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id"> insert into payment(serial) values (#{serial}); </insert> <resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment"> <id column="id" property="id" jdbcType="BIGINT"></id> <id column="serial" property="serial" jdbcType="VARCHAR"></id> </resultMap> <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap"> select * from payment where id = #{id}; </select> </mapper>
entity
@Data @AllArgsConstructor @NoArgsConstructor public class Payment { private Long id;//在数据表中是自增类型 private String serial; }
CommonResult(返回体,统一将此对象返回给前端,用泛型,代表CommonResult里面的结果是不同的类型,是什么类型由后端决定)
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { // 这里为什么要用泛型呢? // 因为这个CommonResult是返回给前端的通用类型,T是什么类型,那我们返回的就是什么类型,封装在这个CommonResult对象中,并返回给前端 // 我们对外的交互(比如和前端)用的是CommonResult,但是我们自己干活用的是实体类Payment private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code, message, null); } }
构建子工程微服务消费者
建module
改pom
写yml配置文件
写主启动类
写业务
RestTemplate
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类。是spring提供的用于访问REST服务的客户端模板工具集。
REST:在url中,使用名词表示资源,使用http动作(四种请求方式)来操作资源。
使用RestTemplate访问restful接口很简单,(url,requestMap,ResponseBean.class)这三个参数分别代表REST请求地址,请求参数,HTTP响应被转换成的对象类型。
构建公共实体类项目
把服务提供者和服务调用者双方都有的公共实体类抽取出来,单独写进一个项目
通过mvn clean、mvn install,将公共实体类module jar包添加进项目配置的maven本地仓库。
在服务提供者和服务消费者的pom文件里都引入公共实体类项目的坐标即依赖,如下
<!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency>
这一步的操作和之前用dubbo框架来实现远程服务调用是一样的!
之前用dubbo,是将接口和实体类,写进一个公共项目里面,同样打包(打包成jar包,分别在服务提供者和调用者的pom文件引入依赖),服务提供者引入此jar包的目的是为了实现接口,做具体方法实现,服务调用者引入此jar包的目的是为了根据接口创建代理对象。也就是将公共的部分抽取出来,分别在双方都引入依赖。这里是一样的。
三、服务注册中心
概述
注册中心类似于病人和医生之间的挂号台。
服务的提供者和调用者都要在注册中心进行注册。
注册中心要管理服务的提供者和消费者。比如哪些服务不可用了,应该怎么办,注册中心要进行管理。
比如zookeeper会有心跳机制,来定时检测注册进注册中心的服务是否可用。
注册中心对服务的提供者和消费者进行统一的协调、调度、管理
Eureka
概述
什么是服务治理
传统的rpc远程调用框架中,管理每个服务与服务之间的依赖关系比较复杂,所以需要服务治理来管理服务与服务之间的依赖,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
什么是服务注册与发现
Eureka采用CS的设计架构,eureka server作为服务注册功能的服务器,它是服务注册中心,而系统中的其他微服务,使用eureka的客户端连接到eureka server并维持心跳连接,这样系统的维护人员就可以通过eureka server来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心,就是eureka server,当服务器启动的时候,会把当前自己的服务的信息比如服务通讯地址等以别名方式注册到注册中心上,另一方(消费者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。消费者通过别名进行调用,不再关心服务的IP地址和端口号。
这就是服务提供者配置的名称(别名):
spring: application: name: cloud-payment-service
在任何rpc框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
一般微服务提供者是大规模的集群的方式!避免单点故障
注册中心和微服务提供者一般都是大规模的集群。
注册中心一般也是分布式的部署。
Eureka Server和Eureka Client
eureka server提供服务注册服务,是注册中心
各个微服务节点通过配置启动后,会在eurekaServer中进行注册,这样eureka server中的服务注册表中将会存储所有可用服务节点信息,服务节点的信息可以在界面中直观看到。
eureka client---需要进行注册的微服务
是一个Java客户端,用于简化eureka server的交互,客户端同时也具备一个内置的,使用轮询负载算法的负载均衡器。(负载算法有轮询、哈希、最少活跃调用数、随机)
在应用启动后,将会向eureka server发送心跳(默认周期30秒),如果eureka server 在多个心跳周期内没有接收到某个节点的心跳,eureka server将会从服务注册表中将这个服务节点移除(默认90秒)
使用
eureka server
新建Eureka Server module
修改pom文件,添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
写application.yml配置文件
server: port: 7001 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: #false表示不向服务注册中心注册自己 register-with-eureka: false #false表示自己就是服务注册中心,职责就是维护服务实例,不需要去检索服务 fetch-registry: false service-url: ## 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
写主启动类
@SpringBootApplication @EnableEurekaServer public class EurekaMain7001 { public static void main(String[] args) { SpringApplication.run(EurekaMain7001.class, args); } }
注册中心Eureka Server本身是一个应用,或者说是一个单独的应用程序
微服务提供者入驻进eureka server
此微服务提供者provider是eureka client端
改pom文件
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
主启动类添加注解
@SpringBootApplication @EnableEurekaClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } }
改application.yml配置文件
eureka: client: ## 表示是否将自己注册进eureka server register-with-eureka: true ## 表示是否从eureka server抓取已有的注册信息,默认为true ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://localhost:7001/eureka
微服务消费者入驻进eureka server
此微服务提供者provider是eureka client端
改pom文件
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
改application.yml配置文件
server: port: 80 spring: application: name: cloud-order-service eureka: client: ## 表示是否将自己注册进eureka server register-with-eureka: true ## 表示是否从eureka server抓取已有的注册信息,默认为true ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://localhost:7001/eureka
主启动类添加注解
@SpringBootApplication @EnableEurekaClient public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(OrderMain80.class, args); } }
注意:企业里,注册中心也一定是集群的方式部署,避免单点故障
eureka使用步骤简述
服务注册:
微服务提供者和消费者将服务信息注册进注册中心
服务发现:
从注册中心上获取服务信息
实质:kv键值对
k:服务名 value:服务调用地址
步骤简述
先启动eureka注册中心module,在实际工作中,是集群,先启动这个集群。
启动服务提供者微服务
服务提供者启动后,会把自身信息(比如服务地址以别名的方式)注册进eureka server
服务的提供者和消费者都是eureka client
消费者在需要调用接口时,**使用服务别名(key)**去注册中心获取实际的RPC远程调用地址(value)。通过服务名称进行调用!
消费者获得调用地址之后,底层实际是通过HttpClient技术实现远程调用,但是RPC协议是远程过程调用,底层网络细节对使用人员透明。
消费者获得服务地址后会缓存在本地JVM中,默认每间隔30秒更新一次服务调用地址。
eureka集群
集群里的各个eureka server之间,相互注册。
每个eureka server需要有这个集群里其他eureka server的信息
这个集群对外呈现一个整体的形式。
eureka server集群构建步骤
建module
改pom文件
写application.yml
和单机的写法有区别
这一步的目的是为了实现相互注册
server: port: 7001 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: #false表示不向服务注册中心注册自己 register-with-eureka: false #false表示自己就是服务注册中心,职责就是维护服务实例,不需要去检索服务 fetch-registry: false service-url: ## 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址 defaultZone: http://eureka7002.com:7002/eureka/
server: port: 7002 eureka: instance: hostname: eureka7002.com #eureka服务端的实例名称 client: #false表示不向服务注册中心注册自己 register-with-eureka: false #false表示自己就是服务注册中心,职责就是维护服务实例,不需要去检索服务 fetch-registry: false service-url: ## 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址 defaultZone: http://eureka7001.com:7001/eureka/
启动两个eureka server之后,能看到以下图片的内容,说明集群搭建成功,注意:这里是在本地模拟集群,实际上工作中多个eureka server是部署在不同的服务器上。
改服务提供者和消费者的yml文件
eureka: client: ## 表示是否将自己注册进eureka server register-with-eureka: true ## 表示是否从eureka server抓取已有的注册信息,默认为true ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
注意:
构建了eureka server集群之后还要构建多个微服务提供者,服务提供者也要是集群部署的。
这多个服务提供者对外是同一个名字!
同一个名字却对应多个服务,服务提供者是集群,注册中心是集群,服务消费者和服务提供者都要进行注册。如图:
消费者来调用远程服务,就不再通过服务的ip和端口,只认服务名称!通过服务名称来进行调用,可能会调到8001这个微服务提供的服务,也可能会调到8002,通过服务名称来调用!也就是上图的
CLOUD-PAYMENT-SERVICE
修改服务消费者的代码,不能写死服务地址和端口,通过服务别名去调用,去注册中心中,通过别名拿到服务具体的访问地址,这就是注册中心的作用!!
//public static final String PAYMENT_URL = "http://localhost:8001"; public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
现在服务ip和端口不写死了,都通过服务的别名来进行调用,但是通过这个别名可能找到多个服务,不知道具体调用哪个服务,这个别名下有多个服务,因为服务提供者是以集群的方式进行部署,所以我们要指定一种负载均衡的方式,即调用远程服务的方式,就是给返回RestTemplate对象的方法上面加上负载均衡的注解。不然远程服务调用,通过别名可能找到多个服务,不知道具体调用哪个,加上负载均衡机制就可以了,默认是轮询
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced // 赋予RestTemplate负载均衡的能力,这样我们才能通过服务名称调用服务(解决了找不到具体是哪个服务的情况,因为通过名称调用) public RestTemplate getRestTemplate() { return new RestTemplate(); } }
配置主机名称(注意这里不是服务名称),和访问的时候有ip信息提示
eureka: client: ## 表示是否将自己注册进eureka server register-with-eureka: true ## 表示是否从eureka server抓取已有的注册信息,默认为true ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka instance: instance-id: payment8002 #主机名称 prefer-ip-address: true #ip信息提示
服务发现Discovery
对于注册进eureka里的微服务,可以通过服务发现来获得该服务的信息
使用:
@Resource private DiscoveryClient discoveryClient; @GetMapping(value = "/payment/discovery") public Object discovery() { List<String> services = discoveryClient.getServices(); for (String element : services) { log.info("****element: " + element); } List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); for (ServiceInstance instance : instances) { log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri()); } return this.discoveryClient; }
@SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient // 新添加的注解 public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } }
以上代码的日志
eureka自我保护机制
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护,一旦进行保护模式,eureka server将尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
简单来说:某时刻一个微服务不可用了,eureka不会立刻注销,依旧会对该服务的信息进行保存
为什么会产生自我保护机制?
为了防止eureka client可以正常运行,但是与eureka server网络不通的情况,eureka server不会立刻将eureka client注销。
默认情况下,如果eureka server在一定时间内没有接收到某个微服务实例的心跳,eureka server将会注销该实例(默认90秒),但是当网络分区发生故障的时候(延时、卡顿、拥挤),微服务(client,提供者和消费者相对于eureka server来说都是客户端,都要注册进注册中心)与eureka server注册中心之间无法正常通信,就需要保护机制,因为微服务本身是健康的,此时本不应该注销这个微服务,只是由于网络通信的问题导致没有接收到心跳。
所以eureka通过自我保护模式来解决这个问题。
使用自我保护模式,可以让eureka集群更加健壮、稳定。
eureka向客户端发送心跳的时间间隔,单位为秒(默认是30秒)
eureka server在收到最后一次心跳后等待时间上限,单位为秒,默认90秒,超过此上限,将会注销服务。
eureka: instance: lease-renewal-interval-in-seconds: 30 lease-expiration-duration-in-seconds: 90
zookeeper
概述
和eureka是一个道理,相当于把注册中心从eureka换成了zookeeper,同样需要把服务的提供者和服务的消费者注册到zookeeper注册中心。而zookeeper本身在生产环境中又是分布式部署的方式,是集群的方式,有leader,有follower,不要把follower理解为服务的消费者,zookeeper这个注册中心是一个集群。
上图的zookeeper service是注册中心,下面的client既有微服务的提供者也有微服务的消费者,他们在这种场景下相对于注册中心都叫做client。
使用
微服务提供者
修改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"> <parent> <artifactId>cloud2020</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-provider-payment8004</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!-- springboot整合zookeeper客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <!--先排除自带的zookeeper3.5.3-beta--> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <!--添加zookeeper的3.4.11版本的jar包依赖--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
创建application.yml文件。
#8004表示注册到zookeeper服务器的微服务提供者端口号 server: port: 8004 #服务别名,即注册到zookeeper注册中心的服务别名 spring: application: name: cloud-provider-payment cloud: zookeeper: connect-string: 42.192.182.4:2181
修改主启动类
@SpringBootApplication @EnableDiscoveryClient public class PaymentMain8004 { public static void main(String[] args) { SpringApplication.run(PaymentMain8004.class, args); } }
写Controller
@RestController @Slf4j public class PaymentController { @Value("${server.port}") private String serverPort; @RequestMapping(value = "/payment/zk") public String paymentZk() { return "springcloud with zookeeper: " + serverPort + "\t" + UUID.randomUUID().toString(); } }
Service层的业务类和数据库访问对象层即dao层不再单独写,因为逻辑都是整合mybatis,通过mapper映射文件,去操作数据库。这里直接在Controller层对是否注册进zookeeper进行测试。
出现bug
服务器上已经装好了zookeeper,版本为3.4.11,springboot微服务需要注册进zookeeper,那么需要添加如下依赖,引入zookeeper的jar包,如下。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> </dependency>
通过这种方式引入,会给我们的springboot项目添加如下jar包
于是springboot项目的zookeeper jar包和服务器上已经装好的zookeeper的版本是冲突的。
现在解决冲突。
<!-- springboot整合zookeeper客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <!--先排除自带的zookeeper3.5.3-beta--> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <!--添加zookeeper的3.4.11版本的jar包依赖--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
以下就是服务注册进zookeeper注册中心的基本信息
[zk: localhost:2181(CONNECTED) 7] get /services/cloud-provider-payment/af00db76-c948-4f81-9e6c-aa5cfbb9fdf6 { "name": "cloud-provider-payment", "id": "af00db76-c948-4f81-9e6c-aa5cfbb9fdf6", "address": "TABLET-H07SRC9P", "port": 8004, "sslPort": null, "payload": { "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance", "id": "application-1", "name": "cloud-provider-payment", "metadata": {} }, "registrationTimeUTC": 1648888007937, "serviceType": "DYNAMIC", "uriSpec": { "parts": [{ "value": "scheme", "variable": true }, { "value": "://", "variable": false }, { "value": "address", "variable": true }, { "value": ":", "variable": false }, { "value": "port", "variable": true }] } } cZxid = 0x8 ctime = Sat Apr 02 16:26:48 CST 2022 mZxid = 0x8 mtime = Sat Apr 02 16:26:48 CST 2022 pZxid = 0x8 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x100009bdc190002 dataLength = 536 numChildren = 0
把服务提供者停掉之后,zookeeper不会马上将注册的服务移除,而是通过心跳机制(tick-time)来检测注册到zookeeper的服务的状态。服务被我们停掉之前,zookeeper也在通过心跳机制检测服务的状态。心跳机制就是zookeeper每间隔一个心跳时间tick-time,就向服务(客户端)发送信号,如果多个心跳都没能得到客户端的反馈,说明注册进去的客户端已经失效,则将客户端移除。
要注意在这里,服务提供者和服务消费者都是客户端,zookeeper注册中心是服务器。zookeeper在生产环境是分布式部署的,集群的各服务器之间也是可以互相发送心跳的,通过心跳机制来检测服务器是否宕机。
微服务消费者
修改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"> <parent> <artifactId>cloud2020</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-consumerzk-order80</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- springboot整合zookeeper客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <!--先排除自带的zookeeper3.5.3-beta--> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <!--添加zookeeper的3.4.11版本的jar包依赖--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
创建yml文件
#80表示注册到zookeeper服务器的微服务消费者端口号 server: port: 80 #服务别名,即注册到zookeeper注册中心的服务别名 spring: application: name: cloud-consumer-order cloud: zookeeper: connect-string: 42.192.182.4:2181
主启动类
@SpringBootApplication @EnableDiscoveryClient public class OrderZKMain80 { public static void main(String[] args) { SpringApplication.run(OrderZKMain80.class, args); } }
zookeeper的作用
zookeeper作为注册中心只是zookeeper的一种作用,zookeeper还可以在分布式环境中有以下几种作用:
- 统一命名服务
- 统一配置管理
- 注册中心
- 软负载均衡
consul
概述
consul是一套开源的分布式服务发现和配置管理系统。
提供了微服务系统中的服务治理、配置中心、控制总线等功能,这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网络,总之consul提供了一套完整的服务网络解决方案。
consul是用go语言开发的。
使用
开发模式启动
consul agent -dev
通过以下地址可以访问consul的首页
localhost:8500
微服务提供者
修改pom文件,添加consul相关场景的依赖
<dependencies> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
依赖的添加,因为我们是使用springboot做开发,springboot简化了spring对jar包的管理,我们不需要自己去添加某个场景开发所需要的所有jar包,做某个场景的开发,就添加这个场景相关的starter,如这里
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
修改application.yml文件
#8006表示注册到consul服务器的微服务提供者端口号 server: port: 8006 #服务别名,即注册到consul注册中心的服务别名 spring: application: name: consul-provider-payment cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
主启动类
@SpringBootApplication @EnableDiscoveryClient public class PaymentMain8006 { public static void main(String[] args) { SpringApplication.run(PaymentMain8006.class, args); } }
微服务消费者
修改pom文件
<dependencies> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
修改application.yml文件,指定consul注册中心的地址、端口
#80表示注册到consul服务器的微服务消费者端口号 server: port: 80 #服务别名,即注册到consul注册中心的服务别名 spring: application: name: consul-consumer-order cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
主启动类
@SpringBootApplication @EnableDiscoveryClient public class OrderConsulMain80 { public static void main(String[] args) { SpringApplication.run(OrderConsulMain80.class, args); } }
写配置类,创建bean---ApplicationContextConfig
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
Eureka、Zookeeper、Consul的异同
Eureka主要保证的是高可用,Zookeeper和Consul主要保证的是数据一致性。
CAP理论中的C是数据一致性,A是高可用性,P是分区容错性。
Nacos
四、服务调用
Ribbon
概述
Spring cloud Ribbon是基于Netflix Ribbon实现的一套客户端
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单地说,就是在配置文件中列出Load Balancer(LB)后面所有的机器,Ribbon会自动基于某种规则(如简单轮询、随机连接等)去连接这些机器。
我们能够很容易地使用Ribbon实现自定义的负载均衡算法。
Ribbon是本地负载均衡,Nginx是服务端负载均衡。
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务器端实现的。
Ribbon是本地负载均衡,在调用服务接口的时候,会在注册中心上获取注册的服务列表之后缓存到JVM,从而在本地实现RPC远程服务调用技术。
Ribbon适合在RPC远程调用实现本地服务负载均衡,比如Dubbo、Springcloud中都是采用本地负载均衡。
集中式负载均衡---服务端负载均衡
即在服务器和客户端之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务器。
进程内负载均衡---本地负载均衡
将LB逻辑集成到消费方,消费方从注册中心获知服务有哪些地址可以用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
Ribbon和Nginx的区别:
比如注册中心是分布式部署的方式,即集群,那么客户端的请求先发送到Nginx,Nginx通过某种策略转发到某个服务器,找到注册中心,消费者从注册中心拿到的服务信息列表,缓存到本地JVM,接着就是进程内的负载均衡,从本地的服务列表中选择合适的服务地址进行调用,因为注册中心是集群部署,同一个服务也是集群部署,避免单点故障。
即客户端的请求是到达哪个服务器,是通过Nginx,而具体的业务是调用部署在哪个服务器的服务,是通过Ribbon。
Ribbon在工作时分成两步:
1)第一步先选择Eureka Server,它优先选择在同一个区域内负载较少的server
2)第二步再根据用户指定的策略,在从**server(注册中心)**取到的服务注册列表中选择一个地址(同一个服务采用集群部署的方式,部署在多台服务器上,避免单点故障)。其中Ribbon提供了多种策略,比如轮询、随机和根据响应时间加权。
**Ribbon可以理解为负载均衡 + RestTemplate调用。**可以理解为客户端的负载均衡工具,配合RestTemplate实现RPC远程调用。
Ribbon是软负载均衡的客户端组件,可以和其他客户端结合使用,和eureka结合只是其中的一个实例。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
我们引入的eureka-client jar包就自动引入了ribbon,点进去可以看到已经自动引入了负载均衡相关的jar包
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.1.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> <version>2.2.1.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-eureka</artifactId> <version>2.3.0</version> <scope>compile</scope> <exclusions> <exclusion> <artifactId>annotations</artifactId> <groupId>com.google.code.findbugs</groupId> </exclusion> <exclusion> <artifactId>servlet-api</artifactId> <groupId>javax.servlet</groupId> </exclusion> </exclusions> </dependency>
RestTemplate
常用的方法:
- getForObject方法、getForEntity方法
- postForObject方法、postForEntity方法
一个是读,一个是写。
getForObject
返回对象为响应体中数据转换成的对象,基本上可以理解为json
通过如下代码打印出来Object对象
CommonResult<Payment> object = restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); System.out.println(object); return object;
打印出来的Object对象为
CommonResult(code=200, message=查找数据成功,serverPort: 8002, data={id=33, serial=donglijiedian})
返回的Object对象完全是我们自定义的CommonResult对象。因为getForObject方法的第三个参数为CommonResult.class
在浏览器上显示如下:
{"code":200,"message":"查找数据成功,serverPort: 8002","data":{"id":33,"serial":"donglijiedian"}}
可以看到是一个Json字符串,这是因为在此Controller上加了@RestController注解,相当于加了@ResponseBody注解,这个注解表明返回的是数据而不是视图,**作用是将json格式的字符串,通过HttpServletResponse输出给浏览器。**所以在浏览器看到的是json格式的字符串,在控制台打印的Object则是Java对象,此Java对象会转换成JSON格式字符串,通过HttpServletResponse输出到浏览器。
加了@ResponseBody注解,相当于:
PrintWriter pw = servlet.getWriter(); pw.print(json); pw.flush(); pw.close();
转换成json字符串的过程:
HttpMessageConverter是一个接口,转换http信息的接口,有7个实现类,这7个实现类放在ArrayList里。
SpringMVC框架根据这个ArrayList里的7个实现类,依次调用他们的canWrite()方法,来找到能处理这个对象的数据的实现类。
找到实现类之后,调用write方法,把对象转换为json,调用Jackson的ObjectMapper实现转换为json格式的字符串。
框架会调用@ResponseBody把Json格式字符串输出到浏览器,请求处理完成
getForEntity
返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
如下是返回的ResnponseEntity对象
<200,CommonResult(code=200, message=查找数据成功,serverPort: 8002, data={id=33, serial=donglijiedian}),[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Thu, 07 Apr 2022 11:30:50 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
响应的信息很完整
返回的ResnponseEntity对象里为什么是CommonResult,是因为调用的时候,第三个参数是CommonResult.class
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
IRule接口
作用:根据特定算法从服务列表中选取一个要调用的服务。
IRule是一个接口
它有如上图所示的实现类。
- RoundRobinRule 轮询
- RandomRule 随机
- RetryRule 重试
- WeightedResponseTimeRule 对轮询的扩展,响应速度越快的实例选择权重越大,越容易被选择
- BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例
- ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器
选择负载均衡规则的方法
自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则这个自定义配置类会被所有的Ribbon客户端所共享,达不到特殊化定制的目的。
新建package
新建MySelfRule类
@Configuration public class MySelfRule { @Bean public IRule myRule() { return new RandomRule();// 定义为随机 } }
给主启动类添加注解
@SpringBootApplication @EnableEurekaClient @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration= MySelfRule.class) public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(OrderMain80.class, args); } }
轮询算法原理:
rest接口第几次请求 % 服务器集群总数量 = 实际调用服务器位置下标。
每次服务重启动后rest接口计数从1开始。
OpenFeign
概述
Feign主要是做服务的调用。
是用在服务消费方,做服务调用的。
Feign是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需创建一个接口,并在接口上添加注解@FeignClient即可。
Ribbon和Feign的区别
Feign Ribbon OpenFeign Feign是Spring Cloud组件中的一个轻量级的RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用注册中心的服务。
使用方式:使用Feign的注解定义接口,调用这个接口中的方法,就可以调用注册中心的服务。类似于Dubbo和zookeeper集成,通过jar包中的接口调用服务,实际上jar包中并没有服务的具体实现,仅仅是接口而已,服务的具体实现在远程,在另一个项目上,部署在另一个服务器上,通过dubbo和zookeeper的配置找到。隐藏了调用的细节,就像是调用自己本地的服务一样,实际上本地并没有服务的具体实现。结合RestTemplate实现服务的调用,实际上就像使用HttpClient一样,通过URL即API服务地址,调用服务。可以实现负载均衡。 是Spingcloud在Feign的基础上,支持了SpringMVC的注解,如@RequestMapping等。
OpenFeign的@FeignClient注解可以解析springMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
使用
新建服务消费方
cloud-consumer-feign-order80
修改pom文件
<dependencies> <!--openFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--eureka--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
写主启动类
@SpringBootApplication @EnableFeignClients public class OrderFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderFeignMain80.class, args); } }
写业务类
- 写业务逻辑接口 + @FeignClient,调用服务提供者的服务。
- 写控制层Controller。
@Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id); }
写控制层Controller
@RestController @Slf4j public class OrderFeignController { @Resource private PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/get/{id}") public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } }
超时控制
服务消费方和服务提供方是两个微服务,那么消费者去调用提供者,有调用时间这么一个概念,需要进行超时控制。
比如说消费者对提供者的调用,消费者只愿意等2秒钟,提供者需要花3秒钟来进行服务的业务逻辑的实现。
这样就产生了时间差,会导致超时报错。在消费者和提供者之间要约定好。
默认Feign客户端只等待一秒钟,但是服务端处理业务逻辑需要超过1秒钟,Feign客户端也就是消费方,会报错。所以我们需要设置Feign客户端的超时控制。
开启配置
## 设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: #指的是建立连接所用的时间,适用于网络正常的情况下,两端连接所用的时间 ReadTimeout: 5000 #建立连接后,从服务器读取到可用资源所用的时间 ConnectionTimeout: 5000
日志打印
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign种Http请求的细节。
就是对Feign接口的调用情况进行监控和输出。
日志级别:
- NONE:默认的,不显示任何日志
- BASIC:仅记录请求方法,URL、响应状态码及执行时间
- HEADERS:除了BASIC中的信息之外,还有请求和响应头信息
- FULL:除了HEADERS的信息之外,还有请求和响应的正文及元数据
写日志配置类
@Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
YML文件需要开启日志
logging: level: ## feign日志以什么级别监控哪个接口 com.atguigu.springcloud.service.PaymentFeignService: debug
五、服务熔断降级
分布式系统面临的问题
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的“扇出”。(一个微服务在一个分布式系统中可能既是服务的消费者,也可能是服务的提供者)
如果扇出的链路上,某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,就是“雪崩效应”。
其实就是服务的高可用被破坏了。
分布式系统的高可用性、分区容错性、数据一致性。
我们需要避免服务雪崩这种故障,就需要有一种兜底的方案,保障分布式系统的可用性。
这种兜底的方案或者说中断服务链路的方案就是服务降级。
服务降级-fallback
概念:
服务不可用了,需要有一个兜底的方法,要给调用方友好的提示和响应。比如服务提供方的业务逻辑处理出了问题,导致服务不可用,需要给服务消费方返回一个符合预期的、可处理的备选响应。
不让客户端(服务消费方)等待并立刻返回一个友好提示,这就是服务降级。
哪些情况会出现服务降级?
- 程序运行异常
- 超时
- 服务熔断触发服务降级
服务熔断-break
概念:
类比服务器达到最大访问数量后,直接拒绝访问,相当于保险丝的作用,拉闸限电,然后调用服务降级的方法并给服务消费方(客户端)返回友好提示。
服务限流-flow limit
概念:
秒杀、高并发等场景,严谨一窝蜂地过来拥挤,让请求排队,有序进行。
Hystrix
概述
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,是处理服务熔断、服务降级的开源库。是一个断路器。
在分布式系统里,许多依赖不可避免地会调用失败,比如超时、异常等,Hystrix能保证在一个服务依赖出问题的情况下, 不会导致整体服务失败,避免了级联故障,以提高分布式系统的弹性。
断路器是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝--服务熔断),向调用方立刻返回一个不符合预期的、可处理的备选响应(Fallback)--服务降级,而不是长时间地等待或者抛出调用方(消费方)无法处理的异常。这样就保证了服务调用方(消费方)的线程不会被长时间的、不必要的占用,从而避免了故障在分布式系统中的蔓延乃至服务雪崩。
也就是正常流程走不通了,走异常流程。相当于即使服务提供方没有正常工作,也让服务消费方拿到能够处理的响应,正常执行下去,保证了整个链路的正常工作,保证了分布式系统的可用性。
使用
微服务提供者
修改注册中心Eureka Server的application.yml,改成单机版。
主要是为了整合Hytrix,并跑通,所以先改成单机版。
server: port: 7001 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: #false表示不向服务注册中心注册自己 register-with-eureka: false #false表示自己就是服务注册中心,职责就是维护服务实例,不需要去检索服务 fetch-registry: false service-url: ## 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址 ## defaultZone: http://eureka7002.com:7002/eureka/ defaultZone: http://eureka7001.com:7001/eureka/
创建module:cloud-provider-hytrix-payment8001
修改pom文件
<dependencies> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
修改application.yml文件
server: port: 8001 spring: application: name: cloud-provider-hystrix-service eureka: client: ## 表示是否将自己注册进eureka server register-with-eureka: true ## 表示是否从eureka server抓取已有的服务注册信息,默认为true ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡(消费者方) fetchRegistry: true service-url: ## defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka defaultZone: http://eureka7001.com:7001/eureka
主启动类
@SpringBootApplication @EnableEurekaClient public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } }
控制层Controller
@RestController @Slf4j public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_OK(id); log.info("**result: " + result); return result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_Timeout(@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_Timeout(id); log.info("**result: " + result); return result; } }
测试
20000个线程发送请求,发送到localhost:8001/payment/hystrix/timeout/31这个接口。
同时拖累了发送到localhost:8001/payment/hystrix/ok/{id}这个接口的请求。
由于这两个接口是在一个微服务里面,产生了连坐效应。
此微服务需要花大量资源处理发送到localhost:8001/payment/hystrix/timeout/31接口的请求,导致发送到localhost:8001/payment/hystrix/ok/{id}这个接口的请求也会变慢!
Tomcat默认的工作线程数都去处理timeout接口的请求,所以没有线程处理ok接口的请求,这就是连坐效应。
微服务消费者
新建module
hytrix在消费方和服务方都可以添加,一般我们用在消费方。
pom文件
<dependencies> <!--hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--openFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--eureka--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--引入公共实体类项目的jar包坐标--> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--子类没有写版本号的,都继承了父类的版本号--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
yml文件
server: port: 80 eureka: client: ## 表示是否将自己注册进eureka server register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka
主启动类
@SpringBootApplication @EnableFeignClients public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class, args); } }
业务类
@Service @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-SERVICE") public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_Timeout(@PathVariable("id") Integer id); }
测试
20000个线程发送请求去8001端口的timeout接口,服务消费者80去调8001端口的ok接口,同样变慢,也就是说,压力测试是在timeout接口做的,同一层次的ok接口也受到了影响,响应速度变慢。
服务降级
提供者
用注解:@HystrixCommand
服务提供方设置自身调用超时时间的阈值,阈值内可以正常运行,超过了需要有兜底的方法处理,做服务降级fallback
在可能出错的业务方法上加注解:@HystrixCommand。
注解的属性
fallbackMethod,值为方法名,此方法名所代表的方法就是当该业务方法内部超时或出错,便会去执行注解指定的方法。
commandProperties
里面是注解@HystrixProperty,指定此业务方法超时时间等。
// 模拟会出错 @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public String paymentInfo_Timeout(Integer id) { int timeNumber = 5; try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:" + Thread.currentThread().getName() + " paymentInfo_Timeout,id: " + id + "\t" + "耗时" + timeNumber + "秒钟"; } public String paymentInfo_TimeoutHandler(Integer id) { return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeoutHandler,id: " + id; }
主启动类添加注解:
@EnableCircuitBreaker
注意:
Hystrix会单独启动新的线程来做处理。
消费者
yml文件
feign: hystrix: enabled: true
主启动类--添加@EnableHystrix
@SpringBootApplication @EnableFeignClients @EnableHystrix public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class, args); } }
修改控制层Controller类,和服务提供者一样的。
@RestController @Slf4j public class OrderHystrixController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/payment/hystrix/timeout/{id}") // 模拟会出错 @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") }) public String paymentInfo_Timeout(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_Timeout(id); return result; } public String paymentInfo_TimeoutHandler(@PathVariable("id") Integer id) { return "我是消费者80,对方支付系统繁忙,请10秒钟后再试。"; } }
paymentInfo_TimeoutHandler就是服务降级方法--fallbackMethod。
问题
如果说每一个方法都要对应一个服务降级,那么会造成代码膨胀,并且现在处理异常的方法和业务方法耦合在一起,需要解决。
解决代码膨胀:
通过注解@DefaultProperties(defaultFallback = "")来配置全局服务降级
没有配置服务降级的,就找全局服务降级@DefaultProperties的方法
配置了服务降级的,就找自己配置的服务降级方法。
解决代码混乱、耦合:
把在controller层进行服务降级转换到service层进行服务降级,service层才是真正处理业务的方法。
为Feign客户端定义的接口添加一个服务降级处理的实现类,即可实现解耦,此实现类专门做服务降级。
首先新写一个feign接口的实现类,专门写服务降级方法
@Component public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK(Integer id) { return "----PaymentFallbackServic fallback----paymentInfo_OK"; } @Override public String paymentInfo_Timeout(Integer id) { return "----PaymentFallbackServic fallback----paymentInfo_Timeout"; } }
如果接口中的方法能够正常执行,那么正常执行,如果发生异常,则到这个类中执行对应的实现方法(重写的接口中的方法),进行服务降级。
在feign接口的注解中标注
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-SERVICE", fallback = PaymentFallbackService.class)
这样的话,在Controller中就不用写服务降级方法代码,也不用添加额外的注解,避免了代码膨胀,同时由于业务feign接口中的方法都有对应的服务降级处理方法,都写在另一个类里,能保证处理完善的同时,又保证了代码解耦合。
服务熔断
概述
服务降级和服务熔断是两回事
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,恢复调用链路。
在springcloud框架中,熔断机制通过hystrix实现,hystrix会监控微服务间调用的情况,当失败的调用达到一定阈值,缺省是5秒内20次调用失败,就会启用熔断机制。
熔断机制的注解是@HystrixCommand
为了进行服务的熔断,调用服务降级,实现服务熔断。
服务降级---服务熔断---恢复调用链路
可以理解为服务降级是为了服务熔断。
服务熔断机制和服务降级不同的一点就是,服务熔断机制在服务调用正常后会恢复调用链路。
服务熔断机制可以理解为断路器
对于建筑物中的断路器,当情况恢复正常时,需要外部干预才能将其重置。对于软件断路器,我们可以让断路器本身检测基础调用是否再次正常工作,我们可以通过在适当的时间间隔后再次尝试受保护的调用来实现这种自我重置行为,并在成功后重置断路器。
其实就是服务熔断机制是软件断路器,除了在链路中的某个服务调用失败时,会熔断服务进行服务降级,进而保护整个分布式系统,还会在服务调用成功的时候,自行恢复链路!!
Half Open是为了进行受保护的调用。相当于将链路闸道慢慢放开,看调用能否成功。
启用了熔断机制之后,如果我们在规定的时间内,请求数达到了一定数量之后,失败率达到了一定的值(我们做测试时,发送的请求一定是失败的,目的就是为了熔断链路),此时链路状态是由closed--open。
断路器就会使得此被调用的微服务熔断,即使下一次的调用是正确的访问地址,本应该返回正确的结果,也不会正确返回结果了,在断路器状态是open状态的时候,即使是正确的调用,也会去执行服务降级方法,这就是服务熔断调用服务降级。
隔了一段时间(默认是5秒),系统尝试受保护的调用,将链路置于half-open的状态,尝试调用(允许一次请求调用,执行主逻辑而不是服务降级逻辑),调用成功后,重置了断路器,那么将链路状态由half-open转换为close的。
启用熔断机制,**那么链路会是closed到open,open到half-open,再到closed的环形过程。**其中open到half-open是系统为了恢复链路而做的保护性的调用,那么就将链路置于半open的这种状态。
服务提供者
在service层添加注解
// 服务熔断 @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") }) public String paymentCircuitBreaker(Integer id) { if (id < 0) { throw new RuntimeException("id不能为负数"); } String serialNumber = IdUtil.simpleUUID(); return Thread.currentThread().getName() + "\t" + "调用成功,流水号:" + serialNumber; } public String paymentCircuitBreaker_fallback(Integer id) { return "id不能为负数,请稍后再试, id:" + id; }
- circuitBreaker.enabled:是否开启断路器
- circuitBreaker.requestVolumeThreshold:请求总数阈值,在时间窗口期内,必须满足请求总数阈值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或者因为其他原因失败,断路器都不会打开。
- circuitBreaker.sleepWindowInMilliseconds:时间窗口期
- circuitBreaker.errorThresholdPercentage:失败率达到多少后,跳闸
上面配置的意义就是在10秒钟内,请求10次以上,失败率达到60%就会进行服务熔断。
Controller层
//服务熔断 @GetMapping("/payment/circuit/{id}") public String paymentCircuitBreaker(@PathVariable("id") Integer id) { String result = paymentService.paymentCircuitBreaker(id); log.info("result: " + result); return result; }
熔断类型
熔断打开--open
请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态(half open)。
熔断关闭--closed
熔断关闭,那么调用服务正常进行。
熔断半开--half open
部分请求根据规则调用当前服务,如果请求成功且符合规则,则认为当前服务恢复正常,关闭熔断(closed)。
断路器
打开或关闭的条件
时间窗口期内,请求总数必须达到,当失败率达到一定阈值的时候,断路器会开启。
断路器开启的时候,所有的请求都不会成功,都会去执行服务降级fallback。这就是服务熔断调用服务降级。
一段时间之后(默认是5秒),断路器会处于半开状态,进行保护性的调用,会让其中的一个请求进行转发,如果成功,断路器会关闭,到closed状态,如果失败,继续开启,然后重复这个过程。这就是服务熔断机制的链路恢复过程。
断路器打开后,再有请求调用的时候,将不会调用主逻辑,而是直接调用服务降级方法fallback
原来的主逻辑如何恢复?
hystrix实现了自动恢复功能。当断路器打开,对服务进行了熔断之后,会进行一段时间,这段时间的逻辑都是降级逻辑,都不会进行主逻辑,因为服务被熔断了,链路处于open状态。这段时间之后,断路器处于半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态。重复这个过程。
可以把服务降级看作为服务熔断的一部分。服务熔断还有很重要的一点是有链路的恢复!服务熔断会调用服务降级。