官网:https://spring.io/projects/spring-cloud
查看版本依赖信息:https://start.spring.io/actuator/info

发布计划:

  • BUILD-XXX:开发版,团队内部使用
  • M:里程碑版,同时标注 PRE,表示预览版
  • RC:候选发布版,正式发布版的前一个观察期
  • SR:正式发布版
  • GA:稳定版

Spring Cloud:一套微服务解决方案
image.png

Eureka

注册中心:

  • 目的:用于完成服务的注册与发现
  • 常见的注册中心:
    • Netflix Eureka
    • Alibaba Nacos
    • HashiCorp Consul
    • Apache Zookeeper

Eureka 文档:

Eureka 工作流程:
屏幕截图 2021-01-08 154510.png

单实例注册中心 eureka-server

1、创建Maven父项目:

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>2.2.4.RELEASE</version>
  5. </parent>
  6. <packaging>pom</packaging>
  7. <dependencyManagement>
  8. <dependencies>
  9. <dependency>
  10. <groupId>org.springframework.cloud</groupId>
  11. <artifactId>spring-cloud-dependencies</artifactId>
  12. <version>Hoxton.SR1</version>
  13. <type>pom</type>
  14. <scope>import</scope>
  15. </dependency>
  16. </dependencies>
  17. </dependencyManagement>

2、创建Eureka服务端:

  1. <dependencies>
  2. <!-- netflix eureka server 依赖 -->
  3. <dependency>
  4. <groupId>org.springframework.cloud</groupId>
  5. <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  6. </dependency>
  7. <!-- SpringWeb -->
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-starter-web</artifactId>
  11. </dependency>
  12. <!-- spring actuator -->
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-actuator</artifactId>
  16. </dependency>
  17. <!-- springboot测试 -->
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-test</artifactId>
  21. <exclusions>
  22. <exclusion>
  23. <groupId>org.junit.vintage</groupId>
  24. <artifactId>junit-vintage-engine</artifactId>
  25. </exclusion>
  26. </exclusions>
  27. </dependency>
  28. </dependencies>

3、创建启动类:

  1. @SpringBootApplication
  2. @EnableEurekaServer
  3. public class EurekaServerApp {
  4. public static void main(String[] args) {
  5. SpringApplication.run(EurekaServerApp.class, args);
  6. }
  7. }

4、创建配置文件 application.yml:

  1. server:
  2. port: 8761
  3. spring:
  4. application:
  5. name: eureka-server #应用名称
  6. # 配置eureka,单节点配置
  7. eureka:
  8. instance:
  9. hostname: localhost #主机名,不配置则根据操作系统的主机名来获取
  10. client:
  11. register-with-eureka: false #是否将自身注册到注册中心,默认true
  12. fetch-registry: false #是否从注册中心获取服务注册信息,默认为true
  13. service-url: #注册中心对外暴露的注册地址
  14. defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

5、点击运行后,访问:http://localhost:8761/

Eureka-server 注册中心集群

创建多个 Eureka 服务端并相互注册:

  • 同一个集群中,应用名称应保持一致 spring.application.name 属性一致
  • 在同一个集群 n 个服务端中,每个 server 都注册到其他的 n-1 个server

服务端一:

  1. server:
  2. port: 8760
  3. spring:
  4. application:
  5. name: eureka-server #应用名称(同一集群下相同)
  6. # 配置eureka,集群配置
  7. eureka:
  8. instance:
  9. hostname: eureka00 #主机名,不配置则根据操作系统的主机名来获取
  10. client:
  11. #设置服务注册中心,指向另一个注册中心
  12. service-url:
  13. defaultZone: http://localhost:8762/eureka/,http://localhost:8761/eureka/

服务端二:

  1. server:
  2. port: 8761
  3. spring:
  4. application:
  5. name: eureka-server #应用名称
  6. # 配置eureka,集群配置
  7. eureka:
  8. instance:
  9. hostname: eureka01 #主机名,不配置则根据操作系统的主机名来获取
  10. client:
  11. #设置服务注册中心,指向另一个注册中心
  12. service-url:
  13. defaultZone: http://localhost:8760/eureka/,http://localhost:8761/eureka/

服务端三:

  1. server:
  2. port: 8762
  3. spring:
  4. application:
  5. name: eureka-server #应用名称
  6. # 配置eureka,集群配置
  7. eureka:
  8. instance:
  9. hostname: eureka02 #主机名,不配置则根据操作系统的主机名来获取
  10. client:
  11. #设置服务注册中心,指向另一个注册中心
  12. service-url:
  13. defaultZone: http://localhost:8760/eureka/,http://localhost:8761/eureka/

