Spring Cloud Alibaba
发表于:2024-03-03 | 分类: 框架
字数统计: 12.8k | 阅读时长: 59分钟 | 阅读量:

1. 背景

Netflix版本由于停更,引发了Cloud各种组件的升级和替换:

c1d3a2461c3ab6176f1a6c5ce6a16511.png

创建父工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<packaging>pom</packaging>

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
<cloud-Alibaba.version>2021.0.4.0</cloud-Alibaba.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-Alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

2. 注册中心

Eureka

Eureka在2.x版本后有些许变化,这里再简单回顾一下Eureka的用法。

服务端

创建子工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

注意依赖发生了变换

1
2
3
4
5
6
7
@EnableEurekaServer
@SpringBootApplication
public class Eureka7001App {
public static void main(String[] args) {
SpringApplication.run(Eureka7001App.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 7001

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

集群版的配置参照1.x版本即可

客户端

创建订单工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@EnableEurekaClient
@SpringBootApplication
public class Order8001App {
public static void main(String[] args) {
SpringApplication.run(Order8001App.class, args);
}
}

@Slf4j
@RestController
public class OrderController {
@Value("${server.port}")
private String port;
@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/order")
public String oder() {
return "下订单:" + port;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8001

spring:
application:
name: order-service

eureka:
client:
#false表示不向注册中心注册自己。
register-with-eureka: true
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: true
service-url:
#设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://localhost:7001/eureka/
instance:
instance-id: order-service:8001
prefer-ip-address: true #访问路径可以显示IP地址

服务发现

客户端添加注解:**@EnableDiscoveryClient**;那么就可以获取到注册到Eureka中的服务信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private DiscoveryClient discoveryClient;

@GetMapping(value = "/payment/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String element : services) {
System.out.println(element);
}

List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance element : instances) {
System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t"+ element.getUri());
}
return this.discoveryClient;
}

注意使用的类是:import org.springframework.cloud.client.discovery.DiscoveryClient;

Zookeeper

修改Eureka7001App模块:

1
2
3
4
5
6
7
8
9
<!--        <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>-->
<!-- </dependency>-->
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
// @EnableEurekaServer
// 以下注解用于向使用consul或者zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
@SpringBootApplication
public class Eureka7001App {
public static void main(String[] args) {
SpringApplication.run(Eureka7001App.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 7001

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

#服务别名----注册zookeeper到注册中心名称
spring:
application:
name: zookeeper-cloud
cloud:
zookeeper:
connect-string: 192.168.124.105:2181

如果项目启动报错,可能是因为自带的zookeeper版本依赖和服务版本有冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--排除自带的zookeeper-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加指定的zookeeper版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.5</version>
</dependency>

默认的版本为3.6.0,这里启动的服务版本为3.5.5;可以兼容

从下图可以看到,我们服务注册使用的是临时节点

c6c13a2b964e1cb15a9359ad3bc66095.png

使用Eureka做注册中心时,需要启动一个专门的SpringBoot服务作为注册中心,而使用consul或者zookeeper时,只需要将需要管理的服务直接注册到对应的服务中即可,即SpringBoot服务都是客户端。

Consul

官网:Consul | HashiCorp Developer

consul的安装启动在这里就不进行介绍了,以下是相关的依赖和配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: consul-cloud
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}

还是仅需要@EnableDiscoveryClient一个注解即可

三种注册中心的比较:

  • AP:Eureka
  • CP:Zookeeper/Consul

3. 服务调用

Ribbon

Ribbon的使用没有大的改动,这里简单介绍一下。

如果只是使用简单的轮询,我们在注册中心部分的依赖就可以实现,创建一个消费者:

1
2
3
4
5
6
7
8
@EnableEurekaClient
@EnableDiscoveryClient
@SpringBootApplication
public class Consumer80App {
public static void main(String[] args) {
SpringApplication.run(Consumer80App.class, args);
}
}
1
2
3
4
5
6
7
8
@Configuration
public class BeanConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
private String order = "http://ORDER-SERVICE";

@GetMapping("/consumer/order")
private String pay() {
log.info("=======进行远程调用=======");
return restTemplate.getForObject(order + "/order", String.class);
}
}

注意这里的服务名称必须大写,和Eureka界面展示的名称保持一致。

再复制一份oder子工程,我们就能发现已经在以轮询的形式进行负载均衡。

如果我们需要其它或自定义负载均衡算法,就需要引入Ribbon的依赖了:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

自定义负载均衡算法以及**@RibbonClient**注解的使用,在此就不再赘述

OpenFeign

Feign OpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端
Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

修改消费者代码:

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

主启动类添加注解:@EnableFeignClients;创建service:

1
2
3
4
5
6
@Component
@FeignClient(value = "PAY-SERVICE")
public interface PayService {
@GetMapping("pay")
String pay();
}
1
2
3
4
5
6
7
@Autowired
private PayService payService;
@GetMapping("/consumer/pay")
private String pay() {
log.info("=======Feign进行远程调用=======");
return payService.pay();
}

超时控制

1
2
3
4
5
6
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000

日志打印

Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,也就是对Feign接口的调用情况进行监控和输出。

日志级别:

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
1
2
3
4
5
6
7
8
9
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
1
2
3
4
logging:
level:
# feign日志以什么级别监控哪个接口
com.test.service.PayService: debug

效果:

c0c3d57ceadae2e94658b8ef23074e8b.png

4. 服务降级

Hystrix

Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

触发降级的情况:

  1. 程序运行异常
  2. 响应超时
  3. 服务熔断触发服务降级
  4. 线程池/信号量打满也会导致服务降级
1
2
3
4
5
6
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>

在消费者服务中使用@HystrixCommand@EnableHystrix两个注解:

@EnableCircuitBreaker已经过时了

Hystrix的使用也没有太大的变化,我们可以使用@DefaultProperties注解来指定当前控制器默认的降级方法:

1
2
3
4
5
6
7
8
9
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class ConsumerController {

//...

public String paymentGlobalFallbackMethod() {
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}

可以使用fallback来制定service的降级方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@FeignClient(value = "PAY-SERVICE",fallback = PayServiceImpl.class)
public interface PayService {
@GetMapping("pay")
String pay();

@GetMapping("pay/timeout")
String payTimeOut();
}

@Component
public class PayServiceImpl implements PayService {
@Override
public String pay() {
return "FallbackFactory.pay()";
}

@Override
public String payTimeOut() {
return "FallbackFactory.payTimeOut()";
}
}

和 fallbackFactory 属性的使用方法差不多

配置文件:

1
2
3
feign:
circuitbreaker:
enabled: true #在Feign中开启Hystrix

注意像现在调用pay/timeout接口会因为超时触发降级

设置全局超时时间:

1
2
3
4
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 5000

服务监控

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}

被监控的服务需要添加依赖:

1
2
3
4
5
<!-- actuator监控信息完善 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

监控地址:http://localhost:80/hystrix.stream

5. 网关

Zuul

作用:

  1. 路由
  2. 过滤
  3. 负载均衡
  4. 灰度发布

路由功能是负责将外部请求转发到具体的服务实例上去,是实现统一访问入口的基础。

创建新的工程:router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class Router9005App {
public static void main(String[] args) {
SpringApplication.run(Router9005App.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9005

spring:
application:
name: zuul

eureka:
client:
service-url:
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
defaultZone: http://localhost:7001/eureka
instance:
instance-id: zuul:9005
prefer-ip-address: true

按照上面的配置运行,最终会出现:

1
2
java.lang.NoSuchMethodError: org.springframework.boot.web.servlet.error.ErrorController.getErrorPath()Ljava/lang/String;
at ...

原因:

2.2.10.RELEASE版本的zuul需要配置2.4.x的springboot版本;但是当前使用的SpringCloud版本需要2.6.x和2.7.x版本的SpringBoot,所以需要整体下调版本,由于用法没有太多变化(并且已经被淘汰了),这里就不再进行演示。

Gateway

官网介绍:Spring Cloud Gateway

Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和 Project Reactor等技术,,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway的目标是提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。

核心概念:

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
  • Predicate(断言):参考的是Java8的java.util.function.Predicate;开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  • Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

工作流程的核心逻辑:路由转发+执行过滤器链

我们在router模块的基础上进行修改:

1
2
3
4
5
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 9005

spring:
application:
name: gateway-service

eureka:
instance:
hostname: gateway-service
instance-id: gateway-service:9005
prefer-ip-address: true #访问路径可以显示IP地址
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://localhost:7001/eureka
1
2
3
4
5
6
7
@EnableEurekaClient
@SpringBootApplication
public class Router9005App {
public static void main(String[] args) {
SpringApplication.run(Router9005App.class, args);
}
}

配置网关路由的两种方式:

  1. 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: pay_service #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:9001 #匹配后提供服务的路由地址
uri: lb://pay-service #根据服务名称进行负载均衡的路由地址
predicates:
- Path=/payService/** # 断言,路径相匹配的进行路由
filters:
- StripPrefix=1 #表示去掉一层前缀;即转发的真实前缀为 /**
  1. 代码中注入RouteLocator的Bean
1
2
3
4
5
6
7
8
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("pay_service", r -> r.path("/payService/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://pay-service")
).build();
}

两种配置二选一即可,当前的请求路径为:http://localhost:9005/payService/pay

Route Predicate Factories

Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。

Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: pay_service #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:9001 #匹配后提供服务的路由地址
uri: lb://pay-service #根据服务名称进行负载均衡的路由地址
predicates:
- Path=/payService/** # 断言,路径相匹配的进行路由
- After=2024-01-05T15:10:03.685+08:00[Asia/Shanghai] # 表示在设置时间点之后可以访问
- Before=2024-02-05T15:10:03.685+08:00[Asia/Shanghai] # 表示在设置时间点之前可以访问
- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai] # 表示在两个时间点之间可以访问
- Cookie=username,asdf # 表示必须带上指定cookie: username=asdf 才能访问通过该路由地址转发的服务
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- Host=**.test.com # 需要指定请求的目标主机的域名或IP地址
- Method=GET # 指定请求方式
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
filters:
- StripPrefix=1 #表示去掉一层前缀;即转发的真实前缀为 /**

其中predicates配置项就指定了九种不同的Route Predicate Factories

  1. Path Route Predicate
  2. After Route Predicate
  3. Before Route Predicate
  4. Between Route Predicate
  5. Cookie Route Predicate
  6. Header Route Predicate
  7. Host Route Predicate
  8. Method Route Predicate
  9. Query Route Predicate

说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。

6. 配置中心

SpringCloud Config

在GitHub中创建仓库并,上传文件(我这里是直接在仓库中新建文件,使用中文会导致读取显示乱码)

远程文件config-dev.yml

1
2
3
today:
weather: 大风阴天
mood: nice

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server:
port: 6001

spring:
application:
name: cloud-config #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: git@github.com:LemonPuer/cloud_config.git #GitHub上面的git仓库名字
#搜索目录
# search-paths:
# - cloud_config
#读取分支
label: main

eureka:
client:
#false表示不向注册中心注册自己。
register-with-eureka: true
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: true
service-url:
#设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://localhost:7001/eureka/
instance:
instance-id: cloud-config:6001
prefer-ip-address: true #访问路径可以显示IP地址

主启动类:

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

读取配置文件:http://localhost:6001/main/config-dev.yml

配置读取规则:

1
2
3
4
#推荐
/{label}/{application}-{profile}.yml
/{application}-{profile}.yml
/{application}/{profile}[/{label}]

客户端

修改消费者consumer80。

依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--需要导入下面这个依赖项目才能支持读取 bootstrap.yaml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

添加配置文件bootstrap.yml

1
2
3
4
5
6
7
spring:
cloud:
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称
uri: http://127.0.0.1:6001 #配置中心地址

加载顺序(优先级): bootstrap > application > 配置中心

即:bootstrap.yml > bootstrap.properties > application.yml > application.properties

后者的设置会覆盖前者

1
2
3
4
5
6
7
@Value("${today.mood}")
private String mood;

@GetMapping("mood")
public String mood() {
return "今天的心情是:" + mood;
}

此时如果配置文件发生改变,那么需要将客户端重新启动才能读取到最新的配置

配置动态刷新

需要引入actuator监控:

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

修改配置文件:

1
2
3
4
5
6
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"

控制器添加注解@RefreshScope

1
2
3
4
5
6
7
8
9
10
11
12
13
@RefreshScope
@RestController
public class configController {

@Value("${today.mood}")
private String mood;

@GetMapping("mood")
public String mood() {
return "今天的心情是:" + mood;
}

}

我们修改配置文件后发现客户端读取的还是旧文件,我们需要手动发送重置请求:

1
curl -X POST "http://localhost:80/actuator/refresh"

c2be53f08ded4d3ae76f4c35950f17f2.png

成功后可以读取到最新的数据,避免了服务重启。

但此时还存在一个问题,当同一个服务的机器过多,对每一台机器进行请求是一件非常繁琐的事情。

7. 消息总线

SpringCloud Bus

分布式自动刷新配置功能;Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。

注意:Bus支持两种消息代理:RabbitMQ 和 Kafka

原理:ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。

两种设计思路:

  1. 利用消息总线触发一个客户端/bus/refresh,从而刷新所有客户端的配置
  2. 利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置

60b290da7ba504a71e9ca082708922dd.png

我们使用第二种,第一种不合适的理由如下:

  1. 打破了微服务的职责单一性,对服务本身进行了侵入
  2. 破坏了微服务各节点的对等性
  3. 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化

需要准备好RabbitMQ的环境,然后调整工程Order8001,使其加入配置中心。

给服务端config6001添加消息总线支持:

1
2
3
4
5
6
7
8
9
<!--添加消息总线RabbitMQ支持-->
<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>

修改配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
#rabbitmq相关配置
rabbitmq:
host: 192.168.124.105
port: 5672
username: john
password: 123456

#rabbitmq相关配置,暴露bus刷新配置的端点
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'

页面查看地址:http://192.168.124.105:15672/

修改客户端(80,8001):

1
2
3
4
5
6
7
8
9
<!--添加消息总线RabbitMQ支持-->
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
#rabbitmq相关配置
rabbitmq:
host: 192.168.124.105
port: 5672
username: john
password: 123456

# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*" # 'refresh'
1
curl -X POST "http://localhost:6001/actuator/busrefresh"

旧版本的刷新地址:curl -X POST "http://localhost:6001/actuator/bus-refresh"

也可以通过请求参数指定集群进行配置更新:

1
curl -X POST "http://localhost:6001/actuator/busrefresh/order-service"

http://配置中心ip:配置中心的端口号/actuator/bus-refresh/{destination}

8. 消息驱动

SpringCloud Stream

作用:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

设计理念:通过向应用程序暴露统一的Channel通道,将应用程序与消息中间件隔离。

其中INPUT对应消费者,OUTPUT对应生产者。

stream中的消息通信方式遵循了发布-订阅模式,使用Topic主题进行广播,在RabbitMQ就是Exchange,在Kafka中就是Topic。

消息发送/消费标准流程:

adbec955683ad0f70d246de123523a20.png

channel:队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置

Source和Sink:可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。

生产者

修改consumer80工程,添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

修改配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
server:
port: 80

spring:
application:
name: consumer-service
cloud:
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称
uri: http://127.0.0.1:6001 #配置中心地址
stream:
binders:
myRabbit: #自定义名称,用于和binding整合
type: rabbit
environment:
spring:
rabbitmq:
host: 192.168.124.105
port: 5672
username: john
password: 123456
bindings:
test-output: #定义的消息通道名称
content-type: application/json #设置消息类型,本次为json,文本则设置“text/plain”
destination: myExchange2 #表示要使用的Exchange名称定义
binder: myRabbit #设置要绑定的消息服务的具体设置


#rabbitmq相关配置
rabbitmq:
host: 192.168.124.105
port: 5672
username: john
password: 123456


eureka:
client:
#false表示不向注册中心注册自己。
register-with-eureka: true
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: true
service-url:
#设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://localhost:7001/eureka/
instance:
instance-id: consumer-service:80
prefer-ip-address: true #访问路径可以显示IP地址

logging:
level:
# feign日志以什么级别监控哪个接口
com.test.service.PayService: debug

feign:
circuitbreaker:
enabled: true #在Feign中开启Hystrix
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000

hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 5000

# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class MqController {
@Autowired
private MqService mqService;

@GetMapping("/send/{message}")
public String sendMessage(@PathVariable String message) {
return mqService.sendMessage(message);
}
}

@Service
public class MqServiceImpl implements MqService {

@Autowired
private StreamBridge streamBridge;

@Override
public String sendMessage(String message) {
boolean send = streamBridge.send("test-output", message);
return send ? "发送成功" : "发送失败";
}
}

消费者

修改order8001,引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

修改配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
server:
port: 8001

spring:
application:
name: order-service
cloud:
stream:
function:
definition: getMessage
binders:
myRabbit: #自定义名称,用于和binding整合
type: rabbit
environment:
spring:
rabbitmq:
host: 192.168.124.105
port: 5672
username: john
password: 123456
bindings:
test-output: #定义的消息通道名称
content-type: application/json #设置消息类型,本次为json,文本则设置“text/plain”
destination: myExchange1 #表示要使用的Exchange名称定义
binder: myRabbit #设置要绑定的消息服务的具体设置
getMessage-in-0: #默认队列的输入通道名称
content-type: application/json
destination: myExchange2
binder: myRabbit
group: order8001 # rocketmq一定要设置group


#rabbitmq相关配置
rabbitmq:
host: 192.168.124.105
port: 5672
username: john
password: 123456

eureka:
client:
#false表示不向注册中心注册自己。
register-with-eureka: true
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: true
service-url:
#设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://localhost:7001/eureka/
instance:
instance-id: order-service:8001
prefer-ip-address: true #访问路径可以显示IP地址

# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
1
2
3
4
5
6
7
@Bean
public Consumer<String> getMessage() {
return message -> {
System.out.println("Received message: " + message);
// 处理消息的逻辑
};
}

注意bean的名称要与配置文件对应,例如getMessage

9. 请求链路跟踪

简称APM(Application Performance Managment)。

SpringCloud Sleuth

Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案,用来监控服务节点之间的调用链路。

兼容支持zipkin(开源的分布式跟踪系统),官网:OpenZipkin · A distributed tracing system

下载链接:https://repo1.maven.org/maven2/io/zipkin/zipkin-server/2.25.2/zipkin-server-2.25.2-exec.jar

这已经是支持java8的最后一个版本

运行:java -jar zipkin-server-2.25.2-exec.jar

上/下游服务(consumer80 / order8001)添加依赖:

1
2
3
4
5
6
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>

修改配置文件:

1
2
3
4
5
6
7
spring:
zipkin:
base-url: http://192.168.124.105:9411
sleuth:
sampler:
#采样率值介于 0 到 1 之间,1 则表示全部采集
probability: 1

界面访问:http://192.168.124.105:9411/

cfaf77354f2bc7a36d588859da2cbbb5.png

10. Alibaba

Nacos

简介:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

具有注册中心+配置中心的功能,相当于:Eureka+Config +Bus。

官网:Nacos

注册中心比较:

服务注册与发现框架 CAP模型 控制台管理 社区活跃度
Eureka AP 支持
Zookeeper CP 不支持
Consul CP 支持
Nacos AP 支持

安装启动

需要Java8环境。

1
2
3
4
tar -zxvf nacos-server-2.3.0.tar.gz -C app/
cd /opt/app/nacos/bin
#单机模式启动
./startup.sh -m standalone

访问地址:http://192.168.124.105:8848/nacos

注册中心

创建工程NacosConsumer80,添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<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>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConsumer80 {
public static void main(String[] args) {
SpringApplication.run(NacosConsumer80.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server:
port: 80

spring:
application:
name: consumer-service
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848

logging:
level:
# feign日志以什么级别监控哪个接口
com.test.service.PayService: debug

feign:
circuitbreaker:
enabled: true #在Feign中开启Hystrix
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000

hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 5000

远程调用业务方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@RestController
public class ConsumerController {
@Autowired
private PayService payService;

@GetMapping("/consumer/pay")
private String pay() {
log.info("=======Feign进行远程调用=======");
return payService.pay();
}
}

修改服务提供方:Pay9001

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

主启动类添加注解:@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server:
port: 9001

spring:
application:
name: pay-service
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848


#eureka:
# client:
# #false表示不向注册中心注册自己。
# register-with-eureka: true
# #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
# fetch-registry: true
# service-url:
# #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
# defaultZone: http://localhost:7001/eureka/
# instance:
# instance-id: pay-service:9001
# prefer-ip-address: true #访问路径可以显示IP地址

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

界面:

72929eb93c13b6267384548b2d7d730e.png

Nacos 支持AP和CP模式的切换

如何选择:

一般来说,
如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如 Spring cloud 和 Dubbo 服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。

如果需要在服务级别编辑或者存储配置信息,那么 CP 是必须,K8S服务和DNS服务则适用于CP模式。
CP模式下则支持注册持久化实例,此时则是以 Raft 协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。

1
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'

配置中心

1
2
3
4
5
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

修改NacosConsumer80项目,引入依赖:

1
2
3
4
5
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

修改bootstrap.yml

1
2
3
4
5
6
7
8
spring:
application:
name: consumer-service
cloud:
nacos:
config:
server-addr: 192.168.124.105:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置

注意,spring.application.name=consumer-service必须放在bootstrap.yml中,否则不能自动更新配置

修改application.yml

1
2
3
4
5
6
7
spring:
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848

业务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RefreshScope
@RestController
public class ConfigController {
@Value("${testName}")
private String testName;

@Value("${today.mood}")
private String mood;

@GetMapping("name")
public String name() {
return testName;
}

@GetMapping("mood")
public String mood() {
return "今天的心情是:" + mood;
}

}

Nacos中配置文件的匹配公式:

1
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

2e74ec37a952de701f85a879f41ddb74.png

Nacos会记录配置文件的历史版本默认保留30天,此外还有一键回滚功能,回滚操作将会触发配置更新

除了DataID,Nacos还支持Group和Namespcae,可以通过配置文件指定:

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: consumer-service
cloud:
nacos:
config:
server-addr: 192.168.124.105:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: cloud
namespace: prod

集群搭建

重启Nacos可以发现之前添加的配置文件都还存在,这是因为Nacos中存在内置数据源derby,当需要以集群形式启动时,内置数据源就会存在数据不一致的问题,Nacos支持Mysql数据存储。

cd /opt/app/nacos/conf可以看到mysql的初始化脚本。同样的,我们需要将mysql的配置信息添加到当前目录的application.properties中:

1
2
3
4
5
6
spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

创建cluster.conf,在其中添加Nacos的集群信息:

1
2
3
192.168.124.106:8848
192.168.124.106:8849
192.168.124.106:8850

Nginx+Nacos+MySQL

Sentinel

实现熔断与限流,相当于Hystrix。

安装启动

官网:home | Sentinel (sentinelguard.io)

GitHub - alibaba/Sentinel)

运行条件:java8;8080端口未被占用。java -jar sentinel-dashboard-1.8.7.jar

管理界面:ip:8080

登录账号密码都是sentinel

修改工程:NacosConsumer80,添加依赖:

1
2
3
4
5
6
7
8
9
10
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

添加sentinel配置

1
2
3
4
5
6
7
8
9
10
11
spring:
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848
sentinel:
transport:
dashboard: 192.168.124.105:8080
port: 8719

Sentinel采用的懒加载,需要请求之后才能在页面展示

效果:

b1745f6489fe30b0bc1eca94dc2dbdce.png

流控规则

52ae473cbf9f8699c335f2850da965c0.png

资源名:唯一名称,默认请求路径

针对来源:Sentinel可以针对调用者进行限流(填写微服务名),默认default(不区分来源)

阈值类型/单机阈值:

  • QPS(每秒钟的请求数量):当调用接口的QPS达到阈值时,进行限流
  • 线程数:当调用接口的线程数达到阈值时,进行限流(不准确,不常用)

是否集群:限流规则是针对服务的单个机器还是整个服务集群

流控模式:

  • 直接:接口达到限流条件时直接限流
  • 关联:当关联的资源达到阈值时,限流当前接口
  • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值就进行限流)

流控效果:

  • 快速失败:直接失败,抛出异常
  • Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor。经过预热时长,才达到设置的QPS阈值(适用于需要时间创建缓存的接口)
  • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效

降级规则

32c8aa6b716bb01e9e1044ca073f40c2.png

RT(平均响应时间,秒级)

  • 统计时长内的平均响应时间 超出比例阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级
  • 熔断时长过后关闭断路器
  • RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)

异常比列(秒级):QPS >= 5 且异常比例(秒级统计)超过比例阈值时,触发降级;时间窗口结束后,关闭降级

异常数(分钟级):异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

热点Key限流

5f4a4a75ddf470dab24bfad6c5f5845b.png

热点即经常访问的数据。

参数索引:可以根据请求参数的值来进行自定义限流策略

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("test")
@SentinelResource("testHotKey")
public String test(String p1, String p2) {
if (p1.equals("name") && p2.equals("mood")) {
return testName + "," + mood;
} else if (p1.equals("name")) {
return testName + "," + p2;
} else if (p2.equals("mood")) {
return p1 + "," + mood;
}
return "p1:" + p1 + " p2:" + p2;
}

当进入限流时,页面会直接报错,给用户不好的使用体验,我们可以通过@SentinelResource注解来自定义降级方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("test")
@SentinelResource(value = "testHotKey",blockHandler = "myBlockHandler")
public String test(String p1, String p2) {
if (p1.equals("name") && p2.equals("mood")) {
return testName + "," + mood;
} else if (p1.equals("name")) {
return testName + "," + p2;
} else if (p2.equals("mood")) {
return p1 + "," + mood;
}
return "p1:" + p1 + " p2:" + p2;
}

public String myBlockHandler(String p1, String p2, BlockException exception) {
return "服务降级:myBlockHandler:" + p1 + "," + p2;
}

处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;

系统规则

2e6af6e75a1d550ac82fca20b1aa60c5.png

  • 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 达到阈值即触发系统保护。

@SentinelResource

在上面的部分我们已经简单使用了@SentinelResource注解,但又出现了以下问题:

  1. 自定义处理方法和业务代码耦合
  2. 每个业务代码都需要添加兜底方法,代码膨胀

解决方案:

1
2
3
4
5
@GetMapping("testHandler")
@SentinelResource(value = "testHandler", blockHandlerClass = {TestBlockHandler.class}, blockHandler = "handlerException")
public String testHandler() {
return "用户:" + testName + "<br/>" + "今天的心情是:" + mood;
}
1
2
3
4
5
public class TestBlockHandler {
public static String handlerException(BlockException exception) {
return "自定义的限流处理信息......TestBlockHandler.handlerException";
}
}

其它属性:

  • value:资源名称,必需项(不能为空)

  • entryType:entry 类型,可选项(默认为 EntryType.OUT

  • blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

  • fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:

    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所以类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:

    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

规则持久化

在前面的实践中可以发现:一旦重启应用,sentinel规则将消失。

  1. 修改NacosConsumer80配置文件设置规则持久化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848
sentinel:
transport:
dashboard: 192.168.124.105:8080
port: 8719
datasource:
myDS:
nacos:
server-addr: 192.168.124.105:8848
data-id: ${spring.application.name}
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
  1. Nacos添加限流规则:

bfe376b0c484b14f2b44595249edb5e2.png

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "testHandler",
"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 Up,2表示排队等待;
clusterMode:是否集群。

重新启动服务后进行请求可以发现配置依然生效。

Seata

在进入分布式之后,每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

官网:Apache Seata

3c7922fc2aa034e6212bb8b46c43b7a6.png

从上面官网的图片可以了解到,seata主要使用四部分实现分布式事务:

  • 全局唯一的事务ID(Transaction ID XID
  • Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
  • Transaction Manager (TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
  • Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

处理过程:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
  2. XID 在微服务调用链路的上下文中传播;
  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

安装启动

这里下载的是2.0.0版本,查看conf目录下的application.yaml可知默认使用的是文件存储:

fa720ec8d402ce347f771a3638af8e21.png

script/server/db存在mysql数据库的脚本,复制执行后,修改配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 192.168.124.105:8848
namespace:
group: SEATA_GROUP
#username:
#password:
#context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
server-addr: 192.168.124.105:8848
group: SEATA_GROUP
namespace:
cluster: default
store:
# support: file 、 db 、 redis 、 raft
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.124.106:3306/seata?rewriteBatchedStatements=true
user: root
password: 123456
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000

启动:./seata-server.sh

使用实践

修改工程NacosConsumer80,添加依赖:

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

这里默认的seata客户端版本为1.5.2,如果排除掉引入2.0.0存在配置冲突问题

修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
server:
port: 80

spring:
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848
# sentinel:
# transport:
# dashboard: 192.168.124.105:8080
# port: 8719
# datasource:
# myDS:
# nacos:
# server-addr: 192.168.124.105:8848
# data-id: ${spring.application.name}
# group-id: DEFAULT_GROUP
# data-type: json
# rule-type: flow
seata:
registry:
type: nacos
nacos:
server-addr: 192.168.124.105:8848
#以下填写的是Seata客户端注册到nacos中的信息
namespace: ""
group: SEATA_GROUP
application: seata-server
#自定义事务组名称
tx-service-group: seata-test
service:
# 事务组与tc集群的映射关系
vgroup-mapping:
# default - tc服务(事务协调器)的集群名称;键为自定义事务组名称,与上面一致
seata-test: default

logging:
level:
# feign日志以什么级别监控哪个接口
com.test.service.PayService: debug

feign:
circuitbreaker:
enabled: true #在Feign中开启Hystrix
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000

hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 5000

可以对照seata-server的配置进行理解

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/consumer/dbPay/{userId}")
@GlobalTransactional(name = "dbPay", rollbackFor = Exception.class)
public String payDb(@PathVariable("userId") Long userId) {
Long l = orderService.dbOrder(userId);
String s = payService.payByDb(userId);
if ("fail".equals(s)) {
throw new RuntimeException("payService调用失败!");
}
orderService.dbOrderFinish(l);
return "success";
}

修改工程order8002,添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
server:
port: 8002

spring:
application:
name: order-service
#rabbitmq相关配置
# rabbitmq:
# host: 192.168.124.105
# port: 5672
# username: john
# password: 123456
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.124.106:3306/seata_order
username: root
password: 123456

seata:
registry:
type: nacos
nacos:
server-addr: 192.168.124.105:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
#自定义事务组名称
tx-service-group: seata-test
service:
# 事务组与tc集群的映射关系
vgroup-mapping:
# default - tc服务(事务协调器)的集群名称;键为自定义事务组名称,与上面一致
seata-test: default
1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("dbOrder/{userId}")
public Long dbOrder(@PathVariable("userId") Long userId) {
Order order = Order.builder().userId(userId).money(new BigDecimal(100)).count(1).productId(1L).status(0).build();
orderMapper.insert(order);
return order.getId();
}

@GetMapping("dbOrderFinish/{id}")
public String dbOrderFinish(@PathVariable("id") Long id) {
orderMapper.updateById(Order.builder().id(id).status(1).build());
return "success";
}

修改项目Pay9001,添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
server:
port: 9001

spring:
application:
name: pay-service
cloud:
nacos:
discovery:
server-addr: 192.168.124.105:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.124.106:3306/seata_pay
username: root
password: 123456


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

seata:
registry:
type: nacos
nacos:
server-addr: 192.168.124.105:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
#自定义事务组名称
tx-service-group: seata-test
service:
# 事务组与tc集群的映射关系
vgroup-mapping:
# default - tc服务(事务协调器)的集群名称;键为自定义事务组名称,与上面一致
seata-test: default
1
2
3
4
5
6
@GetMapping("dbPay/{userId}")
public String payByDb(@PathVariable("userId") Long userId) {
accountMapper.updateAccount(userId, 100);
//int i = 10 / 0;
return "进行支付: " + port;
}

执行理论

分布式的执行流程:

  1. TM 开启分布式事务(TM 向 TC 注册全局事务记录)
  2. 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 )
  3. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务)
  4. TC 汇总事务信息,决定分布式事务是提交还是回滚
  5. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束

AT模式

在上面的示例中,我们使用的就是对业务代码侵入最少也是最简单的AT模式。

原理介绍,Seata会使用数据库连接代理,对sql语句进行拦截。

  1. 一阶段加载

810bea557f28755fb52ca3b7c039ee60.png

  1. 二阶段提交:SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
  2. 二阶段回滚:使用before image还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

除了AT模式,还存在TCC模式,Saga模式和XA模式。

上一篇:
SpringBoot-1
下一篇:
Docker