Spring Cloud简述

Spring Cloud微服务是由多功能模块组成的,每个模块实现耦合性降低。

注意微服务是多个模块进行组合,我们对版本的控制必须严谨。

通过url:

https://start.spring.io/actuator/info 获取json数据,查看版本对应关系。

https://spring.io/projects/spring-cloud#learn 自行查看版本对应关系。

Spring Cloud模块分布如下:

  • 注册中心:Nacos、Eureka、Zookeeper、Consul
  • 服务调用:LoadBalancer、Ribbon、OpenFeign(Feign)
  • 服务熔断:Sentinel、Resilience4j、Hystrix
  • 服务网关:Gateway、Zuul
  • 服务配置:Nacos、Config
  • 服务总线:Nacos、Bus

Spring Cloud Alibaba官网:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

Spring Cloud Alibaba组件版本关系:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

基础回顾

先开启一个干净的maven父工程,随后会展开模块,pom文件如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tang</groupId>
<artifactId>SpringCloud</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<!-- 版本管理 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.18.20</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.17</druid.version>
<mybatis.spring.boot.version>2.1.4</mybatis.spring.boot.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>

模块配置 + 依赖 + yml

使用父子工程配置各个模块。

子模块依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml配置参数

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

spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver # mysql驱动
url: jdbc:mysql://localhost:3306/spring_cloud?useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: 111111

mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.tang.springcloud.entity

微服务支付服务端 entity-dao-service-controller

我们按照前后端分离的规范,每次执行操作后,向前端返回状态码信息,所以还要写一个状态码的封装类

  • CommonResult

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult <T>{
    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code, String message){
    this(code, message, null);
    }
    }
  • PaymentMapper

    1
    2
    3
    4
    5
    6
    @Mapper
    public interface PaymentMapper {
    public int create (Payment payment);

    public Payment getPaymentById(@Param("id") Long id);
    }
  • PaymentMapper.xml

    为了规范一般都会自行封装一个resultmap作为返回类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

    <mapper namespace="com.tang.springcloud.dao.PaymentMapper">
    <!-- useGeneratedKeys 添加语句执行后返回自增主键
    keyProperty 是配合自增主键使用的,将值返回给实体类的对应属性,然后get获取 -->
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
    insert into payment(serial) values(#{serial});
    </insert>

    <resultMap id="BaseResultMap" type="com.tang.springcloud.entity.Payment">
    <id column="id" property="id" jdbcType="BIGINT"/>
    <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>
    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
    select * from payment where id = #{id};
    </select>
    </mapper>
  • PaymentServiceImpl

    接口省略

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Service
    public class PaymentServiceImpl implements PaymentService {
    @Resource
    private PaymentMapper paymentMapper;

    @Override
    public int create(Payment payment) {
    return paymentMapper.create(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
    return paymentMapper.getPaymentById(id);
    }
    }
  • PaymentController

    对应url的http方法:post增、delete删、put改、get查

    注意对象参数要使用@RequestBody,接收客户端发送的数据

    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
    @RestController
    @Slf4j
    public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment){
    int result = paymentService.create(payment);
    log.info("添加的结果" + result);
    if(result > 0){
    return new CommonResult(200, "插入成功", result);
    }else {
    return new CommonResult(404, "插入失败", null);
    }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id){
    Payment payment = paymentService.getPaymentById(id);
    log.info("查询结果" + payment);
    if(payment != null){
    return new CommonResult(200, "查询成功", payment);
    }else {
    return new CommonResult(404, "查询失败,失败id是" + id, null);
    }
    }
    }

测试功能

由于浏览器限制,我们可以使用Postman测试post、get等请求

localhost:8001/payment/get/1

localhost:8001/payment/create?serial=wewewew

Devtools开启热部署

1
2
3
4
5
6
7
<!-- 热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 父类工程启用插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
  • 在setting–Build–Compiler下勾选,以下四个开头的条件:

    Auto···、Display···、Build···、Compile···。

  • 组合键:ctrl + alt + shift + 左斜杠,选中1、Registry···,勾选:

    compiler.automake.allow.when.app.running

    actionSystem.assertFocusAccessFromEdt

微服务客户端 entity-controller

客户肯定是不能操作业务的,所以我们肯定没有service进行调用,使用RestTemplate

RestTemplate是HTTP请求工具,提供了常见的请求方案模板。

这里客户端使用http默认端口80,不用主动指明端口号,因为客户是不用关心端口的,只用输入对应的url即可。

  • entity实体类和服务端一致

  • AppliCationContextConfig配置RestTemplate

    将组件进行手动注入

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class ApplicationContextConfig {
    @Bean
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }
  • OrderController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @RestController
    @Slf4j
    public class OrderController {
    private static final String PAYMENT_URL = "http://localhost:8001";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
    return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
    }

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayemntById(@PathVariable("id") Long id){
    return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
    }
    }

客户端:http://localhost/consumer/payment/get/1

无需端口号,对服务器进行访问。

工程重构

我们发现客户端与服务端实体类重复,希望进行复用操作。单独分一个模块方实体类、工具类等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<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>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>

然后把实体类和工具类都写到这个模块,在maven工具上进行clean-install操作打包,接下来我们去之前的模块把实体类删除,在pom中引用改模块的依赖即可导入。

1
2
3
4
5
6
<!-- 自定义api包通过maven生成 -->
<dependency>
<groupId>com.tang</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>

注册中心

Eureka

服务的中转站,发现并维护服务,让后提供服务给客户,比如长时间未使用的服务则自行删除,不再提供给客户。

Eureka分为服务端与客户端,我们需要在注册中心的启动器入口上进行服务端配置,其余组件进行客户端配置。

  • Eureka Server

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  • Eureka Client

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</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
<dependencies>
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 自定义api包通过maven生成 -->
<dependency>
<groupId>com.tang</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>

