spring cloud之Eureka注册中心

什么是注册中心

​ 微服务将原本单机服务水平扩展为集群(微服务总体是分布式,每一部分相互调用,牵一发而动全身,为了避免单点故障,每一部分采用了集群),以提高并发服务,注册中心应运而生。

注册中心相当于电话本,注册中心将每个注册服务以服务名为keyip和端口为value,而每个服务名包含了多台用于相同service id的spring boot应用,一般是使用http调用(还有RPC),调用的时候只要知道服务名,就可以调用,具体调用那一台服务器的应用策略可以使用负载均衡策略。

常见注册中心

特性 Eureka Nacos Consul Zookeeper
CAP AP CP + AP CP CP
健康检查 Client Beat TCP/HTTP/MYSQL/Client Beat TCP/HTTP/gRPC/Cmd Keep Alive
雪崩保护
自动注销实例 支持 支持 不支持 支持
访问协议 HTTP HTTP/DNS HTTP/DNS TCP
监听支持 支持 支持 支持 支持
多数据中心 支持 支持 支持 不支持
跨注册中心同步 不支持 支持 支持 不支持
SpringCloud集成 支持 支持 支持 支持

CAP 原则与 BASE 理论

CAP 原则

CAP 原则又称 CAP 定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

CAP 由 Eric Brewer 在 2000 年 PODC 会议上提出。该猜想在提出两年后被证明成立,成为我们熟知的 CAP 定理。CAP 三者不可兼得。

特性 定理
Consistency 一致性,也叫做数据原子性,系统在执行某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。等同于所有节点访问同一份最新的数据副本。
Availability 可用性,每一个操作总是能够在一定的时间内返回结果,这里需要注意的是"一定时间内"和"返回结果"。一定时间内指的是在可以容忍的范围内返回结果,结果可以是成功或者是失败,且不保证获取的数据为最新数据。
Partition tolerance 分区容错性,分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。

取舍策略

CAP 三个特性只能满足其中两个,那么取舍的策略就共有三种:

  • CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃 P 的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。
  • CP without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成 CP 的系统其实不少,最典型的就是分布式数据库,如 Redis、HBase 等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
  • AP without C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。

总结

现如今,对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。那么就只能在 C 和 A 之间进行取舍。但对于传统的项目就可能有所不同,拿银行的转账系统来说,涉及到金钱的对于数据一致性不能做出一丝的让步,C 必须保证,出现网络故障的话,宁可停止服务,可以在 A 和 P 之间做取舍。而互联网非金融项目普遍都是基于 AP 模式。

总而言之,没有最好的策略,好的系统应该是根据业务场景来进行架构设计的,只有适合的才是最好的。

BASE 理论

CAP 理论已经提出好多年了,难道真的没有办法解决这个问题吗?也许可以做些改变。比如 C 不必使用那么强的一致性,可以先将数据存起来,稍后再更新,实现所谓的 “最终一致性”。

这个思路又是一个庞大的问题,同时也引出了第二个理论 BASE 理论。

BASE:全称 Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。

BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:

既然无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

Basically Available(基本可用)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性)。需要注意的是,基本可用绝不等价于系统不可用。

  • 响应时间上的损失:正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2 秒。
  • 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。

Soft state(软状态)

什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。

软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本数据同步的延时就是软状态的体现。

Eventually consistent(最终一致性)

系统不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性。从而达到数据的最终一致性。这个时间期限取决于网络延时,系统负载,数据复制方案设计等等因素。

实际上,不只是分布式系统使用最终一致性,关系型数据库在某个功能上,也是使用最终一致性的,比如备份,数据库的复制都是需要时间的,这个复制过程中,业务读取到的值就是旧值。当然,最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。

总结

总的来说,BASE 理论面向的是大型高可用可扩展的分布式系统,和传统事务的 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的。

为什么需要注册中心

了解了什么是注册中心,那么我们继续谈谈,为什么需要注册中心。在分布式系统中,我们不仅仅是需要在注册中心找到服务和服务地址的映射关系这么简单,我们还需要考虑更多更复杂的问题:

  • 服务注册后,如何被及时发现
  • 服务宕机后,如何及时下线
  • 服务如何有效的水平扩展
  • 服务发现时,如何进行路由
  • 服务异常时,如何进行降级
  • 注册中心如何实现自身的高可用

