Spring Cloud

没有什么是加一层解决不了的,如果解决不了,那就再加一层

微服务:提倡单一的应用程序划分为一组小的服务,服务之间相互协调、配合。可以看作是Spring Boot开发的一个又一个模块

image-20220825195727582

Spring Cloud:分布式微服务架构的一站式解决方案,多种微服务架构的集合体,俗称为微服务全家桶

热部署DevTools

引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

添加配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

开启自动编译:

image-20220826103454713

或者使用jrebel

多个服务调用

需要包装多个模块中的实体类相同

此时有两个服务在运行:

  • 服务1为支付模块,提供添加支付、获取支付、删除支付的功能,主要包括servicecontrollerbeanmapper
  • 服务2使用服务1中的服务,需要bean包、config包、controller
    • config包中配置了RestTemplate

RestTemplate

提供了多种访问远程Http服务的方法,是Spring提供的用于访问rest服务的客户端模板工具集,提供了边界访问restful服务模板类

@Configuration
public class MyConfig {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

服务1:

package com.xiaoxu.cloud.controller;

import com.xiaoxu.cloud.bean.Payment;
import com.xiaoxu.cloud.bean.Result;
import com.xiaoxu.cloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
    private PaymentService service;

    @PostMapping("/")
    public Result<Integer> insert(@RequestBody Payment payment) {
        int result = service.insertPayment(payment);
        log.info("插入数据:{}, 结果:{}", payment, result);
        if (result > 0) {
            return new Result<>(200, "添加成功", result);
        }
        return new Result<>(400, "添加失败");

    }

    @GetMapping("/")
    public Result<List<Payment>> getAllPayments() {
        return new Result<>(400, "获取成功", service.getAllPayments());
    }

    @GetMapping("/{id}")
    public Result<Payment> getPaymentById(@PathVariable Integer id) {
        Payment payment = service.queryPaymentById(id);
        if (payment == null) {
            return new Result<>(400, "不存在");
        }
        return new Result<>(200, "获取成功", payment);
    }

    @DeleteMapping("/")
    public Result<Integer> deletePaymentById(@RequestBody Payment payment) {
        int i = service.deletePayment(payment);
        if (i > 0) {
            return new Result<>(200, "删除成功", i);
        }
        return new Result<>(400, "删除失败");
    }

    public PaymentController(PaymentService service) {
        this.service = service;
    }
}

服务2:

@RestController
@RequestMapping("/order")
public class OrderController {
    // 必须带有http等协议,结尾必须有/
    public static final String PAYMENT_URL = "http://localhost:8001/payment/";
    private RestTemplate template;

    @PostMapping("/payment")
    public Result<Payment> insert(@RequestBody Payment payment) {
        return template.postForObject(PAYMENT_URL, payment, Result.class);
    }

    @GetMapping("/payment")
    public Result<List<Payment>> getAllPayment() {
        return template.getForObject(PAYMENT_URL, Result.class);
    }

    public OrderController(RestTemplate template) {
        this.template = template;
    }
}

需要保证返回的数据结构相同,这个时候请求服务1和服务2有相同的效果

重构

可以看到两个服务中含有重复的bean,可以新建一个模块,把重复的东西放到新的模块中

运行mavenclean,再运行install即可将这个模块安装到本地库,删掉之前重复的类

<dependency>
    <groupId>组名</groupId>
    <artifactId>项目名</artifactId>
    <version>版本</version>
    <scope>compile</scope>
</dependency>

完成类型迁移后就直接可以使用了

Eureka

目前已停止更新。

读音yo͞oˈrēkə

服务治理:每个服务与服务之间的关系比较复杂,所以需要服务治理管理服务与服务之间的依赖关系,实现服务调用、负载均衡、容错、服务的发现与注册。

服务注册:采用客户端服务器的设计架构,作为服务注册功能的服务器,是服务注册中心

Eureka Server提供服务、注册服务

Eureka Client通过注册中心进行访问

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

yml:

eureka:
  instance:
  	# 实例名称
    hostname: localhost
  client:
    # false 代表不向注册中心自己注册自己
    register-with-eureka: false
    # false代表自己就是注册中心,职责是维护服务实例,不需要去检索服务
    fetch-registry: false
    service-url:
    # 设置与Eureka Server交互的地址查询服务和注册服务都依赖这个地址
      defaultZone: http://${eureka.instance.hostname}:${serve.port}/eureka

标注注册中心服务器:

@SpringBootApplication
@EnableEurekaServer
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-cloud.version>2021.0.3</spring-cloud.version>

</properties>

<dependencies>
    <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.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</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>

<dependencyManagement>
    <dependencies>
        <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>

微服务接入Eureka

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

添加配置:

eureka:
  instance:
    hostname: localhost
  client:
    # 为true代表入驻,否则不入驻
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

在启动类上添加@EnableEurekaClient注解

服务注册:将服务信息注册到注册中心

服务发现:从注册中心获取服务的信息

实质:就是键值对,key为服务名称,value为服务的地址

Eureka集群

微服务RPC远程服务的调用核心是高可用,实现负载均衡和故障容错

原理是互相注册,例如有两个集群:12,那么1注册22注册1。如果有三个集群,123,那么1注册232注册133注册12,也就是每个单机除了自己需要包含其他的服务单机

假设现在有两个集群,现在只需要做到,那么1注册22注册1

  • server:
      port: 7002
    spring:
      application:
        name: cloud-eureka-server7002
    eureka:
      instance:
        hostname: server7002
      client:
        register-with-eureka: false
        fetch-registry: false
        service-url:
          defaultZone: http://localhost:7001/eureka
    
  • server:
      port: 7001
    
    eureka:
      instance:
        hostname: server7001
      client:
        register-with-eureka: false
        fetch-registry: false
        service-url:
          defaultZone: http://localhost:7002/eureka
    
    spring:
      application:
        name: cloud-eureka-server7001
    

而客户端需要填写这两个集群的地址:

  • server:
      port: 8001
    
    spring:
      application:
        name: cloud-payment-service
      datasource:
      mvc:
        hiddenmethod:
          filter:
            enabled: true
    eureka:
      instance:
        hostname: localhost
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
          defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
    
  • server:
      port: 80
    
    spring:
      application:
        name: cloud-consumer-order80
    
    eureka:
      instance:
        hostname: localhost
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
          defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
    

服务提供者的集群

这里的服务提供者是payment,多个服务提供者集群的spring.applicatio.name相同

多个服务的端口号不同,其他地方保持一致