<!--boot web actuator-->
<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>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001

eureka:
instance:
hostname: localhost # eureka服务端实例名称
client:
# false表示自己不会注册到注册中心
register-with-eureka: false
# 自身就是注册中心,不需要检索服务
fetch-registry: false
# 与Eureka进行查询服务和注册服务需要用到的地址
service-url:
defalutZone: http://${eureka.instance.hostname}:${server.port}/eureka/
1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}

基础配置(客户端)

pom导入Eureka-client的依赖。

1
2
3
4
5
6
7
eureka:
client:
# 开启Eureka对1组件进行检索与注册
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka

在启动入口上添加注解:@EnableEurekaServer。

然后在7001端口的eureka可以看到注册进来的8001组件。

Eureka集群

Eureka包括多台服务器,每台服务器都会注册除自己以外的所有服务器,保证能获取其他服务器的信息。以便切换。

修改host文件,加入自己eureka的多机ip配置

1
2
127.0.0.1       eureka7001.com
127.0.0.1 eureka7002.com

服务器修改yml,每个服务器配置其他所有服务器,逗号分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001

eureka:
instance:
hostname: eureka7001.com # eureka服务端实例名称
client:
# false表示自己不会注册到注册中心
register-with-eureka: false
# 自身就是注册中心,不需要检索服务
fetch-registry: false
# 与Eureka进行查询服务和注册服务需要用到的地址
service-url:
defaultZone: http://eureka7002.com:7002/eureka

客户端修改yml

1
2
3
4
5
6
7
eureka:
client:
# 开启Eureka对1组件进行检索与注册
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

然后我们发现集群的eureka都注册了实例,且集群之间服务器都相互加载。

支付服务端集群

拷贝一个8002支付工程,和8001一模一样,注意改端口号。在Controller中添加以下内容,并在后续操作中返回端口号。

1
2
3
4
@Value("${server.port}")
private String serverPort;

return new CommonResult(200, "查询成功,serverPort:" + serverPort, payment);

此时客户端不能写死端口号,转变成微服务名,且要开启负载均衡,因为集群服务名相同,没有配置负载均衡则无法识别。

1
2
3
4
5
6
7
8
9
10
// url改变为服务名称,不是写死的端口号
private static final String PAYMENT_URL = "http://cloud-payment-service";

// 客户端config中,给restTemplate组件添加注解
// @LoadBalanced开启负载均衡
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}

配置好后发现集群服务器会交替使用,负载均衡。

完善处理

在eureka界面发现各个服务的默认名是带有主机的,很冗杂,可以自定义名称,并且可以开启服务的正确ip跳转。

1
2
3
4
eureka:
instance:
instance-id: payment8001
prefer-ip-address: true

服务发现Discovery

配置支付服务

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

@GetMapping(value = "/payment/discovery")
public Object discovery(){
// 发现服务
List<String> services = discoveryClient.getServices();
for(String s : services){
log.info("服务:" + s);
}

// 发现服务下是实例
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
for(ServiceInstance instance : instances){
log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return this.discoveryClient;
}

@EnableDiscoveryClient需要加到服务启动类上开启服务发现

Eureka自我保护机制

当一个微服务不能使用时,eureka不会马上进行清理,而是对该服务信息进行保存。

在分布式架构CAP理论中属于AP分支。

Zookeeper

apache官方下载zookeeper,在linux完成解压

https://dlcdn.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz

解压后进入bin目录

1
2
./zkServer.sh start
./zkCli.sh

支付业务注册到zookeeper

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
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</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
server:
port: 8004

spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.158.137:2181
1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}

完成基础配置,启动服务,在linux查看zookeeper注册情况是否成功

1
2
3
4
[zk: localhost:2181(CONNECTED) 4] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 5] ls /services
[cloud-provider-payment]

业务测试

1
2
3
4
5
6
7
8
9
10
11
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String serverPort;

@RequestMapping(value = "/payment/zk")
public String paymentzk(){
return "springcloud with zookeeper:" + serverPort + "\t" + UUID.randomUUID().toString();
}
}

http://localhost:8004/payment/zk,发现uuid返回成功

节点类型

可对比Eureka自我保护,zookeeper是临时节点,当服务宕机后,过了一个心跳时间不会保存服务。重启服务后则是新开的一个服务。

客户端调用服务

pom服务端一致

1
2
3
4
5
6
7
8
9
server:
port: 80

spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: 192.168.158.138:2181

controller调服务,template需要先注册,和之前一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@Slf4j
public class OrderZKController {
public static final String INVOKE_URL = "http://cloud-provider-payment";

@Resource
private RestTemplate restTemplate;

@GetMapping(value = "/consumer/payment/zk")
public String paymentInfo(){
String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
return result;
}
}

最后发现客户端可以使用,且linux上的zookeeper也能发现两个服务。

Consul

官方下载windows版本:https://www.consul.io/downloads

解压后进入文件目录命令行执行consul agent -dev开启consul可视化界面

http://localhost:8500/ui/dc1/services 访问可视化界面

服务端注册

相关配置:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8006

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

业务和zookeeper一样,进行简单端口返回测试,查看consul是否注册成功。主启动类使用@EnableDiscoveryClient。

客户端注册调用服务

pom同服务端。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 80

spring:
application:
name: consul-consumer-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}

config注册template组件,然后controller使用网络连接调用服务端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@Slf4j
public class OrderZKController {
public static final String INVOKE_URL = "http://consul-provider-payment";

@Resource
private RestTemplate restTemplate;

@GetMapping(value = "/consumer/payment/consul")
public String paymentInfo(){
String result = restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);
return result;
}
}

最后url测试,服务调用成功。

注册中心的选择

