0%

SpringCloud Hoxton.SR1版本和SpringCloud Alibaba

参考资料

前言

版本选择

SpringCloud选择目前较新的大版本Hoxton,小版本为SR1,根据官方推荐,SpringBoot应该选择2.2.2(见https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/html/spring-cloud.html)。

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

由于SpringCloud部分常用组件都停更了,例如Eureka、Config等,Alibaba提供了新的技术支持,例如Nacos。Alibaba旗下的微服务组件也需要进行版本控制:

1
2
3
4
5
6
7
8
9
10
11
12
 <dependencyManagement>
<dependencies>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

命名规范

IDEA新建项目/模块命名,单词分隔符推荐使用下划线_

SpringBoot服务别名spring.application.name单词分隔符要求使用短横线-

父工程搭建

新建普通Maven工程cloud2020并在pom中添加如下代码:

提示:引入以下代码到pom时,如果报红说找不到对应依赖不用管(可使用alt + enter选择disable inspection取消爆红),因为你本地Maven仓库不存在相关依赖,而这些代码仅作为依赖版本控制,并不是实际引用,真正子模块引入时自会去下载对应依赖。

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
<!--统一管理jar包版本-->
<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.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>

<!--子模块继承之后,提供作用:锁定版本+子module自身的坐标声明不用groupId和version-->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mysql驱动-->
<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>
<!--SpringBoot集成Mybatis-->
<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>
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<!--SpringBoot应用构建插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>

环境

  • IDEA版本:2019.3

  • Maven:3.5+,本文3.6.3

  • 编码设置:

  • 编译环境设置:1.8+

  • 隐藏IDEA配置文件,忽略IDEA自身使用的.idea.iml文件,在如下图位置添加红线标识的代码:

通用模块

建模块

新建子模块cloud_api_common

引pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<!-- optional标签表示假如你的Project A的某个依赖D添加了<optional>true</optional>,当别人通过pom依赖Project A的时候,D不会被传递依赖进来;当你引入某个重量级依赖或此依赖容易与其他工程的依赖冲突的时候建议加上该选项,可以节省依赖传递带来的开销,同时减少依赖冲突 -->
<!-- 热部署,本地环境使用,不应该提交到git远程库,可新建 ignore changelist 并将此修改移入ignore changelist -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- hutool工具集,各种优秀工具类,减少开发过程中上网查找复制的行为和精简代码 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>

写启动类

由于通用模块仅作为代码复用和通用代码集中管理,因此不需要SpringBoot启动类

写业务类

通用接口返回格式:

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

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

RPC回顾

支付服务

建模块

新建模块cloud_provider_payment模拟支付服务提供者

引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
<dependencies>
<!--引入自己定义的api通用包,可以使用Payment支付Entity-->
<dependency>
<groupId>top.zhenganwen.springcloud</groupId>
<artifactId>cloud_api_common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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>
<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>

写yml

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

spring:
application:
name: cloud-provider-payment
datasource:
type: com.alibaba.druid.pool.DruidDataSource #当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding-utr-8&useSSL=false
username: root
password: root

mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: top.zhenganwen.springcloud.entity #所有Entity别名类所在包

启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package top.zhenganwen.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
* @author zhenganwen
* @date 2020/3/29/029
* @desc
**/
@SpringBootApplication
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class);
}
}

业务类

从DAO到Controller,这里省略了Service。

DAO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package top.zhenganwen.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import top.zhenganwen.springcloud.entity.Payment;

/**
* @author zhenganwen
* @date 2020/3/29/029
* @desc
**/
@Mapper
public interface PaymentDAO {

int create(Payment payment);

Payment getById(@Param("id") Long id);
}

对应resource下新建mapper/PaymentMapper.xml用作SQL映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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="top.zhenganwen.springcloud.dao.PaymentDAO">

<resultMap id="BaseResultMap" type="top.zhenganwen.springcloud.entity.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="serial" property="serial" jdbcType="VARCHAR"/>
</resultMap>

<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values(#{serial})
</insert>

<select id="getById" parameterType="Long" resultMap="BaseResultMap">
select * from payment where id=#{id};
</select>
</mapper>
Controller
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
package top.zhenganwen.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import top.zhenganwen.springcloud.dao.PaymentDAO;
import top.zhenganwen.springcloud.entity.CommonResult;
import top.zhenganwen.springcloud.entity.Payment;

/**
* @author zhenganwen
* @date 2020/3/29/029
* @desc
**/
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {

@Autowired
private PaymentDAO paymentDAO;

@PostMapping
public CommonResult<Long> create(@RequestBody Payment payment) {
final int res = paymentDAO.create(payment);
if (res == 1) {
log.info("创建payment成功,流水号:{} ", payment.getSerial());
return new CommonResult<>(200, "创建成功", payment.getId());
} else {
return new CommonResult<>(10001, "创建失败");
}
}

@GetMapping("/{id}")
public CommonResult<Payment> getById(@PathVariable Long id) {
return new CommonResult<>(200, "ok", paymentDAO.getById(id));
}
}

订单服务

在此例中,订单服务作为消费者调用支付服务提供的接口。

建模块

新建子模块cloud_consumer_order80

引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
<dependencies>
<!--引入自己定义的api通用包,可以使用Payment支付Entity-->
<dependency>
<groupId>top.zhenganwen.springcloud</groupId>
<artifactId>cloud_api_common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

写yml

1
2
3
4
5
server:
port: 80
spring:
application:
name: cloud-consumer-order

启动类

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
package top.zhenganwen.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
* @author zhenganwen
* @date 2020/3/29/029
* @desc
**/
@SpringBootApplication
@EnableEurekaClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

业务类

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
package top.zhenganwen.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import top.zhenganwen.springcloud.entity.CommonResult;
import top.zhenganwen.springcloud.entity.Payment;

/**
* @author zhenganwen
* @date 2020/3/29/029
* @desc
**/
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

public static final String PAYMENT_DOMAIN = "http://localhost:8001";

@Autowired
private RestTemplate restTemplate;

@PostMapping("/payment")
public CommonResult<Long> pay() {
Payment payment = new Payment(null, "abcd");
return restTemplate.postForObject(PAYMENT_DOMAIN + "/payment", payment, CommonResult.class);
}

@GetMapping("/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable Long id) {
log.info("asdlksdkfldslkf");
return restTemplate.getForObject(PAYMENT_DOMAIN + "/payment/" + id, CommonResult.class);
}
}