在客户端中:

  • 配置类RestTemplate返回实例的方法中添加负载均衡@LoadBalanced注解

    • @Configuration
      public class MyConfig {
          @Bean
          @LoadBalanced
          public RestTemplate getRestTemplate() {
              return new RestTemplate();
          }
      }
      
  • 修改原本的url为服务名称

    • @RestController
      @RequestMapping("/order")
      public class OrderController {
          // 必须带有http等协议,结尾必须有/,修改为服务名称
          public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE/payment/";
          private RestTemplate template;
      
          @PostMapping("/payment")
          public Result<Payment> insert(@RequestBody Payment payment) {
              return template.postForObject(PAYMENT_URL, payment, Result.class);
          }
      
          @GetMapping("/payment")
          public Result<List<Payment>> getAllPayment() {
              return template.getForObject(PAYMENT_URL, Result.class);
          }
      
          public OrderController(RestTemplate template) {
              this.template = template;
          }
      }
      

这时候会交替使用不同的服务,也是轮询。

消费者这时候只关注服务名称,无需关注服务的IP地址

balance中文为均衡、平衡、余额,读音bælənsloadBlance为负载均衡

actuator信息完善

actuator中文为制动、传动,读音为ˈæktjuˌeɪtər

eureka服务页面显示的信息过于杂乱

没有这配置之前显示的内容大致如下:

Application AMIs Availability Zones Status
CLOUD-CONSUMER-ORDER80 n/a (1) (1) UP (1) - LAPTOP-BV21677A:cloud-consumer-order80:80
CLOUD-PAYMENT-SERVICE n/a (2) (2) UP (2) - LAPTOP-BV21677A:cloud-payment-service:8002 ,LAPTOP-BV21677A:cloud-payment-service:8001

修改服务显示的内容、显示IP

eureka:
  instance:
    instance-id: 自定义名称
    prefer-ip-address: true

服务发现Discovery

对外暴露这个服务的信息,仅能应用于EurekaClient上不能应用于EurekaService

需要在主启动类上添加@EnableDiscoveryClient,在需要的位置注入org.springframework.cloud.client.discovery.DiscoveryClient即可

例如:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/status")
@Slf4j
public class StatusController {
    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/service")
    public List<String> getDiscoveryService() {
        discoveryClient.getServices().forEach(it -> {
            log.info("************{}", it);
        });
        return discoveryClient.getServices();
    }

    @GetMapping("/service/{name}")
    public List<ServiceInstance> getInstance(@PathVariable String name) {
        List<ServiceInstance> instances = discoveryClient.getInstances(name);
        log.info("instance = {} ", instances);
        return instances;
    }
}

Eureka自我保护

可以在管理页面看到:

以下红字:

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

这就说明进入了保护模式

Eureka Server将会尝试保护服务注册表中的信息,不在删除服务注册表中的数据,也就是不会注销任何微服务

也就是说:某一个时刻某个微服务不能用了,Eureka不会立即清理,依旧会保存这个微服务的信息,默认将在90s后进行注销这个微服务。在短时间内丢失大量的客户端时,Eureka会任务可能发生了网络故障,那么将会进入自动保护模式。宁可保留错误的服务注册信息,也不会盲目的注销任何可能健康的服务实例

禁用自我保护

服务修改以下配置即可:

  • 第一项代表是否关闭保护模式
  • 第二项代表心跳超时时间(毫秒)
eureka:
    server:
      enable-self-preservation: false
      eviction-interval-timer-in-ms: 200

客户端修改发送心跳的频率

修改以下配置:

  • 第一项代表间隔秒数发送心跳
  • 第二项代表收到最后一个心跳等待的时间上限
eureka:
  instance:
    lease-renewal-interval-in-seconds:秒数
    lease-expiration-duration-in-seconds: 秒数

Consul

consul中文为领事,读音为ˈkɑːnsl,是一套开源的分布式服务发现和配置管理系统,由HashiCrop使用GO开发

提供了服务治理、配置中心、控制总线等功能,可以根据需要使用这些功能,支持跨数据中心的WAN广域网集群,提供图形界面,跨平台,支持:

  • 服务发现:提供HTTP和DNS两种方式
  • 健康检测,支持多种方式
  • KV存储:key-value
  • 多数中心
  • 可视化Web界面

下载安装:https://www.consul.io/downloads

启动:

consul  agent -dev

启动后打开:http://localhost:8500即可进入web管理页面

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

在主启动类上添加@EnableDiscoveryClient注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Consul8006pplication {
    public static void main(String[] args) {
        SpringApplication.run(Consul8006pplication.class, args);
    }
}

配置文件按照以下格式填写即可:

server:
  port: 8006
spring:
  application:
    name: consul-provider-payment-8006
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

如果搭建服务器集群,也只需要保证名称相同即可

客户端的配置和服务器的配置基本相同,跟之前一样,也需要RestTemplate

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class MyConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

URL依旧是需要填写服务名称标识

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/consumer")
public class MyController {
    public static final String URL = "http://consul-provider-payment-8006/";

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/")
    public Map<String, String> load() {
        return restTemplate.getForObject(URL + "payment/", HashMap.class);
    }
}

Ribbon

中文为丝带、带子、飘带,读音为ˈribən

NetFlix提供的实现客户端负载均衡的工具

Nginx是使用请求转发来实现的负载均衡(负载均衡 + RestTemplate),Ribbon是在调用时实现的负载均衡

可以和eureka等结合起来

工作过程:

  • 选择一个负载比较小的注册中心服务器
  • 根据用户的策略,在注册中心的服务注册列表选择一个服务
    • 提供了多种策略:轮询、随机、权重等策略

可以发现即使没有主动引入相关的负载均衡的依赖也能做到负载均衡,这是因为间接引入了org.springframework.cloud.loadbalancer

RestTemplate

get/postForObject:返回对象为响应体中的数据转化的对象,可以理解为JSON

get/postForEntity:返回ResponseEntity对象,包含了响应的一些信息,例如响应头、状态码、响应体等