其他属性设置

显示 IP + 端口:一个普通的 Netflix Eureka 实例注册的 ID 等于其主机名(即每个主机仅提供一项服务)

  • Spring Cloud Eureka 提供了默认值,${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id}:${server.port},即 主机名:应用名:应用端口
  • 也可以进行自定义:
    1. eureka:
    2. instance:
    3. prefer-ip-address: true #是否使用ip地址注册
    4. instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port

服务提供者 service-provider

1、创建项目,导入依赖:

  1. <dependencies>
  2. <!-- netflix eureka client 依赖 -->
  3. <dependency>
  4. <groupId>org.springframework.cloud</groupId>
  5. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  6. </dependency>
  7. <!-- SpringWeb -->
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-starter-web</artifactId>
  11. </dependency>
  12. <!-- spring actuator -->
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-actuator</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.projectlombok</groupId>
  19. <artifactId>lombok</artifactId>
  20. <version>1.18.16</version>
  21. </dependency>
  22. <!-- springboot测试 -->
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-test</artifactId>
  26. <exclusions>
  27. <exclusion>
  28. <groupId>org.junit.vintage</groupId>
  29. <artifactId>junit-vintage-engine</artifactId>
  30. </exclusion>
  31. </exclusions>
  32. </dependency>
  33. </dependencies>

2、创建启动类

  1. @SpringBootApplication
  2. // 开启EurekaClient注解,当前版本如果配置Eureka注册中心,默认会开启该注解
  3. //@EnableEurekaClient
  4. public class ServiceProviderApp {
  5. public static void main(String[] args) {
  6. SpringApplication.run(ServiceProviderApp.class, args);
  7. }
  8. }

3、创建配置文件

  1. server:
  2. port: 7000
  3. spring:
  4. application:
  5. name: service-provider #应用名称(集群下相同)
  6. eureka:
  7. instance:
  8. prefer-ip-address: true #是否使用ip地址注册
  9. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  10. client:
  11. service-url: #设置服务注册中心地址
  12. defaultZone: http://localhost:8760/eureka/,http://localhost:8761/eureka/,http://localhost:8761/eureka/

4、创建实体类

  1. @Data
  2. @NoArgsConstructor
  3. @AllArgsConstructor
  4. public class ProductDto implements Serializable {
  5. private Integer id;
  6. private String productName;
  7. private Integer productNum;
  8. private Double productPrice;
  9. }

5、编写服务

  1. @Service
  2. public class ProductService {
  3. public List<ProductDto> selectProductList() {
  4. return Arrays.asList(
  5. new ProductDto(1, "华为手机", 2, 5888D),
  6. new ProductDto(2, "联想笔记本", 1, 6888D),
  7. new ProductDto(3, "小米平板", 5, 2666D)
  8. );
  9. }
  10. }

6、编写控制器

  1. @RestController
  2. @RequestMapping("/product")
  3. public class ProductController {
  4. @Autowired
  5. private ProductService productService;
  6. @GetMapping("/list")
  7. public List<ProductDto> selectProductList(){
  8. return productService.selectProductList();
  9. }
  10. }

创建服务消费者 service-consumer

1、创建项目,引入依赖,与服务提供者 Service-Provider 一致
2、创建启动类
3、创建配置项

  1. server:
  2. port: 8000
  3. spring:
  4. application:
  5. name: service-consumer #应用名称(集群下相同)
  6. eureka:
  7. instance:
  8. prefer-ip-address: true #是否使用ip地址注册
  9. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  10. client:
  11. registry-fetch-interval-seconds: 10 #每隔多久去服务器拉取注册信息,默认30s
  12. service-url: #设置服务注册中心地址
  13. defaultZone: http://localhost:8760/eureka/,http://localhost:8761/eureka/,http://localhost:8761/eureka/

4、创建实体类

  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class Order implements Serializable {
  5. private Integer id;
  6. private String orderNo;
  7. private String orderAddress;
  8. private Double totalPrice;
  9. private List<ProductDto> productDtoList;
  10. }
  11. @Data
  12. @AllArgsConstructor
  13. @NoArgsConstructor
  14. public class ProductDto implements Serializable {
  15. private Integer id;
  16. private String productName;
  17. private Integer productNum;
  18. private Double productPrice;
  19. }