访问http://localhost/order/payment/1如果能请求到数据库中的数据说明RPC调用成功

热部署正确姿势

1、引入依赖

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

2、引入Maven的SpringBoot插件

1
2
3
4
5
6
7
8
9
10
11
12
<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>

由于此插件已在父工程引入了,因此子模块都会继承

3、IDEA设置代码修改后自动编译

4、修改IDEA注册参数

ctrl shift alt /打开Registry面板,并勾上如下两个选项

5、使用Recompile快捷键

大多数网文介绍devtool时都是上述4个步骤,但是我发现我修改代码之后需要比较久的时间如30s/1m,感觉还是有点慢。

如果你和我有同样的困惑,不妨使用ctrl shif F9快捷键(当前窗口是Java类时会触发重新编译)手动触发热部署而进行SpringBoot应用的快速重启。

由于这个快捷键比较繁琐,因此我为Recompile新增了ctrl s快捷键:

这样再修改代码之后,例如将某个接口的@GetMapping改为@PostMapping之后,想触发devtool重启只需要按一下ctrl s就可以了,而不用等待其内部监控检查机制存在的延迟。

这种方式对于SQL映射文件或application.yml都是适用的,只要控制台打印了应用重启相关的日志,就说明代码修改已被应用上了

服务注册与发现

Eureka(已停更)

Eureka 2.0 (Discontinued)

The existing open source work on eureka 2.0 is discontinued. The code base and artifacts that were released as part of the existing repository of work on the 2.x branch is considered use at your own risk.

Eureka 1.x is a core part of Netflix’s service discovery system and is still an active project.

——https://github.com/Netflix/eureka/wiki

虽然Eureka已停止维护了,但国内很多一起仍在使用,所以说现在是一个停更不停用的状态。

前面我们通过RestTemplate已经实现了服务间的调用,那为什么还要有注册中心呢。

其实技术都是源于现实生活的,只要我们善于联想就不难理解。类比写字楼,服务提供者就相当于入驻写字楼的企业,而服务消费者就相当于进楼寻求服务的消费者,写字楼的物业就是注册中心。通过物业,进入大楼的所有消费者能够被集中管控、引导、限制等等,这样就为楼中的企业提供了一层保障,提供者和消费者之间也进行了解耦。

在回到Web应用场景,由于服务提供者可能存在多个实例,这个数量是由提供者负责方所管控的,而消费者负责方只能被动知悉。如果采用点对点调用的RPC架构(硬编码写死提供者地址),那么在提供者实例增多可供消费者复杂均衡或提供者实例地址发生变更时,消费者将无法进行灵活应对。

而如果采用注册中心式的三方架构,可对外提供服务的提供者只需注册服务别名-自身地址的映射关系到注册中心,而消费者仅通过服务名到注册中心查找对应的实例地址(若有多个实例则需指定多选一策略)再进行RPC调用,这样消费者就能灵活应对诸如提供者地址变更导致调用异常、提供者集群扩展而消费者无法分流等问题了。

当然了,注册中心也有它的弊端,那就是服务注册和发现的职能一旦失守,那么即使所有的服务完好,服务之间也都将无法通信。因此注册中心必须采用高可用的架构,最常见的就是分布式肌群。

下面我们来复习一下Eureka的集成,并实现通过EurekaClient将实际调用地址获取的责任从消费者转嫁给EurekaServer,最后实现一下EurekaServer集群搭建和负载均衡调用多实例服务

eureka server

建模块

新建子模块cloud_eureka_8001

引pom

引入EurekaServer依赖

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
<dependencies>
<!--eureka server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--引入自己定义的api通用包,可以使用payment支付Entity-->
<dependency>
<groupId>top.zhenganwen.springcloud</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>
</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>

写yml

eureka server相关配置

  • server主机名
  • 是否注册以及是否拉取注册信息
  • serivice-url:eureka server单机版时写自己的eureka服务HTTP端点;集群版时,写集群中其他server的eureka服务HTT端点,使用逗号分隔
1
2
3
4
5
6
7
8
9
10
server:
port: 7001
eureka:
instance:
hostname: localhost
client:
fetch-registry: false # 是否获取eureka server上的服务注册表到本地JVM内存,默认为true
register-with-eureka: false # 是否将当前应用注册到eureka server上的服务注册表中,默认为true
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

启动类

开启eureka server注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package top.zhenganwen.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

改造支付和订单服务

支付服务注册到注册eureka server上

修改cloud_provider_payment子模块

引pom

引入eureka client依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
写yml

配置要注册到哪里的注册中心上

1
2
3
4
5
6
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka # 注册中心地址,集群版时需编写每一个eureka server的地址并使用逗号分隔
instance:
prefer-ip-address: true # 调用服务实例时优先使用IP,避免域名调用多一层DNS解析耗时
启动类

添加启用eureka client的注解,以使当前应用将自身注册到注册中心(register-with-eureka默认为true)并将注册中心上已注册的服务别名-实例地址映射表缓存到本地(fetch-registry默认为true)

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

订单服务注册到eureka server上

虽然此例中,订单服务仅作为消费者,但是为了避免以后订单服务的接口被他人调用(身份转变为提供者),因此也不妨让register-with-eureka默认为true。

既然订单服务向通过eureka server获取支付服务的调用地址,当然要从eureka server上拉取注册信息,因此也让fetch-registry默认为true。

订单模块cloud_consumer_order_80的修改步骤同cloud_provider_payment,这里不再赘述。

访问eureka server web界面

访问localhost:7001,即可查看到支付服务和订单服务都已注册上去了,并且通过服务名就能找到实际调用IP

RestTemplate集成Eureka

如果你启用了eureka client,那么通过RestTemplate调用HTTP接口时,如果该接口所属服务在eureka server上有注册,那么可以通过对方服务别名(spring.application.name)来替代硬编码的实际域名,避免其域名变更导致接口无法调通