@GetMapping("/payment2")
public ResponseEntity<Result> getAllPayment2() {
    ResponseEntity<Result> forEntity = template.getForEntity(PAYMENT_URL, Result.class);
    HttpHeaders headers = forEntity.getHeaders();
    log.info("headers = {}", headers);
    log.info("body = {}", forEntity.getBody());
    log.info("code = {}", forEntity.getStatusCode());
    log.info("entity = {}", forEntity);
    log.info("是否响应4XX:{}", forEntity.getStatusCode().is4xxClientError());
    log.info("是否响应2XX:{}", forEntity.getStatusCode().is2xxSuccessful());
    return forEntity;
}
2022-08-28 09:37:23.884  INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController     : headers = [Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Sun, 28 Aug 2022 01:37:23 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]
2022-08-28 09:37:23.884  INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController     : body = Result(code=400, message=获取成功8001, data=[{id=1, serial=aaaaaa}, {id=3, serial=0000000}])
2022-08-28 09:37:23.884  INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController     : code = 200 OK
2022-08-28 09:37:23.884  INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController     : entity = <200,Result(code=400, message=获取成功8001, data=[{id=1, serial=aaaaaa}, {id=3, serial=0000000}]),[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Sun, 28 Aug 2022 01:37:23 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
2022-08-28 09:37:23.884  INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController     : 是否响应4XX:false
2022-08-28 09:37:23.884  INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController     : 是否响应2XX:true

自定义负载均衡的规则

自定义的配置类不能放在@ComponentScan所扫描的当前包或者子包下,否则这个配置类将会被所有的客户端共享,得不到特殊化定制的目的,也就是放到不能够被SpringBoot扫描到的地方,不能添加@Configuration注解

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

public class LoadBalanceConfig {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                                   LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

在主启动类上指定所使用的规则

@SpringBootApplication
@EnableEurekaClient
@LoadBalancerClient(value = "需要使用的规则的服务标识", configuration = LoadBalanceConfig.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

原理:第n次请求 % 服务总数

以下为源码的信息,在第11-13行做的就是这个工作

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
   if (instances.isEmpty()) {
      if (log.isWarnEnabled()) {
         log.warn("No servers available for service: " + serviceId);
      }
      return new EmptyResponse();
   }

   // Ignore the sign bit, this allows pos to loop sequentially from 0 to
   // Integer.MAX_VALUE
   int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;

   ServiceInstance instance = instances.get(pos % instances.size());

   return new DefaultResponse(instance);
}

OpenFeign

feign中文为假装、伪装,读音为feɪn

用在客户端,也是进行服务调用,可以和以上组件结合起来实现负载均衡。使编写Http客户端变得更容易,可以帮助我们定义和实现依赖服务接口的定义,只需要一个注解即可完成绑定

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.1.3</version>
</dependency>

application.yml保持不变,还是要有一个依赖的注册中心

使用步骤:

  • 在主启动类上添加@EnableFeignClients

    • import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
      import org.springframework.cloud.openfeign.EnableFeignClients;
      @SpringBootApplication
      @EnableFeignClients
      @EnableDiscoveryClient
      public class ConsumerOpenFeign80Application {
          public static void main(String[] args) {
              SpringApplication.run(ConsumerOpenFeign80Application.class, args);
          }
      }
      
  • 自定义接口,添加@Compoent@FeignClient("服务标识名称"),在方法上提供与服务请求相同的路径(@XxxMapping("路径"))、返回值、参数

    • import com.xiaoxu.api.bean.Payment;
      import com.xiaoxu.api.bean.Result;
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.stereotype.Component;
      import org.springframework.web.bind.annotation.GetMapping;
      
      import java.util.List;
      
      @Component
      @FeignClient("CLOUD-PAYMENT-SERVICE")
      public interface MyFeign {
          @GetMapping("/payment/")
          Result<List<Payment>> getAllPayments();
      }
      
  • 最后在需要的地方写自己的业务逻辑,然后注入以上接口

    • import com.xiaoxu.api.bean.Payment;
      import com.xiaoxu.api.bean.Result;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      import java.util.List;
      
      @RestController
      @RequestMapping("/feign")
      public class MyController {
          @Autowired
          private MyFeign myFeign;
      
          @GetMapping("/")
          public Result<List<Payment>> getAllPayments() {
              return myFeign.getAllPayments();
          }
      }
      

Feign自带有负载均衡的功能

如果有路径变量,必须要指定路径名称

@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface MyFeign {
    @GetMapping("/payment/timeout/{time}")
    // 路径中指定名称,否则抛异常
    String timeout(@PathVariable("time") Integer time);
}

超时控制

默认是连接超时为10s,读取超时60s

配置:

feign:
  client:
    config:
      default:
        ConnectTimeOut: 5000
        ReadTimeOut: 5000

日志

提供了日志打印功能,可以通过调整日志级别来了解FeignHttp请求细节

分为:

  • NONE默认,不显示日志
  • BASIC仅显示请求方法、URL、响应状态码、执行时间
  • HEADERS除了BASIC之外,还有请求和响应的头
  • FULL除了HEADERS还有请求和响应的正文以及元数据

设置接口所在的日志等级为Debug

logging:
  level:
    com.xiaoxu.cloud.feign.controller.MyFeign: debug

方式1:

配置文件设置

feign:
  client:
    config:
      default:
        logger-level: full

方式2:

配置类

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

Hystrix

复杂的分布式体系结构的应用程序如果有非常多,如果其中一个失败,那么所有的依赖都会失败

Hystrix是处理分布式系统的延迟和容错的开源库,能够保证在一个依赖出问题的情况下不会导致整体的服务失败,避免级联故障,当某个服务单元发生故障后,向调用方返回一个符合预期的、备选的响应,而不是长时间等待或者抛出无法处理的异常,保证服务调用方的线程不会长时间、不必要的占用,从而有效避免故障的蔓延或者雪崩,可以完成:

  • 服务降级
  • 服务熔断
  • 接近的实时监控
  • 限流
  • 隔离等

已停更

  • 服务降级(fallback
    • 如果出现异常情况需要给出响应结果。例如给出“服务器忙”的提示
    • 出现降级的情况:
      • 程序运行异常
      • 超时
      • 服务熔断发生降级
      • 线程池满了
  • 服务熔断(break
    • 如果服务达到最大的服务访问后,拒绝访问,调用服务降级的方法
  • 服务限流(flowlimit
    • 防止一窝蜂的请求,排队进行请求,有序进行

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.10.RELEASE</version>
</dependency>

服务端和消费端都可以使用

服务降级

使用注解:@HystrixCommand可设置超时后调用后备的方法,只要服务不可用了或者抛出了异常就会立即进行服务降级

  • fallbackMethod为兜底的方法
@HystrixCommand(fallbackMethod = "handlerMethod", commandProperties = {
        @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "3000")
})
public String abnormal(@PathVariable Integer time) throws InterruptedException {
    log.info("访问不正常的方法,时间延迟为:{}秒", time);
    Thread.sleep(time * 1000);
    log.info("执行完成");
    return "延迟" + time + "秒";
}

public String handlerMethod(Integer time) {
    return "出错了,你设置的超时时间是" + time + "秒";
}

还需要在主启动类上添加@EnableHystrix注解

客户端和服务端都可以进行服务降级,但一般都是在服务端进行服务降级

全局服务降级

在类上添加@DefaultProperties(defaultFallback = "默认的处理方法")注解,指定这个类的默认全局处理方法

在需要进行服务降级的方法上添加@HystrixCommand,不需要填写任何的参数

如果指定了专门的处理方法,那么优先被专门的方法处理

@RestController
@RequestMapping("/hystrix")
@Slf4j
@DefaultProperties(defaultFallback = "defaultHandlerMethod")
public class MyController {
    @HystrixCommand
    @GetMapping("/normal")
    public String routine() {
        log.info("访问正常的方法");
        if (new Random().nextInt(100) % 2 == 0) {
            log.info("尝试抛出异常");
            throw new RuntimeException();
        }
        return "正常方法";
    }

    @GetMapping("/abnormal/{time}")
    @HystrixCommand(fallbackMethod = "handlerMethod", commandProperties = {
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "3000")
    })
    public String abnormal(@PathVariable Integer time) throws InterruptedException {
        log.info("访问不正常的方法,时间延迟为:{}秒", time);
        Thread.sleep(time * 1000);
        log.info("执行完成");
        return "延迟" + time + "秒";
    }

    public String handlerMethod(Integer time) {
        return "出错了,你设置的超时时间是" + time + "秒";
    }

    public String defaultHandlerMethod() {
        return "这是默认的处理错误的兜底方法";
    }

}

统配服务降级

在客户端进行降级时,可以很方便针对使用服务端的方法进行一一配置降级,先添加配置:

feign:
  circuitbreaker:
    enabled: true

统一进行修改hysrix的超时时间:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 9000

使用自定义的类实现@FeignClient注解标注的接口,并且自定义类中的返回值就为服务降级的结果,并指定fallback为自定义类

@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX8001", fallback = ApiHandler.class)
public interface Api {
    @GetMapping("/hystrix/normal")
    String normal();

    @GetMapping("/hystrix/abnormal/{time}")
    String abnormal(@PathVariable("time") Integer time);
}
import org.springframework.stereotype.Component;

@Component
public class ApiHandler implements Api{
    @Override
    public String normal() {
        return "服务端的normal出现异常了-来自客户端的提示";
    }

    @Override
    public String abnormal(Integer time) {
        return "服务端的abnormal出问题了-来自客户端的提示";
    }
}

如果服务端崩了,将会直接由服务降级的方法进行直接的处理,如果服务端也进行了降级的处理,优先使用服务端的结果

服务熔断

熔断后将会调用服务降级,是应对雪崩效应的微服务链路保护机制,当某个微服务出错或者响应时间过长时,将进行服务降级,熔断这个微服务的调用

默认是5秒内20次调用失败将会启动熔断,依旧 使用@HystrixCommand注解

circuit中文为电路,读音为ˈsərkət

以下代码的含义是:10秒内10次请求有60%是失败的就触发服务熔断

@GetMapping("/break/{num}")
@HystrixCommand(fallbackMethod = "breakHandler", commandProperties = {
        @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ENABLED, value = "true"), // 开启服务熔断
        @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), // 请求次数
        @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "10000"), // 窗口期
        @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "60"), // 失败的百分比
})
public String serviceBreak(@PathVariable Integer num) {
    if (num < 0) {
        throw new RuntimeException();
    }
    return "已收到/hystrix/break/" + num + " 的请求";
}