5、创建服务接口:

  1. public interface OrderService {
  2. Order selectOrderById(Integer id);
  3. }

对于服务的消费有三种实现方式:

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

使用 DiscoveryClient:

  1. SpringBoot不提供任何自动配置的 RestTemplate bean,所以需要在启动类中注入 RestTemplate

    1. @SpringBootApplication
    2. public class ServiceConsumer01 {
    3. public static void main(String[] args) {
    4. SpringApplication.run(ServiceConsumer01.class, args);
    5. }
    6. @Bean
    7. public RestTemplate restTemplate(){
    8. return new RestTemplate();
    9. }
    10. }
  2. 创建服务实现类 ```java import org.springframework.cloud.client.discovery.DiscoveryClient; //导入Spring提供的DiscoveryClient,而不是Eureka自带的

@Service public class OrderServiceImpl implements OrderService {

  1. @Autowired
  2. private RestTemplate restTemplate;
  3. @Autowired
  4. private DiscoveryClient discoveryClient;
  5. @Override
  6. public Order selectOrderById(Integer id) {
  7. return new Order(id, "order-001", "中国", 123357D,
  8. selectProductListByDiscoveryClient());
  9. }
  10. private List<ProductDto> selectProductListByDiscoveryClient() {
  11. StringBuffer sb = null;
  12. // 获取服务列表
  13. List<String> services = discoveryClient.getServices();
  14. if (CollectionUtils.isEmpty(services)) {
  15. return null;
  16. }
  17. // 根据服务名称获取服务
  18. List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
  19. if (CollectionUtils.isEmpty(instances)) {
  20. return null;
  21. }
  22. ServiceInstance si = instances.get(0);
  23. sb = new StringBuffer();
  24. sb.append("http://")
  25. .append(si.getHost())
  26. .append(":")
  27. .append(si.getPort())
  28. .append("/product/list");
  29. // 使用RestTemplate进行请求
  30. ResponseEntity<List<ProductDto>> response = restTemplate.exchange(
  31. sb.toString(),
  32. HttpMethod.GET,
  33. null,
  34. new ParameterizedTypeReference<List<ProductDto>>() {});
  35. return response.getBody();
  36. }

}

  1. 使用LoadBanlancerClientRibbon负载均衡
  2. ```java
  3. import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
  4. @Service
  5. public class OrderServiceImpl implements OrderService {
  6. @Autowired
  7. private RestTemplate restTemplate;
  8. @Autowired
  9. private LoadBalancerClient loadBalancerClient; // Ribbon负载均衡
  10. @Override
  11. public Order selectOrderById(Integer id) {
  12. return new Order(id, "order-001", "中国", 123357D,
  13. selectProductListByDiscoveryClient());
  14. }
  15. private List<ProductDto> selectProductListByDiscoveryClient() {
  16. StringBuffer sb = null;
  17. // 根据服务名称获取一个服务
  18. ServiceInstance si = loadBalancerClient.choose("service-provider");
  19. if (null == si) {
  20. return null;
  21. }
  22. // 请求地址
  23. sb = new StringBuffer();
  24. sb.append("http://")
  25. .append(si.getHost())
  26. .append(":")
  27. .append(si.getPort())
  28. .append("/product/list");
  29. // 封装了返回数据
  30. ResponseEntity<List<ProductDto>> response = restTemplate.exchange(
  31. sb.toString(),
  32. HttpMethod.GET,
  33. null,
  34. new ParameterizedTypeReference<List<ProductDto>>() {});
  35. return response.getBody();
  36. }
  37. }

@LoadBalanced:通过注解开启Ribbon的负载均衡

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

    1. @SpringBootApplication
    2. public class ServiceConsumer01 {
    3. public static void main(String[] args) {
    4. SpringApplication.run(ServiceConsumer01.class, args);
    5. }
    6. @Bean
    7. // 负载均衡注解
    8. @LoadBalanced
    9. public RestTemplate restTemplate(){
    10. return new RestTemplate();
    11. }
    12. }
  2. 直接通过应用名称调用:

    1. @Service
    2. public class OrderServiceImpl implements OrderService {
    3. @Autowired
    4. private RestTemplate restTemplate;
    5. @Override
    6. public Order selectOrderById(Integer id) {
    7. return new Order(id, "order-001", "中国", 123357D,
    8. selectProductListByDiscoveryClient());
    9. }
    10. private List<ProductDto> selectProductListByDiscoveryClient() {
    11. // 封装了返回数据
    12. ResponseEntity<List<ProductDto>> response = restTemplate.exchange(
    13. // 通过服务名称进行调用
    14. "http://service-provider/product/list",
    15. HttpMethod.GET,
    16. null,
    17. new ParameterizedTypeReference<List<ProductDto>>() {
    18. });
    19. return response.getBody();
    20. }
    21. }