1
2
3
4
public class OrderController {

// public static final String PAYMENT_DOMAIN = "http://localhost:8001";
public static final String PAYMENT_DOMAIN = "http://CLOUD-PROVIDER-PAYMENT";

RestTemplate负载均衡-服务别名解析

然而,经过上述几步之后,访问http://localhost/order/payment/1,报错如下

这是因为虽然你启用了eureka client,但是原生的RestTemplate还只是将服务别名当做域名去走DNS解析,这时需要你通过@LoadBalanced注解告诉他,根据服务别名选取实际调用地址(对于多个服务实例,默认采用轮询算法)来进行调用。这里因为SpringBoot检测到我们引入了eureka client依赖,因此会根据从eureka server上拉取的服务注册信息进行服务别名解析。

如果使用其他的注册中心,如zookeeper、consul、nacos等,原理是一样的。

添加@LoadBalanced注解后,方可调用成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication
@EnableEurekaClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}

/**
* @LoadBalanced 通过restTemplate使用服务别名来调用接口时需添加此注解,若服务别名映射多个实例,默认采用轮询算法
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

验证轮询算法

此节,我们将支付服务扩展一个实例并将其注册到eureka server上,验证restTemplate是否会轮询调用两个服务的同一接口。

首先按照cloud_provider_payment创建一个同样的子模块,cloud_provider_payment_8002,只不过将server.port修改为8002。

然后在不同服务同一接口的返回值message上略作区分:

cloud_provider_payment

1
2
3
4
@GetMapping("/{id}")
public CommonResult<Payment> getById(@PathVariable Long id) {
return new CommonResult<>(200, "ok-8001", paymentDAO.getById(id));
}

cloud_provider_payment_8002

1
2
3
4
@GetMapping("/{id}")
public CommonResult<Payment> getById(@PathVariable Long id) {
return new CommonResult<>(200, "ok-8002", paymentDAO.getById(id));
}

如果连续访问http://localhost/order/payment/1,响应结果能够在8001和8002之间来回切换,说明负载均衡算法默认采用的是轮询

)

提示:如果结果一直是8001,那么有可能是因为8002刚注册到eureka server上,而8001本地缓存的服务注册信息还没更新(eureka client每隔一段时间会拉取eureka server上最新的注册信息更新本地缓存),可以将eureka server、8001、8002、order应用依次重启一下。

eureka server集群

类似于服务注册中心这样的中心化管理的架构最致命的一个缺点就是,中心应用单点故障易导致整个系统的瘫痪和不可用,而对中心应用做集群高可用是常见的解决方案。

此节我们将新建一个eureka server 7002,与7001形成一个“集群”,当其中少部分发生故障时并不会影响注册中心的使用。

为了模拟集群的真实性,我们为7001、7002分别分配一个域名eureka01.com和eureka02.com,并通过修改hosts将这两个域名都指向127.0.0.1(推荐使用SwitchHosts主机映射管理工具,注意使用管理员身份运行)

server互相守望

仿照cloud_eureka_7001新建一个子模块eureka_server_7002,并修改两者的yml(主要修改service-url部分):

  • cloud_eureka_7001

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 7001
    eureka:
    instance:
    hostname: eureka01.com
    client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
    defaultZone: http://eureka02.com:7002/eureka/ # 与集群中其他节点互相守望
  • eureka_server_7002

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 7002
    eureka:
    instance:
    hostname: eureka02.com
    client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
    defaultZone: http://eureka01.com:7001/eureka/ # 与集群中其他节点互相守望

依次启动7001和7002,并访问http://eureka01.com:7001/和http://eureka02.com:7002/,效果如下所示

)

client全都报备

采用eureka server集群架构后,eureka client就需要到每个server节点上注册一遍,不能厚此薄彼。因此order、8001、8002都需要修改yml

1
defaultZone: http://eureka01.com:7001/eureka,http://eureka02.com:7002/eureka

测试只要有一个节点可用,系统就可用

修改后依次重启,并访问http://localhost/order/payment/1,结果仍是轮询调用8001和8002,并且两个eureka server web端都能正常显示已注册的服务实例

这时,如果关闭eureka02,只留一个eureka01运行,并再次测试http://localhost/order/payment/1,发现接口运行正常,说明eureka server集群只要有一个存活就能坚守到底。

服务实例ID和IP完善

你可以通过eureka.instance节下的配置项来配置再eureka server web上显示的实例ID和IP地址,默认的效果(instance-idprefer-ip-address)如下:

实例ID的格式冗余了主机名;鼠标悬浮实例ID超链接上时,左下角超链接显示的是实例的主机名而非IP地址。这两处我们可以分别通过instance-idprefer-ip-address进行个性化配置,例如对于cloud_provider_payment:

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: 8001

spring:
application:
name: cloud-provider-payment
datasource:
type: com.alibaba.druid.pool.DruidDataSource #当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding-utr-8&useSSL=false
username: root
password: root

mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: top.zhenganwen.springcloud.entity #所有Entity别名类所在包

eureka:
client:
service-url:
defaultZone: http://eureka01.com:7001/eureka,http://eureka02.com:7002/eureka
instance:
prefer-ip-address: true # eureka server web上服务实例ID超链接显示IP
instance-id: payment8001 # 实例ID

效果如下:

注意引入starter-web依赖时也引入starter-actuator,eureka server对于服务实例运行状态等信息的采集就来源于starter-actuator提供的web端点 /actuator

1
2
3
4
5
6
7
8
<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>

DiscoveryClient服务发现

如果你想在你的代码中获取eureka server上注册的服务信息,可以通过org.springframework.cloud.client.discovery.DiscoveryClient接口,但此前,你需要在启动类上添加@EnableDiscoveryClient注解使SpringBoot帮你创建一个该接口的实现bean放入容器中,这样你就可以通过注入注解来使用了:

cloud_provider_payment

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class);
}
}
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
package top.zhenganwen.springcloud.controller;

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

import java.util.List;

@RestController
@RequestMapping("/service")
@Slf4j
public class ServiceController {

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping
public List<String> serviceList() {
// 获取eureka server上在册的所有服务的服务别名
final List<String> services = discoveryClient.getServices();
log.info("registered services: {}", services);
return services;
}

@GetMapping("/instances")
public List<ServiceInstance> serviceInstances() {
String serviceName = "cloud_provider_payment";
// 通过服务别名获取在册的所有服务实例信息
final List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
instances.forEach(i -> log.info("instance-id: {}, hostname: {}, port: {}, service-url: {}", i.getInstanceId(), i.getHost(), i.getPort(), i.getUri()));
return instances;
}
}

启动所有现有应用,测试接口如下:

  • http://localhost:8001/service

    1
    2
    3
    4
    [
    "cloud-consumer-order",
    "cloud-provider-payment"
    ]
  • http://localhost:8001/service/instances

    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
    95
    96
    97
    98
    99
    100
    [
    {
    host: "192.168.1.105",
    port: 8001,
    instanceId: "payment8001",
    uri: "http://192.168.1.105:8001",
    metadata: {
    management.port: "8001"
    },
    secure: false,
    serviceId: "CLOUD-PROVIDER-PAYMENT",
    instanceInfo: {
    instanceId: "payment8001",
    app: "CLOUD-PROVIDER-PAYMENT",
    appGroupName: null,
    ipAddr: "192.168.1.105",
    sid: "na",
    homePageUrl: "http://192.168.1.105:8001/",
    statusPageUrl: "http://192.168.1.105:8001/actuator/info",
    healthCheckUrl: "http://192.168.1.105:8001/actuator/health",
    secureHealthCheckUrl: null,
    vipAddress: "cloud-provider-payment",
    secureVipAddress: "cloud-provider-payment",
    countryId: 1,
    dataCenterInfo: {
    @class: "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
    name: "MyOwn"
    },
    hostName: "192.168.1.105",
    status: "UP",
    overriddenStatus: "UNKNOWN",
    leaseInfo: {
    renewalIntervalInSecs: 30,
    durationInSecs: 90,
    registrationTimestamp: 1585621526420,
    lastRenewalTimestamp: 1585621916641,
    evictionTimestamp: 0,
    serviceUpTimestamp: 1585621526420
    },
    isCoordinatingDiscoveryServer: false,
    metadata: {
    management.port: "8001"
    },
    lastUpdatedTimestamp: 1585621526420,
    lastDirtyTimestamp: 1585621526412,
    actionType: "ADDED",
    asgName: null
    },
    scheme: null
    },
    {
    host: "192.168.1.105",
    port: 8002,
    instanceId: "payment8002",
    uri: "http://192.168.1.105:8002",
    metadata: {
    management.port: "8002"
    },
    secure: false,
    serviceId: "CLOUD-PROVIDER-PAYMENT",
    instanceInfo: {
    instanceId: "payment8002",
    app: "CLOUD-PROVIDER-PAYMENT",
    appGroupName: null,
    ipAddr: "192.168.1.105",
    sid: "na",
    homePageUrl: "http://192.168.1.105:8002/",
    statusPageUrl: "http://192.168.1.105:8002/actuator/info",
    healthCheckUrl: "http://192.168.1.105:8002/actuator/health",
    secureHealthCheckUrl: null,
    vipAddress: "cloud-provider-payment",
    secureVipAddress: "cloud-provider-payment",
    countryId: 1,
    dataCenterInfo: {
    @class: "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
    name: "MyOwn"
    },
    hostName: "192.168.1.105",
    status: "UP",
    overriddenStatus: "UNKNOWN",
    leaseInfo: {
    renewalIntervalInSecs: 30,
    durationInSecs: 90,
    registrationTimestamp: 1585621810953,
    lastRenewalTimestamp: 1585621810953,
    evictionTimestamp: 0,
    serviceUpTimestamp: 1585621810953
    },
    isCoordinatingDiscoveryServer: false,
    metadata: {
    management.port: "8002"
    },
    lastUpdatedTimestamp: 1585621810953,
    lastDirtyTimestamp: 1585621810934,
    actionType: "ADDED",
    asgName: null
    },
    scheme: null
    }
    ]
  • 控制台日志

    1
    2
    3
    2020-03-31 10:31:20.629  INFO 11780 --- [nio-8001-exec-7] t.z.s.controller.ServiceController       : registered services: [cloud-consumer-order, cloud-provider-payment]
    2020-03-31 10:30:51.003 INFO 11780 --- [nio-8001-exec-1] t.z.s.controller.ServiceController : instance-id: payment8001, hostname: 192.168.1.105, port: 8001, service-url: http://192.168.1.105:8001
    2020-03-31 10:30:51.003 INFO 11780 --- [nio-8001-exec-1] t.z.s.controller.ServiceController : instance-id: payment8002, hostname: 192.168.1.105, port: 8002, service-url: http://192.168.1.105:8002

自我保护机制

在本地调试过程中,你可能经常在eureka server web上看到如下的红字警示:

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

特殊时刻!eureka可能无法正确判断在册(已持续一段时间没有收到他们的心跳包的)服务实例们是否是真的不可用(例如网络波动导致eureka server接收不到心跳包)。因为这些没有向eureka server发送心跳包以汇报自身状态的服务实例数超过了既定的阈值,因此eureka server进入保护模式并不再提出在册的服务实例。

服务shut down引起的自身无法通过稳定的心跳检测机制和eureka server保持联系,eureka server会注销一段时间内心跳检测都没有回应的服务。然而本地调试场景下,我们会经常重启、停止某些服务应用,导致一段时间内服务注销的次数超过了既定的阈值(例如,只有一个支付服务实例和一个订单服务实例注册到eureka server上时,如果我们停掉支付服务,这时eureka server就可能因为注销的实例数达到了在册实例数的一半而进入保护模式)而进入保护模式。在此模式下,eureka server将不再因为心跳检测而从服务注册表剔除未保持联系的实例,eureka server认为这段时间内剔除操作过于频繁,可能是因为eureka server自己的网络问题而并非被剔除的服务实例shutdown了。

这样的设计是遵循了“宁可保留也许不可用的服务实例,也不错误剔除健康的服务实例”的理念,这让系统架构更加健壮。

但是因为环境问题,我们可能需要对此机制的某些参数进行自定义配置,以方便对应环境下的开发。例如本地开发环境下,重启、停止操作频繁,我们想禁用自我保护机制;eureka server剔除操作自上一次健康检测通过后的等待时间太长,导致复杂均衡调用仍能调到刚停止了的服务实例上等。

现在仅以eureka server 7001和支付8001两个应用为例,期望禁用eureka server的自我保护机制,8001每隔1秒向7001发送一个心跳包,如果持续两秒7001没有收到8001的心跳包就将其剔除:

cloud_eureka_7001

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001
eureka:
instance:
hostname: eureka01.com
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka # 单机版
# defaultZone: http://eureka02.com:7002/eureka/ # 与集群中其他server互相守望
server:
enable-self-preservation: false # 禁用自我保护机制,默认true
eviction-interval-timer-in-ms: 2000 # 间隔多长时间没有收到心跳包就将服务实例剔除,默认60s

cloud_provider_payment

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

...

eureka:
client:
service-url:
defaultZone: http://eureka01.com:7001/eureka,http://eureka02.com:7002/eureka
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: payment8001
lease-renewal-interval-in-seconds: 1 # 每隔多少秒向eureka server发送心跳包,默认30s
lease-expiration-duration-in-seconds: 2 # 多少秒没有发送心跳检测就让eureka server将自身剔除,默认90s,小于eureka server端配置的eureka.server.eviction-interval-timer-in-ms时才有效

再次访问eureka server web:

shutdown 8001,过两秒刷新页面发现其已被剔除

Zookeeper

zookeeper作为分布式协调服务,也可用作服务注册和发现中心,并集成到SpringCloud中借助RestTemplate根据服务别名进行RPC调用

搭建zk服务

这里使用win10下的wsl-ubuntu,如果你使用虚拟机或云机器注意防火墙设置和IP互ping能通

下载安装包

1
$ wget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz

解压

1
2
$ tar zxf zookeeper-3.4.14.tar.gz
$ cd zookeeper-3.4.14

初始化配置文件

1
2
$ cd conf
$ cp zoo_sample.cfg zoo.cfg

启动服务

1
2
$ cd ..
$ bin/zkServer.sh start ZooKeeper JMX enabled by default Using config: /home/anwen/soft/zookeeper-3.4.14/bin/../conf/zoo.cfg Starting zookeeper ... STARTED

查看zk根路径下的节点

1
2
$ bin/zkCli.sh
[zk: localhost:2181(CONNECTED) 0] ls / [zookeeper]

发现只有系统初始创建的 [zookeeper]节点

支付服务入驻zk

建模块

新建子模块cloud_provider_payment8004

引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
<dependencies>
<!--SpringCloud集成zk作为服务注册发现中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!--引入自己定义的api通用包,可以使用Payment支付Entity-->
<dependency>
<groupId>top.zhenganwen.springcloud</groupId>
<artifactId>cloud_api_common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

写yml

配置zk的连接地址

1
2
3
4
5
6
7
8
server:
port: 8004
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 127.0.0.1:2181 # zk集群使用逗号分隔

启动类

使用zk/consul作为注册中心时,需添加@EnableDiscoveryClient注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package top.zhenganwen.springcloud;

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

@SpringBootApplication
@EnableDiscoveryClient
public class Payment8004Application {

public static void main(String[] args) {
SpringApplication.run(Payment8004Application.class, args);
}
}

zk客户端依赖需和zk服务端版本一致

经过上述步骤后直接启动应用报错如下:

这是因为之前搭建zk服务时,下载的3.4.14版本的安装包,而依赖spring-cloud-starter-zookeeper-discovery引入的zk客户端版本是3.5.3-beta,因此启动报错是因为两者版本不兼容导致的。

我们需要排除依赖传递引入的3.5.3-beta并重新引入和服务端版本一致或稍低版本的依赖

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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

启动成功后,查看zk根路径下的变化:

1
2
3
4
5
6
7
8
[zk: localhost:2181(CONNECTED) 0] ls /                                               [services, zookeeper]                

[zk: localhost:2181(CONNECTED) 4] ls /services [cloud-provider-payment]

[zk: localhost:2181(CONNECTED) 2] ls /services/cloud-provider-payment [f35824e5-4b94-4278-abdf-751b90d6450b]

[zk: localhost:2181(CONNECTED) 3] get /services/cloud-provider-payment/f35824e5-4b94-4278-abdf-751b90d6450b {"name":"cloud-provider-payment","id":"f35824e5-4b94-4278-abdf-751b90d6450b","address":"TH201956132.tuhu.ad","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-provider-payment","metadata":{}},"registrationTimeUTC":1585633359065,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}
cZxid = 0xc ctime = Tue Mar 31 13:42:40 CST 2020 mZxid = 0xc mtime = Tue Mar 31 13:42:40 CST 2020 pZxid = 0xc cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x100004c94a50003 dataLength = 540 numChildren = 0

其中f35824e5-4b94-4278-abdf-751b90d6450b就相当于服务实例ID,该节点存储实例相关信息:

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
{
"name": "cloud-provider-payment",
"id": "f35824e5-4b94-4278-abdf-751b90d6450b",
"address": "TH201956132.tuhu.ad",
"port": 8004,
"sslPort": null,
"payload": {
"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
"id": "application-1",
"name": "cloud-provider-payment",
"metadata": { }
},
"registrationTimeUTC": 1585633359065,
"serviceType": "DYNAMIC",
"uriSpec": {
"parts": [
{
"value": "scheme",
"variable": true
},
{
"value": "://",
"variable": false
},
{
"value": "address",
"variable": true
},
{
"value": ":",
"variable": false
},
{
"value": "port",
"variable": true
}
]
}
}

订单服务入驻zk并调用支付服务

引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
<!--        <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.8</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

写yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 80
#eureka:
# client:
# service-url:
# defaultZone: http://eureka01.com:7001/eureka,http://eureka02.com:7002/eureka
# instance:
# prefer-ip-address: true
spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: 127.0.0.1:2181 # zk集群使用逗号分隔

启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
//@EnableEurekaClient
@EnableDiscoveryClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}

/**
* @LoadBalanced 通过restTemplate使用服务别名来调用接口时需添加此注解,若服务别名映射多个实例,默认采用轮询算法
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

业务类

1
2
3
4
5
6
7
public static final String PAYMENT_DOMAIN = "http://cloud-provider-payment";


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

访问http://localhost/order/payment/1

1
2
3
4
5
{
code: 200,
message: "ok-1",
data: null
}

注意,这里服务别名就不能使用大写的CLOUD_PROVIDER_PAYMENT了,因为zk是大小写区分的

Consul

简介

是什么

  • 是一套开源的分布式发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。
  • 提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用, 也可以一起使用构建全方位的服务网格,总之 Consul 提供了一套完整的服务网格解决方案
  • 它具有很多优点, 包括: 基于 raft 协议,比较简洁; 支持健康检查,同时支持 HTTP 和DNS 协议支持跨数据中心的WAN 集群 提供图形化界面,跨平台,支持 Linux 、Max 、Windows。

能干什么

  • 服务发现
  • 健康检测
  • 服务网格
  • KV存储
  • 多数据中心
  • 可视化web管理

下载安装

官网:https://www.consul.io/downloads.html

网盘:https://pan.baidu.com/s/1nMXRms3_ZPhgqawrcEcsdA 提取码:247m

解压后有一个consul.exe,在此路径下使用cmd命令行启动consul服务

1
consul agent -dev

稍等一会,打印日志如下则启动成功

可访问web管理界面:http://localhost:8500

服务注册发现

以cloud_provider_payment8004、cloud_consumer_order_80为例

引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
<!--        <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>org.apache.zookeeper</groupId>-->
<!-- <artifactId>zookeeper</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.apache.zookeeper</groupId>-->
<!-- <artifactId>zookeeper</artifactId>-->
<!-- <version>3.4.8</version>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>log4j</groupId>-->
<!-- <artifactId>log4j</artifactId>-->
<!-- </exclusion>-->
<!-- <exclusion>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-log4j12</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

写yml

cloud_provider_payment8004

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8004
spring:
application:
name: cloud-provider-payment
cloud:
# zookeeper:
# connect-string: 127.0.0.1:2181 # zk集群使用逗号分隔
consul:
host: 127.0.0.1 # consul服务地址
port: 8500 # consul服务端口号
discovery:
hostname: localhost # 以什么主机名注册到consul
service-name: ${spring.application.name} # 以什么服务别名注册到consul

cloud_consumer_order_80

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 80
#eureka:
# client:
# service-url:
# defaultZone: http://eureka01.com:7001/eureka,http://eureka02.com:7002/eureka
# instance:
# prefer-ip-address: true
spring:
application:
name: cloud-consumer-order
cloud:
# zookeeper:
# connect-string: 127.0.0.1:2181 # zk集群使用逗号分隔
consul:
host: 127.0.0.1 # consul服务地址
port: 8500 # consul服务端口号
discovery:
hostname: localhost # 以什么主机名注册到consul
service-name: ${spring.application.name} # 以什么服务别名注册到consul

启动类

使用zk、consul作为服务注册发现时需添加@EnableDiscoveryClient注解

业务类

不变,order仍旧通过RestTemplate使用服务别名进行RPC调用

验证

启动上述两个应用,访问http://localhost:8500

点击其中一个服务可查看该服务下的实例列表和对应的主机名端口号

访问http://localhost/order/payment/2,RPC能够调通

1
2
3
4
5
{
code: 200,
message: "ok-2",
data: null
}

小结

eureka、zk、consul异同

相同:

  • 健康监测
  • SpringCloud集成

不同:

  • eureka、zk使用java开发,consul使用go开发
  • 客户端通信协议:eureka-http、zk-tcp、consul-http/dns
  • CAP:eureka-AP(重高可用)、zk/consul-CP(重一致性)
    • eureka自我保护机制避免了网络不同等情况下的误剔除,但也会导致“僵尸”实例(实际上已经shutdown了)无法调通
    • zk采用临时节点记录注册上来的服务实例、consul实时剔除健康监测不达标的服务实例,都是在心跳检测不通持续一段时间后实时剔除
  • 对于服务注册发现,eureka推荐使用@EnableEurekaClient注解,zk、consul推荐使用@EnableDiscoveryClient注解

CAP理论

CAP

  • Consistency,强一致性
  • Avalibility,高可用性
  • Partition Tolerance,分区容错性

分布式架构下,为了避免单点故障,通常采用分区放置节点进行集群,因此P通常是必须的。而剩下的CA通常无法同时满足,只能二选其一。

客户端负载均衡Ribbon

概述

是什么

Spring Cloud Ribbon 是 Netflix Ribbon 实现的一套客户端 负载均衡工具

简单的说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供 客户端的负载均衡算法和服务调用。 Ribbon 客户端组件提供一系列完善的配置项如超时、重试等。简单的说,就是配置文件中列出 load Balancer (简称 LB)后面所有的机器,Ribbon 会自动的帮助你基于某种规则(如简单轮询,随机链接等)去链接这些机器。我们很容易使用 Ribbon 自定义的负载均衡算法。

现状

从其github官网上可以看到其项目状态:保持维护(言外之意不再更新)

Project Status: On Maintenance

目前看来,存在SpringCloud在未来使用自研的spring cloud loadbalancer来取代springcloud ribbon的趋势,但现在国内ribbon仍是主流

Spring Cloud Netflix Ribbon is now deprecated. To see a demo of the currently recommended client-side-load-balancing approach, please check this guide.

——https://spring.io/guides/gs/client-side-load-balancing/

能干嘛

集中式LB&进程内LB

集中式:即在服务的消费方和提供方之间使用独立的LB 设施(可以是硬件,如F5, 也可以是软件如 Nginx ), 由该设置负责把访问请求通过某种策略转发至服务的提供方。

进程内:将 LB 逻辑集成到消费方,消费方从服务注册中心获取有哪些地址可用,然后自己再从这些地址中选择一个适合的服务器。Ribbon 就属于进程内 LB,它只是一个类库,集成于消费方进程,消费方通过它阿莱获取服务提供方的地址。

配置LB算法

Ribbon默认使用轮询策略作为LB算法,但他也提供了如下其他算法的实现,你很容易就能通过配置进行切换:

  • com.netflix.loadbalancer.RoundRobinRule:轮询
  • com.netflix.loadbalancer.RandomRule:随机
  • com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会重试(继续轮询下一个),直到获取可用的服务
  • WeightedResponseTimeRule:对RoundRobinRule 的拓展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule:会先过滤掉由于多次访问故障而处断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule:根据Server 所在区域的性能和 Server 的可用性选择服务器

HTTP调用

SpringCloud Ribbon集成了RestTemplate作为HTTP调用工具,满足大部分人使用RestTemplate的习惯,只需添加一个@LoadBalanced注解,Ribbon就会在我们通过RestTemplate使用服务别名进行HTTP调用时帮助我们完成在服务别名-服务实例这个一对多场景下选取其中一个进行调用的过程。

小结

Ribbon 其实就是一个软负载均衡的客户端组件,他可以和其他需要发出请求的客户端结合使用,和eureka client结合只是其中的一个实例。

SpringCloud Ribbon 在工作时分为两步:

  1. 先选择 EurekaServer, 从EurekaServer集群中优先选择与自身在同一个区域内(如同一网络、同一地域)的负载较少的
  2. 根据用户配置的LB策略,在服务注册表中选择一个地址进行实际调用

使用

启用LB

以本文使用的SpringCloud版本为例

可以发现,前文使用的eureka、consul、zk服务注册发现依赖都集成了ribbon,因此@LoadBalanced注解能够生效的原因一直都是ribbon在暗处发力。

再次强调:只要使用RestTemplate+服务别名进行调用,就涉及到服务别名解析,需要处理服务别名-服务实例一对多的映射(即使服务只有一个实例在跑,这种映射关系依然存在),就一定要加@LoadBalanced注解

切换LB策略

以现有的eureka01、eureka02、order80、payment8001、payment8001为例,将order80服务注册切换成eureka。全部启动后访问http://localhost/order/payment/1,其中响应的message依次在8001、8002中来回切换,这也是ribbon默认启用的轮询策略,接下来就看一下如何换成ribbon内置提供的随机策略(其它策略的切换以此类推)。

下面我们希望在cloud_consumer_order_80中对于cloud-provider-payment支付服务的调用LB策略采用随机策略:

  1. 首先我们需要添加一个bean配置类,该类返回一个IRule实例,指定需要使用的LB策略(注意不要添加@Configuration注解,因为那样如果被启动类扫描到会将此策略应用到所有ribbon调用中,而本例中我们只期望在使用ribbon调用cloud-provider-payment服务时采用随机算法,调用其他服务时依然使用默认的轮询算法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package top.zhenganwen.springcloud.config;

    import org.springframework.context.annotation.Bean;

    import com.netflix.loadbalancer.IRule;
    import com.netflix.loadbalancer.RandomRule;

    public class MyRibbonRule {
    @Bean
    public IRule iRule() {
    return new RandomRule();
    }
    }
  2. 在启动类上使用@RibbonClient注解,name属性指定服务别名,configuration属性指定步骤1定义的配置类。如下相当于说对cloud-provider-payment服务的ribbon调用LB策略切换成随机策略

    1
    2
    3
    4
    @SpringBootApplication
    @EnableEurekaClient
    @RibbonClient(name = "cloud-provider-payment", configuration = MyRibbonRule.class)
    public class OrderApplication {

验证

多次刷新http://localhost/order/payment/1,观察是否是随机调用payment8001、8002两应用的接口

轮询算法分析

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
private AtomicInteger nextServerCyclicCounter; // RPC调用次数计数器

public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}

private int incrementAndGetModulo(int modulo) {
// 自旋锁更新计数器
for (;;) {
int current = nextServerCyclicCounter.get();
// (过去调用次数+1) % 传入的服务实例总数 = 本次要调用的实例的集合下标
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}

public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}

Server server = null;
int count = 0;
while (server == null && count++ < 10) {
// 获取该服务下可达(能ping通)的服务实例
List<Server> reachableServers = lb.getReachableServers();
// 获取该服务下所有服务实例,包括不可达的
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();

if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}

// (接口调用次数+1) % 服务实例总数 = 本次调用选取的实例下标
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);

if (server == null) {
/* Transient. */
Thread.yield();
continue;
}