public String breakHandler(Integer num) {
    return "已被服务降级,num = " + num;
}

如果位于熔断的状态,时间窗口期内让其通过一次,如果成功了就自动改变为非熔断状态

Gateway

Spring Cloud中的网关组件,底层使用netty,位于Nginx之后,作为微服务的入口

  • 路由:是构建网关的基本模块,由一系列的断言和过滤器组成
  • 断言:如果请求与断言相匹配则进行路由
  • 过滤:使用过滤器可以将请求 被路由前或者路由后对请求进行修改

客户端向Gateway发出请求,Gateway Handler Mapping中找到与请求相匹配的路由后,将其发送到Web Handler,在根据指定的过滤器链将请求发送到实际的业务逻辑

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

配置:

spring:
  application:
  cloud:
    gateway:
      routes:
        - id: normal
          uri: http://localhost:8001
          predicates:
            - Path=/hystrix/normal
        - id: abnormal
          uri: http://localhost:8001
          predicates:
            - Path=/hystrix/abnormal/**

配置路径和URL即可,uri一定要写http://

添加网关后的效果类似于Nginx的反向代理的效果,也可以起到隐藏端口的效果

也可以使用编码的方式配置:

  • @Configuration
    public class GatewayConfig {
        @Bean
        public RouteLocator consumerRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
            return routeLocatorBuilder.routes()
                    .route("baidu", f -> f.path("/baidu")
                            .uri("https://ip.skk.moe/"))
                    .build();
        }
    }
    

动态路由

可以发现上边的IP地址是写死的,所以做到负载均衡,一次只能访问一个服务器,可以利用服务注册中心所提供的服务名实现动态路由

需要开启发现:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true

修改uri,由原来的uri: http://localhost:port修改为uri: lb://服务名,其他地方保持不变:

  • spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
            - id: normal
              uri: lb://CLOUD-PROVIDER-HYSTRIX8001
              predicates:
                - Path=/hystrix/normal
            - id: abnormal
              uri: lb://CLOUD-PROVIDER-HYSTRIX8001
              predicates:
                - Path=/hystrix/abnormal/**
            - id: break
              uri: lb://CLOUD-PROVIDER-HYSTRIX8001
              predicates:
                - Path=/hystrix/break/**
    
  • lb的意思是load balance

Predicate

中文为谓语、断言,读音为ˈpredɪkeɪt

查看启动日志,可以发现:

2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [After]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Before]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Between]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Cookie]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Header]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Host]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Method]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Path]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Query]
2022-09-05 09:43:36.600  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [ReadBody]
2022-09-05 09:43:36.601  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [RemoteAddr]
2022-09-05 09:43:36.601  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [XForwardedRemoteAddr]
2022-09-05 09:43:36.601  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Weight]
2022-09-05 09:43:36.601  INFO 39728 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [CloudFoundryRouteService]

在之前的使用中,只用到了path

规则:

  • 在某个时间之后,也就是在这个时间之后这个规则才有效

    • 时间的格式为2022-09-05T09:52:24.300069800+08:00[Asia/Shanghai]

    • 可以通过System.out.println(ZonedDateTime.now());获取这种格式的时间

    • 以下代表在这个时间之后才能够正常的访问这个路径

    • routes:
        - id: normal
          uri: lb://CLOUD-PROVIDER-HYSTRIX8001
          predicates:
            - Path=/hystrix/normal
            - After=2022-09-05T09:55:24.300069800+08:00[Asia/Shanghai]
      
  • 在某个时间之前

    • - Before=2022-09-05T09:58:24.300069800+08:00[Asia/Shanghai]
  • 在某个时间之间

    • - Between=2022-09-05T15:13:24.300069800+08:00[Asia/Shanghai],2022-09-05T15:14:24.300069800+08:00[Asia/Shanghai]
  • - Cookie: cookie名称,正则表达式匹配规则

  • - Header=请求头,值正则表达式

  • - Method=POST

  • Query=参数名,值的正则表达式带有请求参数的规则

  • 其他规则可参阅:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

Filter

请求被路由前或者后进行路由

自定义全局过滤器:

  • 自定义类实现GlobalFilterOrdered接口

  • import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    @Component
    @Slf4j
    public class MyLogGlobalFilter implements GlobalFilter, Ordered {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("****************************自定义全局过滤器执行*******************************");
            // 放行
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            // 优先级,数字越小优先级越高
            return 0;
        }
    }
    

分布式配置

现在项目中存在的问题:

  • 每个项目都要有一个yml
  • 多个微服务都要访问数据库,都需要写同样的yml

服务配置中心也是一个微服务

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-server</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-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>
<dependencyManagement>
   <dependencies>
      <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>

<build>
   <plugins>
      <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
         <configuration>
            <excludes>
               <exclude>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
               </exclude>
            </excludes>
         </configuration>
      </plugin>
   </plugins>
</build>

配置:

spring:
  application:
    name: cloud-config3344
  cloud:
    config:
      server:
        git:
          uri: git仓库地址
          search-paths: 搜索路径

访问方式:

  • /{label}/{}-{}.yml
    • http://localhost:6677/master/application-dev.yml
  • /{}-{}.yml
    • http://localhost:6677/application-dev.yml,默认读取master分支的内容
  • /{}/{}/{label}
    • http://localhost:6677/application/dev/master,将会返回一个json,可以自行解析
  • {label}代表分支名称,例如master
│   application-dev.yml
│   
├───config
│       application-dev.yml
│       
└───my-config
        test.yml

创建文件时需要遵守{}-{}的格式

客户端的配置

bootstrap.yml的优先级大于application.yml,所以使用bootstrap.yml作为配置文件,但由于新版spring cloud的修改,所以无法直接使用,需要引入:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- 针对 bootstrap.yml的配置 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

配置服务器:

spring:
  application:
    name: cloud-config-client3355
  cloud:
    config:
      label: master
      name: application
      profile: dev
      uri: http://localhost:6677/

可以尝试着注入到文件:

@RestController
@RequestMapping("/config")
public class MyController {
    @Value("${name.message}")
    private String message;

    @RequestMapping("/message")
    public String getMessage() {
        return message;
    }
}

引入依赖:

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

在需要获取配置的类上添加@RefreshScope

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/config")
@RefreshScope
public class MyController {
    @Value("${name.message}")
    private String message;

    @RequestMapping("/message")
    public String getMessage() {
        return message;
    }
}

添加配置:

management:
  endpoints:
    web:
      exposure:
        include: "*"

更新配置后,手动对接受配置的客户端:http://ip:port/actuator/refresh进行POST请求,请求后即可进行更新

Bus 消息总线

是对Spring Cloud Config 的加强,主要完成配置的全自动刷新,支持RabbitMQKafka消息代理,从之前的手动更新配置变成了消息中间件自动更新

总线:使用消息代理(消息队列)构建的共同消息主题,是所有微服务都连接上来。

所有的配置客户端都连接一个Topic交换机,数据刷新时,同一个交换机中的服务都能收到通知,以用来更新配置

设计思想:

  • 消息总线触发一个客户端的刷新,从而导致所有的客户端都刷新
  • 消息总线触发服务端的刷新,从而刷新所有的客户端(更合适)

采用:消息总线触发一个客户端的刷新,从而导致所有的客户端都刷新的方式

配置服务端、客户端引入依赖:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

如果配置文件发生改变了,对配置中心(配置服务器)的:http://ip:port/actuator/busrefresh发送post请求,这时候客户端的配置就更新了

这个时候配置中心的配置文件内容大致如下:

server:
  port: 6677
spring:
  application:
    name: cloud-config3344
  cloud:
    config:
      server:
        git:
          uri: https://配置文件地址
          search-paths: my-config
  rabbitmq:
    addresses: localhost
    port: 5672
    username: guest
    password: guest
eureka:
  instance:
    hostname: localhost
    instance-id: cloud-config3344
    prefer-ip-address: true
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
management:
  endpoints:
    web:
      exposure:
        include: "bus-refresh"

两个配置客户端的配置文件内容大致如下:

server:
  port: 3355
spring:
  application:
    name: cloud-config-client3355
  cloud:
    config:
      label: master
      name: application
      profile: dev
      uri: http://localhost:6677/
  rabbitmq:
    username: guest
    password: guest
    port: 5672
eureka:
  instance:
    hostname: localhost
    instance-id: cloud-config-client3355
    prefer-ip-address: true
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
management:
  endpoints:
    web:
      exposure:
        include: "*"

配置中心的pom.xml内容大致如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.7.3</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.xiaoxu.config</groupId>
   <artifactId>cloud-config-6677</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>cloud-config-6677</name>
   <description>cloud-config-6677</description>
   <properties>
      <java.version>17</java.version>
      <spring-cloud.version>2021.0.3</spring-cloud.version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-bus-amqp</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-config-server</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-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>
   <dependencyManagement>
      <dependencies>
         <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>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
               <excludes>
                  <exclude>
                     <groupId>org.projectlombok</groupId>
                     <artifactId>lombok</artifactId>
                  </exclude>
               </excludes>
            </configuration>
         </plugin>
      </plugins>
   </build>

</project>

客户端的pom.xml大致如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.xiaoxu.config.client</groupId>
    <artifactId>cloud-config-client3355</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cloud-config-client3355</name>
    <description>cloud-config-client3355</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2021.0.3</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</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-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</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>
    <dependencyManagement>
        <dependencies>
            <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

打开RabbitMQ的管理页面,可以看到创建了一个名为springCloudBus的交换机,交换机的类型为topic

也可以定点通知,也就是只通知其中的一个,只需要请求时带上微服务的名称即可:POST请求配置中心的http://ip:port/actuator/busrefresh/微服务名称

Stream

消息驱动,不同的项目用到的消息队列可能是不同的,不同的消息队列是不可能直接进行通信的。Spring Cloud Stream就是解决的这个问题,屏蔽底层的细节,直接使用spring cloud stream就能无缝的操作多个消息队列,也就是统一消息的编程模型

官方文档给出的定义:spring cloud stream是构建消息驱动的微服务框架

只需要通过所提供的Binder对象与不同的消息中间件进行交互,目前支持整合RabbitMQKafkaRocketMQ

  • Binder连接中间件,屏蔽差异
  • Channel通道,对Queue的抽象
  • Source/Sink消息的生产者和消费者

此时:有8801生产者、8802接收者

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>

生产者配置:

server:
  port: 8801
spring:
  application:
    name: cloud-stream-producer8801
  rabbitmq:
    username: guest
    password: guest
    port: 5672

eureka:
  instance:
    hostname: localhost
    instance-id: cloud-stream-producer8801
    prefer-ip-address: true
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka

消费者和生产者配置:

server:
  port: 8802
spring:
  application:
    name: cloud-stream-consumer8802
  rabbitmq:
    username: guest
    password: guest
    port: 5672

eureka:
  instance:
    hostname: localhost
    instance-id: cloud-stream-consumer8802
    prefer-ip-address: true
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka

生产者发送消息:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

@Component
public class MessageProvider {
    @Autowired
    private StreamBridge streamBridge;

    public boolean sendMessage(String message) {
        return streamBridge.send("cloud-in-0", MessageBuilder.withPayload(message).build());
    }
}

消费者接收消息:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.function.Consumer;

@Component
@Slf4j
public class ReceiveProvider {

    @Bean
    public Consumer<String> cloud() {
        return message -> {
            log.info("接收消息为:{}", message);
        };
    }
}

交换机的类型默认为topic

分布式请求链路跟踪 sleuth

监控多个程序之间的服务调用

下载:zipkin

访问http://127.0.0.1:9411/可以进入web管理面板

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
    <version>2.2.8.RELEASE</version>
</dependency>

配置:

spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      probability: 1

可以对多个微服务引入,如果这几个微服务是相互联系的,可以在网页上看到访问时调用的中间服务和延迟

Spring Cloud Alibaba

Spring Cloud Netflix已全部都进入维护模式

https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/zh-cn/index.html

Nacos

Nacos全称为Naming Configuration Service ,就是配置中心+注册中心的组合,相当于Eureka + Config + Bus

官网:https://nacos.io/zh-cn/index.html

下载安装:https://github.com/alibaba/nacos/releases

bin目录下:.\startup.cmd -m standalone进行启动

运行后,直接进入localhost:8848/nacos,默认账号密码是nacos

支持CPAP切换

服务注册

以下的配置针对于服务端

<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>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

配置:

server:
  port: 9001
spring:
  application:
    name: cloud-alibaba-provider-payment
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'

Eureka一样,也可以多个服务实例使用一个名字,只需要保证spring.application.name的值相同即可

负载均衡

以下的依赖、配置仅针对客户端

nacos集成了ribbon

可以选择使用RestTemplate,在配置类上一定要加@LoadBalanced注解,否则将无法找到这个服务

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class MyConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
@RestController
@RequestMapping("/consumer")
public class MyController {
    @Autowired
    private RestTemplate restTemplate;

    public static final String URL = "http://cloud-alibaba-provider-payment";

    @GetMapping("/nacos")
    public String consumer() {
        return restTemplate.getForObject(URL + "/my/nacos", String.class);
    }
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class CloudConsumerPayment83Application {
    public static void main(String[] args) {
        SpringApplication.run(CloudConsumerPayment83Application.class, args);
    }
}

如果想要使用openFeign,需要引入spring-cloud-starter-loadbalancer并且将nacos-discovery集成的ribbon移除掉,否则会导致spring-cloud-starter-loadbalancer失效,移除ribbon不会影响RestTemplate的效果

<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>
    <!-- 移除ribbon -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-loadbalancer -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>

</dependencies>
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;

@Component
@FeignClient("cloud-alibaba-provider-payment")
public interface Api {
    @GetMapping("/my/nacos")
    String useApi();
}
@RestController
@RequestMapping("/consumer")
public class MyController {
    @Autowired
    private Api api;

    @GetMapping("/feign/nacos")
    public String feign() {
        return api.useApi() + "feign";
    }
}

配置中心

默认是支持自动刷新的

在 Nacos Spring Cloud 中,dataId 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。
  • spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 propertiesyaml 类型。

例如在以下配置文件中:

server:
  port: 9001
spring:
  profiles:
    active: dev
  application:
    name: cloud-alibaba-nacos-config
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml

按照以上的规则,需要在nacos创建的配置名为cloud-alibaba-nacos-config-dev.yml

引入依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/config")
@RefreshScope
public class MyController {
    @Value("${config.info}")
    private String config;

    @GetMapping("/info")
    public String getConfigInfo() {
        return config;
    }
}

需要添加@RefreshScope注解实现配置文件更新时自动刷新配置

命名分组

分为namespace + group + data Id,类似于包名和类名:

  • namespace用于区分部署环境,实现隔离
    • 不同的namespace属于不同的环境
  • Group用于在逻辑上区分两个目标对象
    • 将不同的微服务划分到同一个分组中
  • data id用于在逻辑上区分两个目标对象
    • 就是配置文件的名称

默认情况:

  • namespacepublic
  • groupDEFAULT_GOUP
  • clusterdefault

image-20220909204444502

spring:
  profiles:
    active: test
  application:
    name: cloud-alibaba-nacos-config
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
        group: FIRST_GROUP
        namespace: BEIJING_SERVER

namespace为名称空间的id

Nacos持久化

默认nacos使用的嵌入式的数据库,如果要搭建集群,那么需要使用MySQL来保证各个集群的数据统一

创建一个数据库,执行nacos/conf/nacos-mysql.sql,将会自动创建用到的表

修改nacos/conf/application.properties,根据注释信息,修改如下内容:

#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
# spring.datasource.platform=mysql
spring.datasource.platform=mysql

### Count of DB:
# db.num=1
db.num=1

### Connect URL of DB:
# db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
# db.user.0=nacos
# db.password.0=nacos
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=用户名
db.password.0=密码

重新运行nacos即可

集群

nacos/conf/cluster.conf.example文件复制修改问cluster.conf,在里边填入IP:端口号即可,一行一个,IP不能为127.0.0.1或者localhost,可以填入局域网IP

这里以搭建3个集群为例:

例如:

10.54.70.146:8850
10.54.70.146:8860
10.54.70.146:8870

注意:Nacos2.0版本相比1.X新增了gRPC的通信方式,因此需要增加2个端口。新增端口是在配置的主端口(server.port)基础上,进行一定偏移量自动生成。

所以三个端口的距离尽量增大。

集群 Nacos端口 gRPC端口1 gRPC端口2
1 x.x.x.x:8850 x.x.x.x:9850 x.x.x.x:9851
2 x.x.x.x:8860 x.x.x.x:9860 x.x.x.x6:9852
3 x.x.x.x:8870 x.x.x.x:9870 x.x.x.x:9853

nacos/conf/applicaton.properties文件中修改每个集群的端口,因为这个时候创建了cluster.conf,所以在启动nacos时只需要使用以下命令,无需添加其他的参数:

.\startup.cmd

使用Nginx进行反向代理时,除了对Nacos端口进行负载均衡的配置之外,还需要对gRPC进行单独配置

stream {
    # tcp负载均衡
    upstream nacos-tcp-2111 {
        server 127.0.0.1:9850 weight=1;
        server 127.0.0.1:9860 weight=1;
        server 127.0.0.1:9870 weight=1;
    }
    # Nacos客户端gRPC请求服务端端口
    server {
        listen 2111;
        proxy_pass nacos-tcp-2111;
    }
    # tcp负载均衡
    upstream nacos-tcp-2112 {
        server 127.0.0.1:9851 weight=1;
        server 127.0.0.1:9861 weight=1;
        server 127.0.0.1:9871 weight=1;
    }
    # Nacos服务端gRPC请求服务端端口
    server {
        listen 2112;
        proxy_pass nacos-tcp-2112;
    }
}

http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;

    #gzip  on;
    upstream nacos {
        server localhost:8850;
        server localhost:8860;
        server localhost:8870;
    }

    server {
        listen 1111;
        server_name localhost;

        location / {
            proxy_pass http://nacos/;
            # root html;
            # index index.html index.htm;
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}

官方文档上有一句:使用VIP/nginx请求时,需要配置成TCP转发,不能配置http2转发,否则连接会被nginx断开。

配置的客户端的地址尽量不要填localhost之类的本地地址,端口号填写反向代理的nacos的端口:

spring:
  profiles:
    active: test
  application:
    name: cloud-alibaba-nacos-config
  cloud:
    nacos:
      discovery:
        server-addr: x.x.x.x:1111
      config:
        server-addr: x.x.x.x:1111
        file-extension: yml
        group: FIRST_GROUP
        namespace: BEIJING_SERVER

Sentinel

中文为哨兵,读音为ˈsent(ə)nəl,用于服务熔断、服务降级,可以在web页面进行管理

下载:https://github.com/alibaba/Sentinel/releases

选择带有deshboard字样的版本即可,运行后打开localhost:8080,账号密码都是sentinel,可以直接进入web管理页面

引入依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-datasource-nacos -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

也是需要nacos运行并引入

配置:

server:
  port: 8401
spring:
  application:
    name: cloud-alibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719

management:
  endpoints:
    web:
      exposure:
        include: '*'

8719端口是指定与dashboard进行交互的端口,如果这个端口被占用了,默认将会不断的+1,直到找到一个可用的端口

此时打开8080的监控页,可能无法看到任何的监控信息,这是因为采用的懒加载的机制,等到第一次访问后,将会在监控页面看到详情信息

流量监控规则

image-20220911161402934

在簇点链路可以对访问过的路径进行管理,也可以在流控规则中手动添加相关路径进行设置,其中:

  • 资源名为路径
  • 针对来源默认为default,也就是不区分来源,也可以填写微服务名,达到对某个微服务进行限制
  • 阈值类型
    • QPS:每秒的请求量
  • 流控模式
    • 直接:api达到限流条件时直接进行限流
    • 关联:当关联的资源达到阈值时,限流自己
    • 链路:只记录链路上的流量,也就是指定资源从入口资源进来的流量,如果达到阈值就进行限流,在api级别上针对来源
  • 流控效果
    • 快速失败:直接失败,直接抛异常
    • warm up:针对codeFactor(冷加载因子)的值进行
    • 排队等待:匀速排队,使请求匀速通过,必须设置QPS,否则没有效果

流控模式-直接:当超过设置的每秒请求的次数之后,请求的响应会显示为:Blocked by Sentinel (flow limiting)

  • image-20220911163923469

流控模式-关联:当A关联的资源B达到阈值后,限制A自己,比如支付服务接口达到阈值了,对订单服务进行限制

流控效果-warm up(预热):

  • image-20220911173423524
  • 服务有可能出现长时间没有访问,然后一下子来了许多个访问,如果突然访问量增加,服务器可能顶不住
  • 例如单机阈值为10,时长为5,使用预热模式,将会在5秒内慢慢的提高单机阈值到10
  • 将会从阈值/3开始,例如阈值为10,将会从3作为第一次的阈值

流控效果-排队等待:只能在阈值类型是QPS的情况下使用

  • 适用于处理间隔性的突发流量

熔断降级

策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

降级后,在时间窗口内对降级的资源所有的调用都自动熔断

热点key

热点是经常访问的数据,可以对访问频率前k的数据进行限制

@GetMapping("/hotkey")
@SentinelResource(value = "名称", blockHandler = "兜底方法名称")
public String hotKey(@RequestParam(value = "content", required = false) String content, 
  @RequestParam(value = "param", required = false) String param) {
    return "hotkey - content = " + content + ",  param = " + param;
}

public String 兜底方法(String content, String param
                        , BlockException exception) {
    return "hotkey - content = " + content + ",  param = " + param + ":(";

}

在热点规则中,资源名为@SentinelResource中自定义的名称,参数索引从0开始,针对以上的代码:

索引 参数
0 content
1 param

如果不在@SentinelResource中指定处理异常的兜底方法,那么将会直接把异常抛到前端页面

也可以设置例外情况,也就是某个参数达到特殊值后的限制和平常不一样,例如当param参数的值为"300"时,限制QPS100,在其他情况限制1

  • image-20220915115351380
  • 参数的类型要和实际处理这个请求的方法一致,设置后需要点添加按钮
  • 当代码中有异常抛出时,将不会经过兜底方法

系统规则

从应用入口进行流量限制,经合多方面的配置等因素进行限流,在系统规则页进行新增

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

Sentinel Resouce

可以按照资源名限制

使用方法类似于:

@GetMapping("/resource")
@SentinelResource(value = "resource", blockHandler = "resourceHandler")
public Map<String, String> resource() {
    HashMap<String, String> map = new HashMap<>();
    map.put("code", "200");
    map.put("status", "访问成功");
    return map;
}

public Map<String, String> resourceHandler(BlockException exception) {
    HashMap<String, String> map = new HashMap<>();
    map.put("code", "400");
    map.put("status", "访问失败");
    map.put("cause", exception.getMessage());
    return map;
}

单独写一个兜底的方法,并在注解中指定兜底的方法,请求的方法中如果含有参数(例如路径变量、参数对应的变量),可以在兜底方法的参数列表中填写这些参数,还可以添加BlockException类型的参数,用来表示默认抛出的一个异常

@SentinelResource注解不支持private方法

以上方案也存在着一些问题:

  • 对于每一个方法,都需要提供一个兜底的方法,否则发生限流/熔断降级时将会使用默认的兜底方法,也就是屏幕上输出Blocked by Sentinel (flow limiting)
  • 兜底的方法和处理请求的方法耦合在一块了
  • 没有体现出全局统一的处理方法

自定义限流处理类

自定义一个类,类中所有的方法都是静态的方法,静态方法上必须要添加BlockException类型的参数,否则将不会执行兜底方法,并且前端页面将会报500错误,在需要有兜底方法的请求方法上:

@SentinelResource(value = "名称", blockHandlerClass = 自定义兜底类.class, blockHandler = "自定义的兜底方法")
package com.xiaoxu.cloud.handler;

import com.alibaba.csp.sentinel.slots.block.BlockException;

import java.util.HashMap;
import java.util.Map;

public class CustomerBlockHandler {
    public static Map<String, Object> firstHandler(BlockException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "400");
        map.put("cause", e);
        map.put("message", "失败---first");
        return map;
    }

    public static Map<String, Object> secondHandler(BlockException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "400");
        map.put("cause", e);
        map.put("message", "失败---second");
        return map;
    }
}
@GetMapping("/first")
@SentinelResource(value = "first", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "firstHandler")
public Map<String, Object> first() {
    HashMap<String, Object> map = new HashMap<>();
    map.put("code", "200");
    map.put("status", "访问成功---first");
    return map;
}

@GetMapping("/second")
@SentinelResource(value = "second", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "secondHandler")
public Map<String, Object> second() {
    HashMap<String, Object> map = new HashMap<>();
    map.put("code", "200");
    map.put("status", "访问成功---second");
    return map;
}

服务熔断

服务熔断就是在出现异常情况时进行熔断

fallback中文为倒退、退却、让出

可以设置异常的兜底方法:

@SentinelResource(value = "index", blockHandlerClass = MyBlockHandler.class, blockHandler = "itemHandler",
        fallback = "itemExceptionFallback")
public String getItem(@PathVariable Integer index) {
    return list.get(index) + " - 服务器2";
}

public String itemExceptionFallback(Integer index, Throwable e) {
    return "抛异常了,服务器2,index = " + index + ", 异常为:" + e;
}

兜底方法中可以有请求方法中的参数,可以根据需要添加@PathVariable等注解,也可以不添加,但兜底方法中的异常接受参数不能使用Exception接受,需要使用Throwable进行接收

也可以将异常处理方法单独放到一个类中,使用@SentinelResource(fallbackClass = 类名.class)指定异常处理类:

public class MyExceptionFallback {
    public static String itemExceptionFallback(Integer index, Throwable e) {
        return "抛异常了,服务器1,index = " + index + ", 异常信息为:" + e;
    }
}
@GetMapping("/{index}")
@SentinelResource(value = "index", blockHandlerClass = MyBlockHandler.class, blockHandler = "itemHandler",
        fallbackClass = MyExceptionFallback.class, fallback = "itemExceptionFallback")
public String getItem(@PathVariable Integer index) {
    return list.get(index) + " - 服务器1";
}

兜底方法也可以忽略异常:

@SentinelResource(exceptionsToIgnore = {java.lang.IndexOutOfBoundsException.class})

如果忽略后发生了这个异常,将不会执行兜底方法

持久化

每次重启服务,sentinel的规则都会丢失,所以需要持久化,可以将规则直接保存到Nacos

需要:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

引入配置:

spring:
  application:
    name: cloud-alibaba-payment
  profiles:
    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloud-alibaba-sentinel-payment
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

nacos创建一个名为指定的dataId格式为json的配置文件,文件内容格式如下:

[
    {
        "resource": "index",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]
  • resource:资源名称
  • limitApp:来源应用
  • grade:阈值类型,0表示线程数,1表示QPS
  • count:单机阈值
  • strategy:流控模式,0表示直接,1表示关联,2表示链路
  • controlBehavior:流控效果,0表示快速失败,1表示Warm Up2表示排队等待
  • clusterMode:是否集群

Seata 分布式事务

在微服务的架构中,可能存在着多库多表,需要保证多个服务对数据库访问时的一致性

Transcation ID XID全局唯一的事务ID

  • Transaction Coordinator 事务协调器,维护全局的事务的运行状态,负责协调、驱动全局事务的提交或者回滚
  • Transaction Manager 控制事务的边界,负责开启全局事务,发起全局提交或者全局回滚
  • Resource Manager 控制分支事务,负责分支注册、状态的汇报,接收事务协调器的指令
image
  • TMTC申请开启一个全局事务,全局事务创建成功后生成一个全局唯一的XID
  • XID在微服务调用链的上下文传播
  • RMTC注册分支事务,将其踏入XID全局事务的管辖
  • TMTC发起针对XID全局提交或者回滚的决议
  • TC调度XID下管辖的所有分支事务完成提交或者回滚

安装

下载:https://github.com/seata/seata/releases

修改配置文件可参阅:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html

服务注册中心、配置中心使用nacos,数据库使用MySQL

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: public
      group: SEATA_GROUP
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: localhost:8848
      group: SEATA_GROUP
      namespace: public
      cluster: default
  store:
    # support: file 、 db 、 redis
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true
      user: 账号
      password: 密码
      min-conn: 5
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 100
      max-wait: 5000
    
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

需要创建一个数据库,并执行seata\script\server\db下相应数据库.sql

根据文档可知,对jdk17的支持不是特别好,直接运行将会报错,需要修改启动命令:

%JAVACMD% %JAVA_OPTS% %SKYWALKING_OPTS% --add-opens java.base/java.lang=ALL-UNNAMED -server -Dloader.path=../lib -Xmx2048m -Xms2048m -Xmn1024m -Xss512k -XX:SurvivorRatio=10 -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1024m -XX:-OmitStackTraceInFastThrow -XX:-UseAdaptiveSizePolicy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="%BASEDIR%"/logs/java_heapdump.hprof -XX:+DisableExplicitGC -Xlog:gc:"%BASEDIR%"/logs/seata_gc.log -verbose:gc -Dio.netty.leakDetectionLevel=advanced -classpath %CLASSPATH% -Dapp.name="seata-server" -Dapp.repo="%REPO%" -Dapp.home="%BASEDIR%" -Dbasedir="%BASEDIR%" -Dspring.config.location="%BASEDIR%"/conf/application.yml -Dlogging.config="%BASEDIR%"/conf/logback-spring.xml -jar "%BASEDIR%"/target/seata-server.jar %CMD_LINE_ARGS%

注意:Nacos要以单个的形式启动,即.\startup.cmd -m standalone

订单服务、库存服务、账户服务,用户下单在微服务创建订单,调用远程库存服务扣减库存,通过账户服务扣余额,最后在订单服务标记 订单状态

下订单->减库存->扣余额->改变订单状态

以下依赖中,第二个依赖不兼容jdk17,添加此依赖无法启动项目,如果不添加此依赖能够正常启动项目,但会造成使用的openfeign全局事务标识符xid无法传递,可以尝试手动传递xid,例如在请求头中放入xid,使用RootContext.bind(xid)进行手动绑定

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.2.0.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>

配置:

seata:
  tx-service-group: default-tx-group # 需要保证与第19行内容一致
  enabled: true
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      namespace: aa34dcea-fa3e-43a0-a0a9-5567ddd5234b # 需要手动创建一个命名空间,不能使用public
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: public
  service:
    vgroupMapping:
      default-tx-group: default # 键需要保证与第2的值一致

nacos中创建一个名为service.vgroupMapping.20行内容的配置文件,文件类型为txt,内容为20行的值

完整配置文件类似于:

server:
  port: 2001
spring:
  application:
    name: cloud-alibaba-seata-order-service
  datasource:
    url: jdbc:mysql://localhost:3306/xxxx
    username: xxxx
    password: xxxx
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  mvc:
    hiddenmethod:
      filter:
        enabled: true
management:
  endpoints:
    web:
      exposure:
        include: '*'
seata:
  tx-service-group: default-tx-group
  enabled: true
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      namespace: aa34dcea-fa3e-43a0-a0a9-5567ddd5234b
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: public
  service:
    vgroupMapping:
      default-tx-group: default

在需要全局事务的地方使用@GlobalTransactional注解,这个注解可以指定忽略的异常等信息

Q.E.D.


念念不忘,必有回响。