注册中心作用都一样,但每一个在分布式系统中的定位不同,也就是CAP实现不同。

  • C:Consistency(强一致性),所有节点在集群中具有相同数据
  • A:Avaliability(可用性),保证请求无论成功失败都有响应,但可能接收到的数据节点是过时或错的。
  • P:Partition(分区容错性),对于分布式分区容错是必备的,也就是集群中部分机器宕机,不会影响整个系统的运作。

一般来说分布式系统必须实现P,但全部特性又不能同时满足,所以一般是CP、AP两种方式。

  • CA:单点集群,满足一致性,一般不利于扩展。、
  • CP(Zookeeper、Consul):满足一致性、分区容错,但性能不高。
  • AP(Eureka):满足可用性、分区容错,对一致性要求低。

服务调用(负载均衡)

Ribbon

我们在eureka集群环境下测试该技术

Ribbon主要实现负载均衡(Load Balance)

  • 集中式LB(Nginx):在服务消费者与提供方之间独立的负载均衡设施,如Nginx反向代理,由该设备将请求通过策略转发给服务的提供方。
  • 进程内LB(Ribbon):将负载均衡逻辑集成到消费者,由消费者从注册中心选取合适的服务器进行调用。

对应依赖(不适用):

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

RestTemplate

我们在注册中心的客户端就使用过@LoadBalance + RestTemplate完成服务调用及负载均衡。这里要说说RestTemplate。

RestTemplate对应get、post都有两种请求方法,这里以get举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 返回json
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayemntById(@PathVariable("id") Long id){
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}

// 返回ResponseEntity对象,包含响应头、响应体、状态码等,可获取更多信息
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayemntById2(@PathVariable("id") Long id){
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);

// 对象可根据状态码进行判断
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();
}else {
return new CommonResult<>(404, "操作失败");
}
}
  • getForObject:获取响应数据,类似一个json
  • getForEntity:获取整个响应,信息更多。

分情况使用,post和get一样,只是换了前缀。

负载均衡算法替换

Ribbon依赖中有一个接口IRule,一般实现该接口的类就对应了一种算法,抽象类除外。有轮询、随机等。

我们不想使用默认的轮询算法。可以自行构造一个新算法规则,但官方规定这个规则不应该放在@ComponentScan能扫描的包中,这样做不到特殊化,也就是说我们要跳出主启动类所在包,新建一个规则包来实现规则。

注意坑,现在版本eureka-client没有集成ribbon,所以不能直接使用IRule自定义策略,需要去导入Ribbon包,但我执行导入Ribbon运行都会冲突报错,没找到原因,只能说老技术没人维护是这样的,赶紧往后学Nacos配合Feign,负载均衡策略都可以在yml改,不用自定义类过于麻烦。

解决办法:https://blog.csdn.net/Curtain_show01/article/details/116838815

https://www.cnblogs.com/minejava/p/14851495.html

我们使用Spring Cloud LoadBalancer顶替Ribbon,LoadBalancer被Eureka所集成

1
2
3
4
5
6
7
8
9
10
// 自定义负载均衡算法配置,不用@Configuration注册
// 但测试后发现加不加@Configuration都可,应该是不用注册成组件的,这里不太清楚
public class MyLoadBalance {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
1
2
3
4
5
6
7
8
9
10
// 在原本的RestTemplate设置类中使用注解@LoadBalancerClient修改配置
@Configuration
@LoadBalancerClient(name = "cloud-payment-service", configuration = MyLoadBalance.class)
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}

然后我们再使用改客户端即可完成随机算法的负载均衡。

1
2
// 第二种方法是自定义负载均衡配置加上@Configuration进行注册,然后在主启动类加上以下注解
@LoadBalancerClients(defaultConfiguration = MyLoadBalance.class)

OpenFeign

传统Ribbon + RestTemplate对http请求封装,形成模板化调用方法,而Feign在该基础上进一步封装,通过接口 + 注解进行配置,进一步简化操作。(类比Dao层配合Mapper注解)

服务调用

基础配置:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
1
2
3
4
5
6
7
8
server:
port: 80

eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

service对应你要调用的服务端服务,我们原本业务是使用RestTemplate + 逻辑自行实现Controller,而OpenFeign是先定义服务的接口,指明服务与业务。

1
2
3
4
5
6
@Component
@FeignClient(value = "cloud-payment-service")
public interface PaymentFeignService {
@GetMapping(value = "/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

Controller直接获取业务接口的实例,通过它调用服务端的业务,不用注入RestTemplate。

1
2
3
4
5
6
7
8
9
10
11
@RestController
@Slf4j
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;

@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymetById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id);
}
}

主启动类使用@EnableFeignClients,开启OpenFeign

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

测试客户端链接,发现能调用业务,且开启了轮询负载均衡。

超时控制

OpenFeign默认等待服务1s,我们模拟业务等待3s,客户端调用后会报错