// 判断选取的实例是否可用
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}

// Next.
server = null;
// 重试10次,直到轮询选取的实例可用
}

// 重试10次选取的实例都不可用
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}

自定义LB算法

禁用ribbon

1
2
3
4
5
@Bean
// @LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
1
2
3
4
5
@SpringBootApplication
@EnableEurekaClient
//@EnableDiscoveryClient
//@RibbonClient(name = "cloud-provider-payment", configuration = MyRibbonRule.class)
public class OrderApplication {

实现服务别名选取实例算法

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
package top.zhenganwen.springcloud.rule;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class MyRoundRule {

private AtomicInteger counter = new AtomicInteger(0);

@Autowired
private DiscoveryClient discoveryClient;

/**
* 通过服务别名选取调用实例-轮询算法
* @param serviceName 服务别名
* @return
*/
public ServiceInstance choose(String serviceName) {
final List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
if (CollectionUtils.isEmpty(instances)) {
throw new RuntimeException("没有在册的服务实例,服务别名:" + serviceName);
}
for (; ; ) {
final int current = counter.get();
int next = current + 1;
int nextIndex = next % instances.size();
if (counter.compareAndSet(current, next)) {
return instances.get(nextIndex);
}
}
}

public String mapServiceUrl(String serviceName) {
final ServiceInstance instance = choose(serviceName);
return "http://" + instance.getHost() + ":" + instance.getPort();
}
}

业务类

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
package top.zhenganwen.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import top.zhenganwen.springcloud.entity.CommonResult;
import top.zhenganwen.springcloud.entity.Payment;
import top.zhenganwen.springcloud.rule.MyRoundRule;

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

public static final String PAYMENT_SERVICE_NAME = "cloud-provider-payment";

@Autowired
private MyRoundRule myRoundRule;

@Autowired
private RestTemplate restTemplate;

@GetMapping("/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable Long id) {
final String serviceUrl = myRoundRule.mapServiceUrl(PAYMENT_SERVICE_NAME);
return restTemplate.getForObject(serviceUrl + "/payment/" + id, CommonResult.class);
}
}