这些问题的解决都依赖于注册中心。简单看,注册中心的功能有点类似于 DNS 服务器或者负载均衡器,而实际上,注册中心作为微服务的基础组件,可能要更加复杂,也需要更多的灵活性和时效性。所以我们还需要学习更多 Spring Cloud 微服务组件协同完成应用开发。

注册中心解决了以下问题:

  • 服务管理
  • 服务之间的自动发现
  • 服务的依赖关系管理

Eureka 介绍

Eureka 是 Netflix 开发的服务发现组件,本身是一个基于 REST 的服务。Spring Cloud 将它集成在其子项目 Spring Cloud Netflix 中,实现 Spring Cloud 的服务注册与发现,同时还提供了负载均衡、故障转移等能力。

Eureka 注册中心三种角色(两种角色)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjFqqrTj-1611660353890)(注册中心.assets/timg-1578234270981.jfif)]

Eureka Server

通过 Register、Get、Renew 等接口提供服务的注册和发现。

Service Provider(Eureka Client)

服务提供方,把自身的服务实例注册到 Eureka Server 中。同时拉取服务列表,消费其他服务。

Service Consumer(Eureka Client)

服务调用方,通过 Eureka Server 获取服务列表,消费服务。

编码

注册中心配置说明

# 配置 Eureka Server 注册中心
eureka:
  instance:
    hostname: localhost			  # 主机名,不配置的时候将根据操作系统的主机名来获取
  client:
    register-with-eureka: false   # 是否将自己注册到注册中心,默认为 true
    fetch-registry: false         # 是否从注册中心获取服务注册信息,默认为 true
    service-url:                  # 注册中心对外暴露的注册地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

单节点注册中心:不将自己注册到注册中心,并且不从注册中心获取服务

register-with-eureka: false # 是否将自己注册到注册中心,默认为 true
fetch-registry: false # 是否从注册中心获取服务注册信息,默认为 true

多节点将两个false改成true,或者不写这两个配置

显示ip而不是localhost

一个普通的 Netflix Eureka 实例注册的 ID 等于其主机名(即,每个主机仅提供一项服务)。 Spring Cloud Eureka 提供了合理的默认值,定义如下:

${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}},也就是:主机名:应用名:应用端口。

我们也可以可以自定义进行修改:

eureka:
  instance:
    prefer-ip-address: true # 是否使用 ip 地址注册
    instance-id: ${
   spring.cloud.client.ip-address}:${
   server.port} # ip:port

依赖

spring boot基础上加上

 <!-- netflix eureka server 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

双注册中心(代码)

父工程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.day1</groupId>
    <artifactId>spring-cloud-eureka</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>service-1</module>
        <module>service-2</module>
    </modules>
    <!-- 继承 spring-boot-starter-parent 依赖 -->
    <!-- 使用继承方式,实现复用,符合继承的都可以被使用 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
    </parent>

    <!--
        集中定义依赖组件版本号,但不引入,
        在子工程中用到声明的依赖时,可以不加依赖的版本号,
        这样可以统一管理工程中用到的依赖版本
     -->
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <!-- Spring Cloud Hoxton.SR1 依赖 -->
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>

    <!-- 项目依赖管理 父项目只是声明依赖,子项目需要写明需要的依赖(可以省略版本信息) -->
    <dependencyManagement>
        <dependencies>
            <!-- spring cloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

service1:注册中心1

server:
  port: 8761 # 端口

spring:
  application:
    name: eureka-server # 应用名称

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
    #hostname: localhost			  # 主机名,不配置的时候将根据操作系统的主机名来获取
  client:
    service-url:                  # 注册中心对外暴露的注册地址
      defaultZone: http://localhost:8762/eureka/
<?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>spring-cloud-eureka</artifactId>
        <groupId>com.day1</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-1</artifactId>

    <!-- 项目依赖 -->
    <dependencies>
        <!-- netflix eureka server 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <!-- spring boot test 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@EnableEurekaServer
public class Service1
{
    public static void main( String[] args )
    {
        SpringApplication.run(Service1.class,args);
    }
}

service2:注册中心2

@EnableEurekaService注解

与service1同样的pom依赖

配置文件,修改端口,并且两个注册中心互相注册

server:
  port: 8762 # 端口

