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

基础回顾

先开启一个干净的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.4.6</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.4</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是最佳方案,其他几种可用来配置特殊情况。

服务熔断

可类比保险丝,当某个服务不可用或响应超时时,急时停止该服务,防止系统雪崩现象出现。

服务限流

处理高并发,限流排队等待,防止瞬间大流量造成服务崩溃。