本地模拟远程服务接口OpenFeign

概述

前文中所有HTTP远程调用都是使用的RestTemplate,这样有以下几个弊端:

  • 各种RestTemplate遍布各地、零星散乱,无法对所有远程HTTP调用进行集中管控(如超时控制、日志记录)
  • 对于同一远程HTTP接口,项目可能有多处用到,应该抽取出以代码复用,否则远程HTTP接口一旦变更,就需要项目相关调用出同时修改
  • 直接面向http client细节编程,不符合Spring面向接口编程的设计理念

未解决上述困惑,Feign应运而生。Feign的中文意思是虚假,即你只需要在本地借助SpringMVC注解声明与远程HTTP接口对应的接口方法,即可享受通过调用本地方法一样调用远程接口的效果,它的引入把RPC调用封装得就好像是本地调用一样。

Feign是对Ribbon+RestTemplate的进一步封装,帮助我们完成模板式代码的编写,而使我们只需关注远程HTTP接口规范和己方HTTP调用声明,实际调用过程(如构建请求体、发送请求、解析响应结果)交给Feign去实现。

feign已被openfeign取代,并已被废弃,但对于我们开发者来说改变是透明的,下文的feign均指现用的openfeign

使用

以cloud_consumer_order_80为例

引pom

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

启动类