6、控制器

  1. @RestController
  2. public class OrderController {
  3. @Autowired
  4. private OrderService orderService;
  5. @GetMapping("/{id}")
  6. public Order selectOrderById(@PathVariable("id") Integer id) {
  7. return orderService.selectOrderById(id);
  8. }
  9. }

7、访问:http://localhost:8000/1

Eureka 架构原理

image.png

  • Register:服务注册,将自身的 IP 和端口注册给 Eureka
  • Renew:服务续约,发送心跳包,每 30 秒一次,告诉 Eureka 自身存活,如果超过 90s 还没有发送心跳,则宕机
  • Cancel:服务下线,当 Provider 关闭时会向 Eureka 发送消息,把自己从服务列表中删除,防止 Consumer 调用到不存在的服务
  • Get Register:获取服务注册列表,用于获取其他服务列表
  • Replicate:集群中数据同步,Eureka 集群中的数据复制和同步
  • Make Remote Call:远程调用,完成服务的远程调用过程

CAP 原则

image.png
CAP 原则:指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),这三个要素最多只能同时实现两点,不可能三者兼顾

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • 可用性(A):保证每个请求不管成功或者失败都有响应
  • 分区容忍性(P):系统中任意信息的丢失或失败不会影响系统的继续运作

取舍策略:

  • CA:放弃了分区容错性,即每份数据只保存一份,则可以保证强一致性和可用性,如单体应用的 mysql
  • CP:不要求可用,要求分区数据的强一致性是可能的,即在数据同步完成之前用户不能正常访问,如 Redis、HBase等,对这些分布式数据库来说,数据一致性是基本要求
  • AP:要高可用和分区容错,则会放弃强一致性,一旦分区发生节点之间的故障,为了该可用,每个节点只能用本地数据提供服务,会造成全局数据的不一致

Eureka 自我保护

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

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

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

关闭自我保护:

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

Eureka 优雅停服

配置了优雅停服以后,将不需要 Eureka Server 中配置关闭自我保护

1、添加依赖:

  1. <!-- spring actuator -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-actuator</artifactId>
  5. </dependency>

2、配置文件:服务提供者配置度量指标监控与健康检查

  1. # 度量指标监控与健康检查
  2. management:
  3. endpoints:
  4. web:
  5. exposure:
  6. include: shutdown #开启shutdown端点访问
  7. endpoint:
  8. shutdown:
  9. enabled: true #开启shutdown实现优雅停服

3、使用 POST 请求访问: http://localhost:8000/actuator/shutdown,就能停止服务
image.png

Eureka 安全认证

1、添加依赖:在三个Eureka Server中添加Spring Security依赖

  1. <!-- spring-boot-security -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>

2、配置文件:注册中心配置安全认证

  1. spring:
  2. #安全认证
  3. security:
  4. user:
  5. name: root
  6. password: 123456

3、修改访问集群节点的url:即在主机前使用 username:password@ 标注访问账号密码

  1. # 配置eureka,集群配置
  2. eureka:
  3. instance:
  4. hostname: eureka00 #主机名,不配置则根据操作系统的主机名来获取
  5. prefer-ip-address: true #是否使用ip地址注册
  6. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  7. client:
  8. #设置服务注册中心,指向另一个注册中心
  9. service-url:
  10. defaultZone: http://root:123456@localhost:8762/eureka/,http://root:123456@localhost:8761/eureka/

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

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

方案一:使CSRF忽略 /eureka/** 的所有请求

  1. @EnableWebSecurity
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. // 访问eureka控制台和/actuator时能作安全控制
  6. super.configure(http);
  7. // 忽略/eureka/**的所有请求
  8. http.csrf().ignoringAntMatchers("/eureka/**");
  9. }
  10. }

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

  1. @EnableWebSecurity
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. // 直接disable的话会把安全验证也禁掉
  6. http.csrf().disable().authorizeRequests()
  7. .anyRequest()
  8. .authenticated()
  9. .and()
  10. .httpBasic();
  11. }
  12. }

5、访问:http://localhost:8761/