spring:
  application:
    name: eureka-server # 应用名称

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
    #hostname: localhost			  # 主机名,不配置的时候将根据操作系统的主机名来获取
  client:
    service-url:                  # 注册中心对外暴露的注册地址
      defaultZone: http://localhost:8761/eureka/

服务提供者配置说明

在微服务里没有纯粹的消费者(服务提供者),生产者也可以作为消费者,而单纯消费者并不能作为生产者。

server:
  port: 7070 # 端口

spring:
  application:
    name: service-provider # 应用名称(集群下相同)

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

生产者配置:

spring.application.name:所有相同服务提供者名字都需要相同

eureka.instance:配置ip显示服务列表

eureka.client.service-url.defaultZone:注册中心地址,多个使用,分隔

spring boot约定大于配置,所以并不需要添加@EnableEurekaClient注解

双服务提供者(代码)

provider1

<?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.day1</groupId>
  <artifactId>provider-1</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>provider-1</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <parent>
    <artifactId>spring-cloud-eureka</artifactId>
    <groupId>com.day1</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <!-- 项目依赖 -->
  <dependencies>
    <!-- netflix eureka client 依赖 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- spring boot web 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok 依赖 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <scope>provided</scope>
    </dependency>

    <!-- spring boot test 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</project>

配置文件

server:
  port: 7070 # 端口

spring:
  application:
    name: service-provider # 应用名称(集群下相同)

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

provider2

<?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.day1</groupId>
  <artifactId>provider-2</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>provider-2</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <parent>
    <artifactId>spring-cloud-eureka</artifactId>
    <groupId>com.day1</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <!-- 项目依赖 -->
  <dependencies>
    <!-- netflix eureka client 依赖 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- spring boot web 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok 依赖 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <scope>provided</scope>
    </dependency>

    <!-- spring boot test 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</project>

配置

server:
  port: 7071 # 端口

spring:
  application:
    name: service-provider # 应用名称(集群下相同)

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

纯粹消费者配置说明

server:
  port: 9090 # 端口

spring:
  application:
    name: service-consumer # 应用名称

# 配置 Eureka Server 注册中心
eureka:
  client:
    register-with-eureka: false         # 是否将自己注册到注册中心,默认为 true
    registry-fetch-interval-seconds: 10 # 表示 Eureka Client 间隔多久去服务器拉取注册信息,默认为 30 秒
    service-url:                        # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

纯粹消费者不将自己注册到注册中心,并且定时拉取服务信息

双消费者(代码)

consomer1

<?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.day1</groupId>
  <artifactId>consumer-1</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>consumer-1</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <!-- 继承父依赖 -->
  <parent>
    <artifactId>spring-cloud-eureka</artifactId>
    <groupId>com.day1</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <!-- 项目依赖 -->
  <dependencies>
    <!-- netflix eureka client 依赖 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- spring boot web 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok 依赖 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <scope>provided</scope>
    </dependency>

    <!-- spring boot test 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</project>
server:
  port: 9090 # 端口

spring:
  application:
    name: service-consumer # 应用名称

# 配置 Eureka Server 注册中心
eureka:
  client:
    register-with-eureka: false         # 是否将自己注册到注册中心,默认为 true
    registry-fetch-interval-seconds: 10 # 表示 Eureka Client 间隔多久去服务器拉取注册信息,默认为 30 秒
    service-url:                        # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

consumer2

该application.yml的应用端口即可,依赖与consumer1相同

项目结构

添加服务

服务1

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@RestController
public class Provider1
{
    public static void main( String[] args )
    {
        SpringApplication.run(Provider1.class,args);
    }
    @Bean
    public RestTemplate tempLate(){return new RestTemplate();}
    //提供服务
    @RequestMapping("/")
    public String test(){
        return "服务提供者1";
    }
}

服务2

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@RestController
public class Provider2
{
    public static void main( String[] args )
    {
        SpringApplication.run(Provider2.class,args);
    }
    @Bean
    public RestTemplate tempLate(){return new RestTemplate();}
    //提供服务
    @RequestMapping("/")
    public String test(){
        return "服务提供者2";
    }
}

消费服务

对于服务的消费我们这里讲三种实现方式:

  • DiscoveryClient:通过元数据获取服务信息
  • LoadBalancerClient:Ribbon 的负载均衡器
  • @LoadBalanced:通过注解开启 Ribbon 的负载均衡器

DiscoveryClient