添加@EnableFeignClients,扫描@FeignClient组件并为其创建实现类(底层采用Ribbon+RestTemplate完成实际的RPC调用)

1
2
3
4
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderApplication {

RPC接口声明

编写远程调用接口,就好像编写本地方法一样,使用SpringMVC注解完成对远程HTTP接口的声明式定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package top.zhenganwen.springcloud.remote;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import top.zhenganwen.springcloud.entity.CommonResult;
import top.zhenganwen.springcloud.entity.Payment;

/**
* 以接口声明的方式定义远程HTTP接口,一个接口类代表一个远程服务
* @FeignClient标明这是一个feign组件,需要被@EnableFeignClients识别,value属性填服务别名
* 接口方法声明需和远程接口HTTP规范保持一致,包括但不局限于如下几点:
* - 请求方式
* - 去掉域(schema+host+port)之后的uri相对路径
* - queryParams查询参数名称:需使用@RequestParam("xxx")指定,即使参数名和方法参数名一致
* - 路径参数:需使用@PathVariable("xxx")指定路径参数名,即使路径参数名和方法参数名一致
* - 请求体:需使用@RequestBody指定
* - 返回值类型
*/
@FeignClient("cloud-provider-payment")
public interface PaymentRemote {
@GetMapping("/payment/{id}")
CommonResult<Payment> getById(@PathVariable("id") Long id);
}

业务类

top.zhenganwen.springcloud.controller.OrderController

1
2
3
4
5
6
7
@Autowired
private PaymentRemote paymentRemote;

@GetMapping("/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable Long id) {
return paymentRemote.getById(id);
}

测试

启动7001、80、8001、8002,测试用例如下

超时设置

使用Feign调用远程接口时默认超时时间为1秒,即如果调用远程HTTP接口时的等待时间超过了1s就会不再等待其响应而自身抛出一个异常。

场景复现

修改服务提供方的查询接口,故意延长接口响应时间:

cloud_provider_payment、cloud_provider_payment_8002

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {

@Autowired
private PaymentDAO paymentDAO;

@GetMapping("/{id}")
public CommonResult<Payment> getById(@PathVariable Long id) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new CommonResult<>(200, "ok-8001", paymentDAO.getById(id));
}
}