1
2
3
4
5
6
7
8
9
10
// 服务端
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeOut(){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
1
2
3
4
5
6
7
8
// 接口
@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeOut();
// 实现类
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeOut(){
return paymentFeignService.paymentFeignTimeOut();
}
1
2
3
ribbon:
ReadTimeout: 1000
ConnectTimeout: 1000

配置yml可延长超时时间。

ps:不知道是什么问题,我客户端一直在等待,没有默认1s的判断???不管了反正只是一个小点

OpenFeign自带日志输出

  • NONE:默认不显示日志
  • BASIC:记录请求方法、URL、响应状态码、执行时间
  • HEADERS:除BASIC定义的信息外,还有请求和响应的头信息
  • FULL:除HEADERS定义的信息外,还有请求与响应的正文、元数据
1
2
3
4
5
6
7
8
// config类
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
1
2
3
4
# yml配置需打印日志的接口的等级
logging:
level:
com.tang.springcloud.service.PaymentFeignService: debug

Hystrix

当微服务配置多了,一个微服务会调用多个微服务,也就是扇出(广播),而其中某一个微服务出现问题,会影响其他所有微服务,好比雪崩。简单就是说高可用被破坏。

Hystrix在某一个服务出现问题时,不会导致整个服务全部失败,避免级联故障,可以提高分布式系统的弹性。

基础服务构建

  • 服务端:
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8001

spring:
application:
name: cloud-provider-hystrix-payment

eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com/7001/eureka
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 业务模拟,一个耗时短,一个耗时长
@Service
public class PaymentService {
public String paymentInfo_OK(Integer id){
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK, id:" + id + "\t";
}

public String paymentInfo_TimeOut(Integer id){
int time = 3;
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut, id:" + id + "\t" + "耗时" + time + "s";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注入Controller
@RestController
public class PaymentController {
@Resource
private PaymentService paymentService;

@GetMapping(value = "/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_OK(id);
return result;
}

@GetMapping(value = "/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
}
  • 客户端:
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
server:
port: 80

eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka
1
2
3
4
5
6
7
8
9
10
// Feign接口调服务
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id);

@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Controller服务注入
@RestController
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;

@GetMapping(value = "/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}

@GetMapping(value = "/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}

如果我们现在对服务端施加压力测试,多线程执行服务,那么客户端和服务端执行操作时都会变慢,加载时间变长,此时需要相应措施。

服务降级(fallback)

服务器压力剧增,可对不重要的服务进行延迟或暂停处理,以便释放服务器资源,保证核心服务正常运行。

服务端设置服务时间阈值,若超时则执行兜底方法处理,也就是进行降级操作。且以下情况都会发生服务降级:程序运行异常 + 超时

注意,以下配置超时时间,不配置则使用默认值1s。

服务端

1
2
// 主启动类开启Hystrix
@EnableHystrix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 线程超时,执行兜底方法,参数1兜底方法,参数2超时时间
// 服务出现异常也会直接进行降级操作,比如执行10/0
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandle", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value="3000")
})
public String paymentInfo_TimeOut(Integer id){
// int i= 10 / 0;
int time = 5;
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut, id:" + id + "\t" + "耗时" + time + "s";
}

public String paymentInfo_TimeOutHandle(Integer id){
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOutHandle, id:" + id + "\t";
}

客户端

可以发现两端对应时间配置可以不一样的,这里服务响应是3s,刚刚服务端配置5s可以响应,现在客户端只配置1s则不能响应。

1
2
// 主启动类开启Hystrix
@EnableHystrix
1
2
3
4
5
6
7
8
9
10
11
12
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandle", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value="1000")
})
@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}

public String paymentInfo_TimeOutHandle(Integer id){
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOutHandle, id:" + id + "\t";
}

技术重构–全局fallback配置

对于兜底方法可以进一步简化,设置一个全局兜底,没有自行配置则走整个方法,不用每次服务降级都要自行配置兜底方法。以下为改变后的客户端,使用 @DefaultProperties 全局兜底

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
@RestController
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
})
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;

@GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}

@HystrixCommand
@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}

// 全局fallback
public String paymentGlobalFallbackMethod() {
return "全局异常,稍后重试";
}
}

全局fallback,可应对服务端宕机

1
2
3
4
# 客户端配置熔断器
feign:
circuitbreaker:
enabled: true
1
2
3
4
5
6
7
8
9
10
11
12
13
// 配置全局Fallback,直接实现业务接口
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "paymentInfo_OK,NO";
}

@Override
public String paymentInfo_TimeOut(Integer id) {
return "paymentInfo_TimeOut,NO";
}
}
1
2
// 业务接口fallback属性声明实现接口的Fallback类
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)

经过实验,使用该方法开启全局fallback,可应对服务端宕机,宕机后不会显示服务404,而是走全局fallback进行服务提醒。而且注释前面的 @HystrixCommand + @DefaultProperties,我们服务超时和运行异常也会走这个全局fallback,可以说这个实现接口的全局fallback是最佳方案,其他几种可用来配置特殊情况。

服务熔断

可类比保险丝,当某个服务不可用或响应超时时,急时停止该服务,防止系统雪崩现象出现。而当检测到某个微服务响应正常后,恢复调用链路。

HystrixCommandProperties,这个抽象类包含了我们可以配置的参数信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//service,服务端配置熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //断路器是否开启
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //请求时间ms
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") //失败率,达到就熔断
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
if(id < 0){
throw new RuntimeException("------负数-------");
}
// 自行封装的maven依赖中引用了hutool工具包,随机生成uuid
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
//服务降级调用方法
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能是负数,请稍后再试,id: " +id;
}
1
2
3
4
5
6
//controller
@GetMapping(value = "/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
return result;
}

解释以下配置的参数,开启断路器,若10s内10次请求失败率达到60%,则熔断。而后续正确回升则会恢复链路。我们测试时先输入正数会成功调用返回uuid,输入负数则抛异常走兜底方法处理。但10s内多次失败后,我们再去输入整数,返回的是兜底方法,因为失败率达到60%被熔断了,而我们后续多次调用正数,失败率降低,又可以走正常方法返回uuid。

熔断后不再走正常逻辑,而是执行服务降级的兜底方法,但熔断可以自行恢复链路重新执行正常逻辑。

工作流程

43.png

图形化Dashboard

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
1
2
3
4
5
<!-- 其他模块记得配置actuator,进行图形化监控 -->
<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
15
/** 在被监控的模块主启动器上注入
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*否则,Unable to connect to Command Metric Stream 404
*/
@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;
}

访问仪表盘界面:http://localhost:9001/hystrix,输入 http://localhost:8001/hystrix.stream 对服务进行图形化监控,然后我们就可以监控服务熔断demo的流程。

Gateway网关