消费者1

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.LongStream;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@RestController
public class Consumer1
{
    public static void main( String[] args )
    {
        SpringApplication.run(Consumer1.class,args);
    }
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    //消费服务
    @Resource
    private RestTemplate restTemplate;
    @Autowired
    private DiscoveryClient discoveryClient;
    @RequestMapping("/")
    public String consumer(){
        StringBuffer sb = null;

        // 获取服务列表
        List<String> serviceIds = discoveryClient.getServices();
        if (CollectionUtils.isEmpty(serviceIds)){
            return null;
        }

        // 根据服务名称获取服务
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-provider");
        if (CollectionUtils.isEmpty(serviceInstances)){
            return null;
        }

        ServiceInstance si = serviceInstances.get(0);
        sb = new StringBuffer();
        sb.append("http://" + si.getHost() + ":" + si.getPort() + "/");

        // ResponseEntity: 封装了返回数据
        ResponseEntity<String> response = restTemplate.exchange(
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<String>() {});
        return response.getBody();
    }
}

消费者2同

重启生产者,消费者

访问两个生产者

消费

分别刷新

LoadBalancerClient

修改消费者1示例

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.LongStream;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@RestController
public class Consumer1
{
    public static void main( String[] args )
    {
        SpringApplication.run(Consumer1.class,args);
    }
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    //消费服务
    //DiscoveryClient服务消费======================================================
    @Resource
    private RestTemplate restTemplate;
    @Autowired
    private DiscoveryClient discoveryClient;
    @RequestMapping("/")
    public String consumer(){
        StringBuffer sb = null;

        // 获取服务列表
        List<String> serviceIds = discoveryClient.getServices();
        if (CollectionUtils.isEmpty(serviceIds)){
            return null;
        }

        // 根据服务名称获取服务
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-provider");
        if (CollectionUtils.isEmpty(serviceInstances)){
            return null;
        }

        ServiceInstance si = serviceInstances.get(0);
        sb = new StringBuffer();
        sb.append("http://" + si.getHost() + ":" + si.getPort() + "/");

        // ResponseEntity: 封装了返回数据
        ResponseEntity<String> response = restTemplate.exchange(
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<String>() {});
        return response.getBody();
    }
    //Ribbon代码服务消费==================================================
    @Autowired
    private LoadBalancerClient loadBalancerClient; // Ribbon 负载均衡器

    /**
     * 根据主键查询订单
     *
     */
    @RequestMapping("/ribbon")
    public String ribbonCode() {

        StringBuffer sb = null;

        // 根据服务名称获取服务
        ServiceInstance si = loadBalancerClient.choose("service-provider");
        if (null == si){
            return null;
        }

        sb = new StringBuffer();
        sb.append("http://" + si.getHost() + ":" + si.getPort() + "/");

        // ResponseEntity: 封装了返回数据
        ResponseEntity<String> response = restTemplate.exchange(
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<String>() {});
        return response.getBody();
    }
}

@LoadBalanced

启动类注入 RestTemplate 时添加 @LoadBalanced 负载均衡注解,表示这个 RestTemplate 在请求时拥有客户端负载均衡的能力。

这里我们改消费者2

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.List;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@RestController
public class Consumer2
{
    public static void main( String[] args )
    {
        SpringApplication.run(Consumer2.class,args);
    }
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    //消费服务
    @Resource
    private RestTemplate restTemplate;
    @Autowired
    private DiscoveryClient discoveryClient;
    @RequestMapping("/")
    public String consumer(){
        StringBuffer sb = null;

        // 获取服务列表
        List<String> serviceIds = discoveryClient.getServices();
        if (CollectionUtils.isEmpty(serviceIds)){
            return null;
        }

        // 根据服务名称获取服务
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-provider");
        if (CollectionUtils.isEmpty(serviceInstances)){
            return null;
        }

        ServiceInstance si = serviceInstances.get(0);
        sb = new StringBuffer();
        sb.append("http://" + si.getHost() + ":" + si.getPort() + "/");

        // ResponseEntity: 封装了返回数据
        ResponseEntity<String> response = restTemplate.exchange(
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<String>() {});
        return response.getBody();
    }
    /**
     * @LoadBalanced消费服务
     */
    @RequestMapping("/loadBalance")
    public String loadBananced(){
        // ResponseEntity: 封装了返回数据
        ResponseEntity<String> response = restTemplate.exchange(
                "http://service-provider/",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<String>() {});
        return response.getBody();
    }
}