再次访问http://localhost/order/payment/1,抛出等待远程接口执行超时的异常

个性化配置

如果你觉得默认的1s等待时间太短了,想要给RPC调用更长一些的缓冲,可以通过配置项进行个性化设置。由于这个等待是否超时是和实际发起HTTP调用的动作挂钩的,而Feign底层是依赖Ribbon做这件事的,因此配置项使用的是Ribbon定义的配置项:

1
2
3
ribbon:
ReadTimeout: 5000 # 等待远程接口响应的最大时间
ConnectTimeout: 2000 # 与远程主机建立tcp连接最大时间

注意上述配置项IDEA没有代码提示

日志打印

Feign内置了远程调用日志打印功能,诸如传参数值、请求/响应头信息、响应结果内容之类的,我们只需一个配置项说明哪个远程服务的调用需要打日志以及用配置bean指定日志需要输出哪些内容:

cloud_consumer_order_80

1
2
3
4
logging:
level:
# feign日志以什么级别监控哪个接口
top.zhenganwen.springcloud.remote.PaymentRemote: debug
1
2
3
4
5
6
7
8
9
10
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

@Bean
public Logger.Level level() {
return Logger.Level.FULL; // 打印格式:最全信息格式
}

访问http://localhost/order/payment/1,日志打印效果如下

1
2
3
4
5
6
7
8
9
10
11
2020-04-02 09:47:01.363 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote     : [PaymentRemote#getById] ---> GET http://cloud-provider-payment/payment/1 HTTP/1.1
2020-04-02 09:47:01.363 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] ---> END HTTP (0-byte body)
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] <--- HTTP/1.1 200 (3005ms)
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] connection: keep-alive
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] content-type: application/json
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] date: Thu, 02 Apr 2020 01:47:04 GMT
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] keep-alive: timeout=60
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] transfer-encoding: chunked
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById]
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] {"code":200,"message":"ok-8002","data":{"id":1,"serial":"aaa"}}
2020-04-02 09:47:04.369 DEBUG 5192 --- [p-nio-80-exec-1] t.z.springcloud.remote.PaymentRemote : [PaymentRemote#getById] <--- END HTTP (63-byte body)
鼓励一下~