工作流程

  • 路由(Route):匹配规则,由一系列断言和过滤器组成,若断言匹配成功则选择该路由。
  • 断言(Predicate):可以匹配HTTP请求的全部内容,断言判断为true则继续进行路由,即选择路由的路径。
  • 过滤(Filter):Spring的GatewayFilter实例,使用过滤器可在请求被路由前后对请求进行修改。

44.png

客户端经过网关的路由、Predicate、以及过滤器的筛选后达到服务端。这个过程是双向的,其中过滤器分为pre、post两类,pre是过滤客户端到服务端,post过滤服务端到客户端。

网关搭建

1
2
3
4
5
<!-- 网关不是web工程,无需配置web、actuator启动器 -->
<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
18
19
20
21
22
23
24
25
26
server:
port: 9527

spring:
application:
name: cloud-gateway
# 在9527网关上配置8001服务信息
cloud:
gateway:
routes:
- id: payment_route #路由的ID
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由

- id: payment_route2
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/feign/**

eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka

然后运行8001服务与9527网关,分别访问;

http://localhost:8001/payment/feign/timeouthttp://localhost:9527/payment/feign/timeout

可以通过9527访问8001的服务,返回的端口号依旧是8001,通过网关进行中转,我们无需暴露真实服务端口。

动态路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
#开启从注册中心动态创建路由的过程,通过微服务名进行路由
enabled: true
routes:
- id: payment_route
uri: lb://cloud-payment-service
predicates:
- Path=/payment/get/**

- id: payment_route2
uri: lb://cloud-payment-service
predicates:
- Path=/payment/feign/**

先开启动态路由配置,然后对网关配置中写死的uri进行替换,从注册中心获取动态路由,格式是lb://微服务名,断言路径不变。

常用的preticates

官方配置:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-request-predicates-factories

  • 时间限制
1
2
3
4
5
6
7
# 时区时间的生成:ZonedDateTime time = ZonedDateTime.now();
- id: payment_route2
uri: lb://cloud-payment-service
predicates:
- Path=/payment/feign/**
# 提前上线,定时起效
- After=2021-10-17T10:40:00.781+08:00[Asia/Shanghai]

示例:我们将上面的路由设置After时区,规定只有在这段时间之后服务网关才会生效,类似的还有before、between

  • cookie限制
1
2
predicates:
- Cookie=username,tang
1
2
3
# 不带cookie无法通过,带cookie可以通过
curl http://localhost:9527/payment/feign/timeout
curl http://localhost:9527/payment/feign/timeout --cookie "username=tang"

设置cookie的kv键值对,只有对应键值对的请求可以正常访问。

  • Header限制
1
2
predicates:
- Header=X-Request-Id, \d+

请求头要有X-Request-Id这个属性,且值为整数(正则表达式)

1
2
# 带有对应请求头的请求才能正常访问
curl http://localhost:9527/payment/feign/timeout -H "X-Request-Id:123"

Filter

可在请求被路由前后对请求进行处理。

官方配置:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories

1
2
3
4
# yml可进行简单的请求头添加,但按照工作流程,filter是在predicates后执行的
# 所以这里添加请求头不会影响predicates的请求头筛选判断
filters:
- AddRequestHeader=X-Request-Id,123

推荐使用全局Filter,功能更丰富,可自行配置日志输出。主要是实现两个接口,GlobalFilter和Ordered

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Slf4j
public class LogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 请求过滤,不为空链路放行,为空打印日志
log.info("FilterLog" + new Date());
String name = exchange.getRequest().getQueryParams().getFirst("name");
if(name == null){
log.info("用户名为null???");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
// 值越小,优先级越高
return 0;
}
}

http://localhost:9527/payment/get/1?name=tang 符合过滤器的规则可以正常访问。

http://localhost:9527/payment/get/1?username=xxx 只要不符合直接404,无连接,后台也会打印日志。

服务配置

Config

服务端配置

每个微服务都有一个配置文件,但服务多了以后一个个修改配置效率太低并任意出错,所以使用配置中心避免重复配置。为不同的微服务环境提供中心化的外部配置。这个外部配置指的是把配置文件上传到github/gitee上,然后通过yml读取仓库的配置。

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
<version>3.0.4</version>
</dependency>
1
2
3
4
5
6
7
@SpringBootApplication
@EnableConfigServer
public class ConfigMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigMain3344.class, args);
}
}

http://config-3344.com:3344/master/config-dev.yml

http://config-3344.com:3344/master/config-prod.yml

http://config-3344.com:3344/master/config-test.yml

通过以上格式,/分支/文件名读取配置

客户端配置

application.yml是用户级资源配置,bootstrap.yml是系统级资源配置,优先级更高。我们为了引入外部的配置文件,需要使用bootstrap.yml,其先于application加载,我们拉取外部配置后,再加载application。

springcloud默认关闭bootstrap,需要我们手动加入依赖开启。

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.0.4</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 3355

spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称
#上述3个综合:master分支上config-dev.yml被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://config-3344.com:3344 #配置中心地址

eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
1
2
3
4
5
6
7
8
9
10
11
12
// 通过路径查看是否加载了正确的配置文件
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;

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

访问 http://localhost:3355/configInfo,和之前config-dev内容一致。

客户端手动动态刷新

我们实时修改仓库内的配置,3344可以实时更新,而3355服务读取配置却没有实时更新,需要我们重启服务才能更新配置。所以我们需要开启动态刷新解决该问题。

1
2
3
4
5
6
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
1
2
// 在刚刚资源访问路径业务类上添加刷新注解
@RefreshScope

但是直接网页刷新是不会变的。我们必须发送POST请求让服务刷新配置,但不用重启服务。

1
2
# cmd发送post,然后再查看配置发现已更新
curl -X POST "http://localhost:3355/actuator/refresh"

Bus(jar包问题)

上面Config并没有实现实时的更新,只是从重启服务变为了发送请求后更改,使用Bus消息总线完成自动动态刷新配置。Bus消息代理支持RabbitMQ和Kafka。

先再linux开启mq,访问mq图形化界面 http://192.168.158.138:15672。

自动动态刷新的两种实现:

  • Bus触发一个客户端的刷新,进而传播所有客户端都刷新。
  • Bus触发服务端ConfigServer的刷新,然后由服务端广播所有客户端进行刷新。

我们选择由服务端进行广播的模式吗,这种肯定是更优的。

服务端广播动态刷新

新建一个客户端3366一起测试,业务输出端口号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;

@Value("${server.port}")
private String serverPort;

@GetMapping("/configInfo")
public String getConfigInfo() {
return serverPort + "," + configInfo;
}
}

在3344服务端和3355、3366客户端加入依赖

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

配置文件也要引入mq相关,bus-refresh是官方指定的刷新端点。客户端也要配置mq的相关属性

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
rabbitmq:
host: 192.168.158.138
port: 5672
username: admin
password: 111

#rabbitmq相关配置,暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: "bus-refresh"
1
curl -X POST "http://localhost:3344/actuator/bus-refresh"

按理来说我们启动服务端和多个客户端,更新依赖后,往服务端端点bus-refresh发送POST请求即可广播全部的客户端。但我在使用bus-amqp这个mq连接依赖时遇到了问题,说缺失类(当前依赖缺失需要的类,使用其他依赖版本),自己删除jar包导入低版本仍无法解决,所以Bus的广播模拟失败,版本才是唯一神,更新换代太快了,网上也搜索不到解决办法就很难受,还得看我Nacos。

Stream(jar包问题)

MQ有多种款式,可使用Stream进行统一。stream会忽略底层使用的MQ差异,适配不同版本的MQ。

通过Stream的Binder对象进行中间件的交互。定义绑定器Binder作为中间层,实现了中间件的版本隔离。

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

常用注解

注解 说明
@Input 注解标识输入通道,通过该输乎通道接收到的消息进入应用程序
@Output 注解标识输出通道,发布的消息将通过该通道离开应用程序
@StreamListener 监听队列,用于消费者的队列的消息接收
@EnableBinding 将信道channel和exchange绑定在一起

现在这个绑定注解依旧过时了,可参考帖子下面的官方文档设置https://www.5axxw.com/questions/content/0b9xic,虽然注解过时了但还是可以用的。

消息生产者(output)

依赖及配置

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
server:
port: 8801

spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.158.139
port: 5672
username: admin
password: 111
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true

接口 + 实现类 + 业务Controller,启动eureka7001,访问http://localhost:8801/sendMsg,发现后台有随机数,且MQ内创建的channel产生了消息。

1
2
3
public interface MessageProvider {
public String send();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableBinding(Source.class)
public class MessageProviderImpl implements MessageProvider {
@Resource
private MessageChannel output;

@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println(serial);
return null;
}
}
1
2
3
4
5
6
7
8
9
10
@RestController
public class SendMessageController {
@Resource
private MessageProvider messageProvider;

@GetMapping("/sendMsg")
public String sendMessage(){
return messageProvider.send();
}
}

消息消费者(input)

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
server:
port: 8802

spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.158.139
port: 5672
username: admin
password: 111
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2
lease-expiration-duration-in-seconds: 5
instance-id: receive-8802.com
prefer-ip-address: true
1
2
3
4
5
6
7
8
9
10
11
@Component
@EnableBinding(Sink.class)
public class ReceiverMessageController {
@Value("${server.port}")
private String serverPort;

@StreamListener(Sink.INPUT)
public void input(Message<String> msg){
System.out.println("消费者1,消息:" + msg.getPayload() + "\t port: " + serverPort);
}
}

生产者发送随机序列号,通过withPayload发送,消费者通过getPayload获取序列号进行消费。开启8802消费8801信息,然后又遇到Bus的jar包问题,还好这个消息队列的处理不关键,我们记录一个大概即可,实在是不知道jar包问题该如何处理······

Sleuth链路跟踪

zipkin安装及启动 https://zipkin.io/

1
2
3
4
5
6
7
# get the latest source
git clone https://github.com/openzipkin/zipkin
cd zipkin
# Build the server and also make its dependencies
./mvnw -DskipTests --also-make -pl zipkin-server clean install
# Run the server
java -jar ./zipkin-server/target/zipkin-server-*exec.jar

访问 http://localhost:9411/ zipkin界面。

在服务8001和消费者80添加依赖和新的配置:

1
2
3
4
5
<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://localhost:9411
sleuth:
sampler:
#采样率值介于 0 到 1 之间,1 则表示全部采集
probability: 1
1
2
3
4
5
// 服务业务类添加链路请求路径
@GetMapping("/payment/zipkin")
public String paymentZipkin(){
return "Zipkin";
}
1
2
3
4
5
6
// 消费者调用服务端
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin(){
String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin", String.class);
return result;
}

使用消费者调用服务,9411端口会出现记录。注意使用单机环境要先把集群的配置和负载均衡关闭,不然会报错。

Nacos

下载及启动

Nacos充当服务配置中心,Nacos = Eureka + Config + Bus

在官方进行下载:https://github.com/alibaba/nacos/releases/tag/1.4.2

在命令行进入bin/startup.cmd进行启动,微盟单机模式需要修改.cmd的配置。

1
2
3
4
# 默认集群
set MODE="cluster"
# 切换单例
set MODE="standalone"

启动后访问:http://localhost:8848/nacos,初始账号密码均为nacos。

服务提供者

官方2.2.6版本相关配置:https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/en-us/index.html#_introduction

父类进行pom管理,子类配置依赖无需版本号,在yml中将9001端口配置到nacos。

1
2
3
4
5
6
7
8
9
<dependencymanagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencymanagement>
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9001

spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址

management:
endpoints:
web:
exposure:
include: '*'
1
2
3
4
5
6
7
8
9
10
11
// 业务类
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;

@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
1
2
3
4
5
6
7
8
// 主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}

然后我们在8848nacos上的服务列表中,可以找到注册的服务。

建立两个一样的服务端9001、9002,方便客户端测试Nacos负载均衡功能。

客户端消费者

nacos依赖集成ribbon,实现负载均衡

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 83

spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class OrderMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderMain83.class, args);
}
}
1
2
3
4
5
6
7
8
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@Slf4j
public class OrderController {
@Resource
private RestTemplate restTemplate;

@Value("${service-url.nacos-user-service}")
private String serverURL;

@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);
}
}

多次访问请求localhost:83/consumer/payment/nacos/1,发现服务端轮询调用,实现了负载均衡。

注册中心对比

Eureka:AP

Zookeeper:CP

Consul:CP

Nacos:AP & CP(可切换)

服务配置中心

1
2
3
4
5
6
7
8
9
<!-- 两个相关依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

和config一样,bootstrap优先级高于application,而使用nacos后,bootstrap读配置可以从nacos上读取,不用去git读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# bootstrap
server:
port: 3377

spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yml #指定yml格式的配置
1
2
3
4
5
6
# yml
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info

注意配置注解@RefreshScope,动态更新

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RefreshScope //支持Nacos的动态刷新功能。
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;

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

官方:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html

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

1
${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 类型。
1
2
3
4
5
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 我们nacos上的配置文件需要按照官方要求书写,服务名-配置类型-文件类型
# 也就是我们上面bootstrap和application的内容
nacos-config-client-dev.yml
nacos-config-client-test.yml

在nacos上data id进行对应声明,内容则是yml/properties格式配置。

最后走请求 localhost:3377/config/info 可读取配置文件的config:info: 内容,且修改版本后会动态刷新,相比于config + bus,nacos又快又好。

Nacos配置详解

Nacos配置由NameSpace + Group + Data Id构成,也就是逐层分类。

NameSpace命名空间,实现环境隔离,如生产、测试、开发三个环境,它们之间是相互隔离的。

Group,默认为DEFAULT_GROUP,Group可将不同微服务划分到同一个分组中。

Data Id也就是Service服务,一个服务可包含多个集群。

Data Id

刚刚分析了Data Id的官方配置:${prefix}-${spring.profiles.active}.${file-extension}

我们配置是对应Data Id分为bootstrap 和 application两个文件,其中bootstrap用于读取nacos配置信息,后续不修改,applition记录配置类型,即生产、测试等版本。也就是要修改的单独放。之后只用改application就能换版本,无需改变bootstrap。

1
2
3
4
spring:
profiles:
active: dev # 表示开发环境
# active: test # 表示测试环境

nacos-config-client-dev.yml、 nacos-config-client-test.yml,改配置文件spring.profiles.active属性即可指明要使用的环境。

Group

Data Id GROUP
nacos-config-client-info.yml DEV_GROUP
nacos-config-client-info.yml TEST_GROUP

配置服务名相同,组分配的不同的两个服务。通过config下group切换。

1
2
3
4
5
spring:
cloud:
nacos:
config:
group: TEST_GROUP

NameSpace

在Nacos命名空间处声明新的命名空间,默认为public保留空间不可删除,在config下namespace生成的将命名空间id进行配置。

1
2
3
4
5
spring:
cloud:
nacos:
config:
namespace: d52c7eba-1ada-4ed1-8934-69f388dbdbe9

Nacos持久化

官网配置信息:https://nacos.io/zh-cn/docs/deployment.html

Nacos原本使用内置数据库derby进行持久化,我们可以对持久化存储的数据库进行配置。将脚本表在本地mysql中构建,然后配置nacos的application。

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&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=111111

Nacos集群

切记linux开启nacos集群,在start.sh中设置jvm大小限制以免越界,虚拟机容量建议分配多一点。

nginx + nacos集群模拟,将集群ip映射到nginx上进行反向代理,关于nginx和nacos的相关配置就不赘述了,资料很多。

Sentinel(熔断 + 降级 + 限流)

下载、安装

官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html

然后我们按照官方文档的版本对应,cloud2.2.6对应sentinel1.8.1:https://github.com/alibaba/Sentinel/releases/tag/1.8.1

在jar包目录下执行

1
java -jar sentinel-dashboard-1.8.1.jar

然后访问本机8080便是图形化界面,账号密码都是sentinel。

监控初始化

xml新引入sentinel相关依赖,和nacos搭配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 8401

spring:
application:
name: sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719

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

feign:
sentinel:
enabled: true # 激活Sentinel对Feign的支持
1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelMain8401 {
public static void main(String[] args) {
SpringApplication.run(SentinelMain8401.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试业务
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}

@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName() + "\t" + "...testB");
return "------testB";
}
}

Sentinel采用懒加载机制,我们开启服务后不会主动监测,只有第一次服务被调用后才会被监控。

我们在簇点链路可以查看服务链接,并对它进行流控、降级、热点、授权等操作。

流控规则(单机)

  • QPS:每秒请求数,可设置阈值
    • 流控模式
      • 直接:api接口达到限流条件时,进行限流
      • 关联:A关联B资源,B达到阈值时,A自行限流
      • 链路:层级链路,设置上层入口监控,若对应的下层资源达到阈值,则对入口进行限流。
    • 流控效果
      • 快速失败:直接报错,Blocked by Sentinel (flow limiting)
      • Warm Up:冷加载、预热,官方默认coldFactor为3,初次QPS阈值为 阈值/coldFactor,然后随着时间的预热,慢慢将阈值恢复到设定的值。让流量逐步增加,而不是瞬间反应。
      • 排队等待:排队匀速通过,超时则排队,会设置超时等待时间。

一开始使用直接失败的配置,当前点击超过1次/s,sentinel会自行限流控制(Blocked by Sentinel (flow limiting))

使用关联模式,我们可以用Postman设置多次请求B,此时手动访问A发现被限流。

  • 线程数:每秒线程处理数量,可设阈值
1
2
3
4
5
6
7
8
9
@GetMapping("/testA")
public String testA() {
try{
TimeUnit.MILLISECONDS.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testA";
}

我们设置阈值为1,让第一个线程进来后睡10s,然后不断发起请求,前面的线程由于睡眠并没有处理完业务,然后当前申请的线程数超过阈值,则会进行限流。

线程数只有流控模式,没有流控效果选择(默认为直接失败)。

降级规则

https://sentinelguard.io/zh-cn/docs/circuit-breaking.html

  • 慢调用比例

    设置RT最大响应时间,当前响应时间大于该值,则记为慢调用,同时规定时长、请求数、阈值比例。若在规定时长内,达到请求数且慢调用比例超过阈值,则发生熔断。此后经过一段熔断时长进入half-open半开状态,并放行一次请求做测试,若当前响应超过RT继续熔断,反之结束熔断。

  • 异常比例

    规定时间内,请求达到最小请求数,且异常发生的比例超过阈值,则发生熔断。经过一段熔断时长后进入half-open,通过放行一次请求来判断是否结束熔断。比例范围0% ~ 100%。

  • 异常数

    规定时间内,请求达到最小请求数,且异常发生次数超过阈值。经过熔断时长后进入half-open,放行一次请求判断是否结束熔断。

热点规则

热点搜索访问量大,对热点数据进行限制。

controller新增兜底方法,使用**@SentinelResource对应Hystrix的@HystrixCommand**。

@SentinelResource只能处理违反控制台配置的情况,并使用兜底方法处理。如果是代码本身抛出运行时异常,是不会用兜底方法处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2) {
//int age = 10/0;
return "------testHotKey";
}

//兜底方法
public String deal_testHotKey (String p1, String p2, BlockException exception) {
//sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
return "------deal_testHotKey,o(╥﹏╥)o";
}

我们设置热点规则,资源名即@SentinelResource设置的value,设置的参数索引对应我们服务的参数,从0开始对应,也就是0对应第一个参数。只要当我们请求中有这个参数时就会走热点监控。超过设置的阈值就会走兜底方法,若我们没有兜底方法则直接报错。

参数例外项:当配置参数时某个特殊值时,阈值会不一样,可自行确认值的类型和值。

系统规则

https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

相当于一个全局入口配置。

@SentinelResource

blockHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}

public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}

@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200, "按url限流测试OK", new Payment(2020L, "serial002"));
}
}

我们对资源进行限流处理时,可以对@SentinelResource声明的value处理,限流会走自定义方法,没有配置则走默认。或是直接走@GetMapping的url,限流只会走自定义的方法。

而兜底方法每次都要配置,我们需要进行解耦操作。设置自定义限流处理类:CustomerBlockHandler

1
2
3
4
5
6
7
8
9
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----1");
}

public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----2");
}
}
1
2
3
4
5
6
7
8
//controller调用兜底方法处理类
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定义", new Payment(2020L, "serial003"));
}

fallback

配饰配置服务9003/9004和消费者84,消费者配置sentinel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 9003/9004的业务类
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
//模拟数据库
public static HashMap<Long, Payment> hashMap = new HashMap<>();

static {
hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
}

@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, payment);
return result;
}
}
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
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";

@Resource
private RestTemplate restTemplate;

@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", blockHandler = "blockHandler", fallback = "handlerFallback", exceptionsToIgnore = IllegalArgumentException.class)
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}

public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}

public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
}

启动服务,发现ribbon生效,默认轮询调用两个服务端口,且@SentinelResource可配置fallback 和 blockHandler。二者同时配置时,因控制台限流、降级抛出的异常优先由blockHandler处理。

  • fallback可设置业务代码异常的兜底方法。

  • blockHandler可设置控制台配置的熔断降级处理的兜底方法。

  • exceptionsToIgnore配置异常进行过滤,兜底方法不会处理。

整合OpenFeign

修改84消费者的配置,注意OpenFeign会与devtools依赖冲突,需去掉devtools

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
1
2
3
feign:
sentinel:
enabled: true

主启动类加上@EnableFeignClients注解

1
2
3
4
5
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping("/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
1
2
3
4
5
6
7
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(444, "服务降级,PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
1
2
3
4
5
6
7
8
// controller使用Feign配置兜底方法
@Resource
private PaymentService paymentService;

@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}

使用OpenFeign配置熔断的兜底方法,接口声明服务,实现接口作为fallback为服务熔断后的处理具体实现。然后在controller中无需使用restTemplate,直接使用接口的组件调用服务。

Sentinel持久化

每次重启应用,规则配置都会重置,所以我们需要进行持久化操作。这里使用8401服务模拟。

1
2
3
4
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
# nacos数据源配置
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow

完成依赖及配置后,进入Nacosxi新增配置,配置名即我们的服务名sentinel-service,选择json数据类型进行以下配置。然后在sentinel中我们就会加载这个限流的配置。

1
2
3
4
5
6
7
8
9
[{
"resource": "/rateLimit/byUrl",
"IimitApp": "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:是否集群

完成持久化配置后,我们每次调用服务后,sentinel会监听到服务,并加载Nacos中对应服务的流控配置。

Seata(分布式事务)

官网:http://seata.io/zh-cn/docs/overview/what-is-seata.html

分布式事务要保证全局数据一致性。Seata由1 + 3组成,即一个全局唯一的事务ID + TC + TM + RM构成。

  • TC (Transaction Coordinator) - 事务协调者

    维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器

    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器

    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。