Eureka 自我保护

启动自我保护条件

一般情况下,服务在 Eureka 上注册后,会每 30 秒发送心跳包,Eureka 通过心跳来判断服务是否健康,同时会定期删除超过 90 秒没有发送心跳的服务。

有两种情况会导致 Eureka Server 收不到微服务的心跳

  • 微服务自身的原因
  • 微服务与 Eureka 之间的网络故障

自我保护模式

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,同时提示一个警告。这种算法叫做 Eureka Server 的自我保护模式。

为什么要启动自我保护

  • 因为同时保留"好数据"与"坏数据"总比丢掉任何数据要更好,当网络故障恢复后,这个 Eureka 节点会退出"自我保护模式"。
  • Eureka 还有客户端缓存功能(也就是微服务的缓存功能)。即使 Eureka 集群中所有节点都宕机失效,微服务的 Provider 和 Consumer 都能正常通信。
  • 微服务的负载均衡策略会自动剔除死亡的微服务节点。

如何关闭自我保护

注册中心配置自我保护

eureka:
  server:
    enable-self-preservation: false # true:开启自我保护模式,false:关闭自我保护模式
    eviction-interval-timer-in-ms: 60000 # 清理间隔(单位:毫秒,默认是 60*1000)

Eureka 优雅停服

配置了优雅停服以后,将不需要 Eureka Server 中配置关闭自我保护。本文使用 actuator 实现。

添加依赖

服务提供者添加 actuator 依赖

<!-- spring boot actuator 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置文件

服务提供者配置度量指标监控与健康检查

# 度量指标监控与健康检查
management:
  endpoints:
    web:
      exposure:
        include: shutdown         # 开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true # 开启 shutdown 实现优雅停服

优雅停服

使用 POST 请求访问:http://localhost:7070/actuator/shutdown 效果如下

Eureka 安全认证

添加依赖

注册中心添加 security 依赖

<!-- spring boot security 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置文件

注册中心配置安全认证

spring:
  # 安全认证
  security:
    user:
      name: root
      password: 123456

修改访问集群节点的 url

注册中心的配置文件

# 配置 Eureka Server 注册中心
eureka:
  instance:
    hostname: eureka01            # 主机名,不配置的时候将根据操作系统的主机名来获取
    prefer-ip-address: true # 是否使用 ip 地址注册
    instance-id: ${
   spring.cloud.client.ip-address}:${
   server.port} # ip:port
  client:
    # 设置服务注册中心地址,指向另一个注册中心
    service-url:                  # 注册中心对外暴露的注册地址
      defaultZone: http://root:123456@localhost:8762/eureka/

服务提供者的配置文件

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true # 是否使用 ip 地址注册
    instance-id: ${
   spring.cloud.client.ip-address}:${
   server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8762/eureka/

服务消费者的配置文件

# 配置 Eureka Server 注册中心
eureka:
  client:
    register-with-eureka: false # 是否将自己注册到注册中心,默认为 true
    registry-fetch-interval-seconds: 10 # 表示 Eureka Client 间隔多久去服务器拉取注册信息,默认为 30 秒
    service-url:                        # 设置服务注册中心地址
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8762/eureka/

过滤 CSRF

Eureka 会自动化配置 CSRF 防御机制,Spring Security 认为 POST, PUT, and DELETE http methods 都是有风险的,如果这些 method 发送过程中没有带上 CSRF token 的话,会被直接拦截并返回 403 forbidden。

官方给出了解决的方法,具体可以参考 spring cloud issue 2754,里面有大量的讨论,这里提供两种解决方案。

首先注册中心配置一个 @EnableWebSecurity 配置类,继承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,然后重写 configure 方法。

方案一

使 CSRF 忽略 /eureka/** 的所有请求

package com.xxxx.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/** * 安全认证配置类 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
        super.configure(http); // 加这句是为了访问 eureka 控制台和 /actuator 时能做安全控制
        http.csrf().ignoringAntMatchers("/eureka/**"); // 忽略 /eureka/** 的所有请求
    }

}

方案二

保持密码验证的同时禁用 CSRF 防御机制

package com.xxxx.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/** * 安全认证配置类 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
        // 注意,如果直接 disable 的话会把安全验证也禁用掉
        http.csrf().disable().authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .httpBasic();
    }

}

访问