# spring-cloud-microservice
**Repository Path**: wzk9261/spring-cloud-microservice
## Basic Information
- **Project Name**: spring-cloud-microservice
- **Description**: Spring Cloud 微服务
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2019-08-06
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Spring Cloud 微服务
## Eureka 服务治理
本项目的项目结构为 Maven 多模块,各模块层级结构及配置如下:
- **spring-cloud-microservice** (root)
- **eureka-server**
- **eureka-server1** (8081)
`@EnableEurekaServer` + `@SpringBootApplication`
```yaml
spring:
application:
name: eureka-server
server:
port: 8081
eureka:
instance:
hostname: peer1
client:
service-url:
defaultZone: http://peer2:8082/eureka/
```
- **eureka-server2** (8082)
`@EnableEurekaServer` + `@SpringBootApplication`
```yaml
spring:
application:
name: eureka-server
server:
port: 8082
eureka:
instance:
hostname: peer2
client:
service-url:
defaultZone: http://peer1:8081/eureka/
```
因为 Eureka Server 的主机名必须不同才可互相注册,所以在本地需要模拟两个不同的主机名。在 `C:\Windows\System32\drivers\etc` 中配置 peer1 和 peer2 到 localhost 的映射即可:
```properties
127.0.0.1 peer1
127.0.0.1 peer2
```
- **eureka-provider**
- **eureka-provider1** (depends on MyBatis, 8083)
`@EnableEurekaClient` + `@SpringBootApplication`
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: eureka-provider
mybatis:
mapper-locations: classpath:mapper/*xml
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
```
- **eureka-provider2** (depends on MyBatis, 8084)
`server.port=8084`,其他同 eureka-provider1
- **eureka-provider3** (depends on JPA, 8085)
`server.port=8085`,其他同 eureka-provider1
- **mybatis-crud**
spring datasource 配置、mapper 配置
- **jpa-crud**
spring datasource 配置
- **model** (MyBatis POJO)
无需配置 application.yml
- **feign-ribbon-consumer** (RestTemplate, 8086)
`@SpringCloudApplication`
```yaml
spring:
application:
name: feign-ribbon-consumer
server:
port: 8086
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
feign:
hystrix:
enabled: true
```
高可用 Eureka 架构图
各模块说明:
- Server - 服务注册中心。由两台 Eureka Server 组成集群,提供注册服务。
- Provider - 服务提供者。包括 MyBatis 和 JPA 提供的 CRUD 服务。
- Consumer - 服务消费者。使用 RestTemplate 来调用注册到 Server 上的 Provider 服务。
## Ribbon 负载均衡
Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具。Ribbon 虽然只是一个工具类框架,不像服务注册中心、配置中心、API 网关那样需要独立部署,但它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用、API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的,包括后面要介绍的 Feign,也是基于 Ribbon 实现。
通过 Ribbon 的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需以下两步:
- 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
- 服务消费者直接通过调用被 `@LoadBalanced` 注解修饰的 `RestTemplate` 来实现面向服务的接口调用。
在 feign-ribbon-consumer 模块中配置 `RestTemplate`:
```java
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
return restTemplate;
}
}
```
在 consumer 中创建 `UserController`,并注入 `RestTemplate`,调用 eureka-provider 的接口。
```java
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{uid}")
public String getUserById(@PathVariable Integer uid) {
return restTemplate.getForObject(USERS_URL + uid, String.class);
}
}
```
访问 Consumer 的 `/users/{uid}` 接口,查看各个项目相应位置的日志输出。
- `MyBatisUserController`
```java
@GetMapping("{uid}")
public UserDTO findUserById(@PathVariable Integer uid) {
log.info(getClass().toString());
return userService.findUserById(uid);
}
```
- `JpaUserController`
```java
@GetMapping("{uid}")
public UserEntity findUserById(@PathVariable Integer uid) {
log.info(getClass().toString());
return userService.findUserById(uid);
}
```
在 Postman 中访问 `localhost:8086/users/1` 15次,在 IntelliJ IDEA 的 Services 面板中查看日志的输出行数。输出情况如下:
- EurekaProvider1 输出以下语句 5 次:
```java
2019-10-16 17:37:13.488 INFO 20552 --- [nio-8083-exec-4] c.j.e.p.m.c.MyBatisUserController : class UserController1
```
- EurekaProvider2 输出以下语句 5 次:
```java
2019-10-16 17:37:14.866 INFO 19272 --- [nio-8084-exec-6] c.j.e.p.m.c.MyBatisUserController : class UserController2
```
- EurekaProvider3 输出以下语句 5 次:
```java
2019-10-16 17:37:12.476 INFO 8232 --- [nio-8085-exec-8] c.j.e.p.j.controller.JpaUserController : class UserController3
```
可以看出访问 3 个 Provider 被访问的次数是相同的,所以可以证明 Eureka 消费者是采用轮询的方式调用服务。
## Hystrix 服务容错保护
在微服务架构中,存在着众多服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致系统的瘫痪。这样的架构相较传统架构更加不稳定。为了解决这一问题,产生了断路器等一系列服务保护机制。
对此,只需要为需要做熔断保护的方法加上 `@HystrixCommand` 即可,并为其设置 `fallbackMethod` 属性,注意让 fallback 方法和被熔断保护方法的入参保持一致。
首先在 feign-ribbon-consumer 模块中引入 Hystrix 依赖:
```xml
org.springframework.cloud
spring-cloud-starter-hystrix
1.4.7.RELEASE
```
配置 fallbackMethod:
```java
@GetMapping("{uid}")
@HystrixCommand(fallbackMethod = "userFallback")
public String getUserById(@PathVariable Integer uid) {
return restTemplate.getForObject(USERS_URL + uid, String.class);
}
```
```java
public String userFallback(Integer value) {
return "您来到了没有用户的荒原,输入参数为:" + value;
}
```
一旦 `RestTemplate` 调用的服务不可用,就会跳转到 fallback 方法,由 fallback 方法代替被熔断保护的方法 `getUserById` 返回值。
## Feign 声明式服务调用
Feign 整合了 Ribbon 和 Hystrix,提供这两者的强大功能,另外还提供一种声明式的 Web 服务客户端定义方式。由于 `RestTemplate` 的封装,几乎每一个对服务的调用都是简单的模板化内容。Feign 在此基础上做了进一步封装,由它来进行依赖服务接口的定义,我们只需创建一个接口并使用注解 `@FeignClient` 配置它。
在 feign-ribbon-consumer 模块中引入 Feign 相关依赖:
```xml
org.springframework.cloud
spring-cloud-starter-feign
1.4.7.RELEASE
```
在启动项上加入 `@SpringCloudApplication` `@EnableFeignClients` 注解:
```java
@SpringCloudApplication
@EnableFeignClients
public class EurekaConsumer {
public static void main(String[] args) {
SpringApplication.run(EurekaConsumer.class, args);
}
}
```
在服务层声明使用 @FeignClient 注解的接口:
```java
@FeignClient(name = "eureka-provider", fallback = UserFeignServiceFallback.class)
public interface UserFeignService {
@GetMapping("users")
String getUsers();
@GetMapping("users/{uid}")
String getUserById(@PathVariable Integer uid);
}
```
进行服务降级 fallback 配置:
```java
@Component
public class UserFeignServiceFallback implements UserFeignService {
@Override
public String getUsers() {
return "Feign 获取不到所有用户";
}
@Override
public String getUserById(Integer uid) {
return "Feign 获取不到 ID 为" + uid + "的用户";
}
}
```
写一个 Controller 暴露接口以便访问 `UserFeignService`
```java
@RestController
@RequestMapping("feign/users")
public class UserFeignController {
@Autowired
private UserFeignService userFeignService;
@GetMapping
public String getUsers() {
return userFeignService.getUsers();
}
@GetMapping("{uid}")
public String getUserById(@PathVariable Integer uid) {
return userFeignService.getUserById(uid);
}
}
```
需要在 application 中做如下配置:
```properties
feign.hystrix.enabled=true
```
这一配置用于开启 Hystrix 熔断器,这样 Feign 的 fallback 配置才会生效。
启动 eureka-server、feign-ribbon-consumer,不启动 eureka-provider,使用 Postman 访问 feign-ribbon-consumer 的 `UserFeignController` 对应的 `localhost:8086/feign/users` 和 `localhost:8086/feign/users/1` 接口,返回体为预期的 fallback 方法中的返回值。
## Zuul API 网关服务
[为什么要使用 Spring Cloud Zuul?](https://blog.csdn.net/weixin_38362455/article/details/86810171)
1. Zuul 和 Ribbon 以及 Eureka 相结合,可以实现智能路由和负载均衡的功能,可以将流量按照某种策略分发到集群中的多个实例。
2. 统一了对外暴露接口,外界系统不需要知道微服务系统中各服务之间调用的复杂性,也保护了内部微服务的 API 接口。
3. 可以统一做用户身份认证,权限验证,这样就不用在每个微服务中进行认证了。
4. 可以统一实现监控、日志的输出。
5. 客户端请求多个微服务时,可以只请求 Zuul 一次,在 Zuul 中请求多个微服务,减少客户端和微服务的交互次数。
引入 Zuul 相关依赖:
```xml
org.springframework.cloud
spring-cloud-starter-zuul
1.4.7.RELEASE
```
在启动项上加入 `@SpringCloudApplication` `@EnableZuulProxy` 注解:
```java
@SpringCloudApplication
@EnableZuulProxy
public class EurekaGateway {
public static void main(String[] args) {
SpringApplication.run(EurekaGateway.class, args);
}
}
```
在 application.yml 中做如下配置:
```yaml
spring:
application:
name: zuul-gateway
server:
port: 8087
zuul:
routes:
consumer:
path: /consumer/**
serviceId: feign-ribbon-consumer
provider:
path: /provider/**
serviceId: eureka-provider
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
```
使用 Postman 访问符合网关路由定义的 URL:
`localhost:8087/consumer/users/1`,`localhost:8087/provider/users/1`
均可以得到想要的 User 对象数据。
另外,`serviceId` 可以改为 `url`:
```yaml
zuul:
routes:
consumer:
path: /consumer/**
url: http://localhost:8086/
provider:
path: /provider/**
serviceId: eureka-provider
```
其中,`url: http://localhost:8086/` 与 `serviceId: feign-ribbon-consumer` 等价,都是被路由转发到 feign-ribbon-consumer 的 API.
## Config 分布式配置中心
Spring Cloud Config 用于为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,分为服务端和客户端。服务端为分布式配置中心,是一个独立的微服务应用;客户端为分布式系统中的基础设置或微服务应用,通过指定配置中心来管理相关的配置。Spring Cloud Config 构建的配置中心,除了适用于 Spring 构建的应用外,也可以在任何其他语言构建的应用中使用。Spring Cloud Config 默认采用 Git 存储配置信息,天然支持对配置信息的版本管理。
传统模式的高可用不需要额外的配置,只需将所有的 config-server 实例全部指向同一个 Git 仓库,客户端指定 config-server 时指向上层负载均衡设备地址。
服务模式通过将 config-server 纳入 Eureka 服务治理体系,将 config-server 注册成为一个微服务应用,客户端通过服务名从服务注册中心获取配置中心的实例信息。
### 构建配置中心
在 config-server 中加入依赖:
```xml
org.springframework.cloud
spring-cloud-config-server
2.1.4.RELEASE
```
在启动类上使用 `@EnableConfigServer` 开启 Spring Cloud Config 的服务端功能;使用 `@EnableDiscoveryClient` 开启服务发现功能,以便于当前服务注册到 eureka-server 上。
```java
@EnableDiscoveryClient
@EnableConfigServer
@SpringBootApplication
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
```
新建 config-repo 文件夹,在其中新建相应的 properties 文件,并在其中写入相应内容:
- config-repo
- microservice.properties (from=git-default-1.0)
- microservice-dev.properties (from=git-dev-1.0)
- microservice-test.properties (from=git-test-1.0)
- microservice-prod.properties (from=git-prod-1.0)
在 application.yml 中配置 `spring.cloud.server.git`相关属性:
```yaml
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/wzk9261/spring-cloud-microservice.git
search-paths: config-repo
username: jakeweng
password: 123456
server:
port: 8088
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
```
其中,`search-paths` 要配置存放 properties 文件的 config-repo 文件夹。
启动 config-server,使用 Postman 按照如下规则访问 Spring Cloud Config 的配置内容:
- / {application} / {profile} [ / {label} ]
- / {application} - {profile} . yml
- / {label} / {application} - {profile} . yml
- / {application} - {profile} . properties
- / {label} / {application} - {profile} . properties
可得到如下结果:
- 访问 `localhost:8088/microservice/prod/master` 或 `localhost:8088/microservice/prod`,返回:
```json
{
"name": "microservice",
"profiles": [
"prod"
],
"label": "master",
"version": "2f64ef08d5985f3826daacca72c39b5771384e6a",
"state": null,
"propertySources": [
{
"name": "https://gitee.com/wzk9261/spring-cloud-microservice.git/config-repo/microservice-prod.properties",
"source": {
"from": "git-prod-1.0"
}
},
{
"name": "https://gitee.com/wzk9261/spring-cloud-microservice.git/config-repo/microservice.properties",
"source": {
"from": "git-default-1.0"
}
}
]
}
```
- 访问 `localhost:8088/microservice-prod.yml` 或 `localhost:8088/microservice-prod.properties` 或 `localhost:8088/master/microservice-prod.yml` 或 `localhost:8088/master/microservice-prod.properties`,返回:
```json
from: git-prod-1.0
```
需要注意的是,如果在访问规则中没有写明 label,那么默认获取 master 分支的配置信息。
基于 master 分支创建一个新分支 config-label,并将 config-repo 文件夹下的 properties 文件中的内容从 1.0 修改为 2.0:
- config-repo
- microservice.properties (from=git-default-2.0)
- microservice-dev.properties (from=git-dev-2.0)
- microservice-test.properties (from=git-test-2.0)
- microservice-prod.properties (from=git-prod-2.0)
- 访问 `localhost:8088/microservice/prod/config-label`,返回:
```json
{
"name": "microservice",
"profiles": [
"prod"
],
"label": "config-label",
"version": "903492e9fc9f77e84c765880809565402e278938",
"state": null,
"propertySources": [
{
"name": "https://gitee.com/wzk9261/spring-cloud-microservice.git/config-repo/microservice-prod.properties",
"source": {
"from": "git-prod-2.0"
}
},
{
"name": "https://gitee.com/wzk9261/spring-cloud-microservice.git/config-repo/microservice.properties",
"source": {
"from": "git-default-2.0"
}
}
]
}
```
- 访问 `localhost:8088/config-label/microservice-prod.yml` 或 `localhost:8088/config-label/microservice-prod.properties`,返回:
```json
from: git-prod-2.0
```
### 客户端获取配置
在 config-client 中添加依赖:
```xml
org.springframework.boot
spring-boot-actuator
```
在 config-client 的 resources 文件夹下创建 bootstrap.yml,并增加注册中心 eureka-server 和配置中心 config-server 相关配置。
```yaml
spring:
application:
name: microservice
cloud:
config:
profile: dev
label: master
uri: http://localhost:8088/
discovery:
service-id: config-server
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
management:
endpoints:
web:
exposure:
include: "*"
server:
port: 8089
```
其中,`spring.application.name` 需要设置为 microservice,与 config-repo 下的 properties 文件前缀同名;利用 `spring.cloud.config.discovery.service-id` 参数来指定配置中心注册的服务名 config-server. 另外,使用 `management.endpoints.web.exposure.include="*"` 启用所有的监控端点,如 `health`、`info`、`metrics` 等,后面才能访问 `/actuator/refresh` 来进行配置刷新,否则会报 404 错误。
启动类如下:
```java
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigClient {
public static void main(String[] args) {
SpringApplication.run(ConfigClient.class, args);
}
}
```
#### 动态刷新配置
有时需要对配置内容进行实时更新,Spring Cloud Config 通过 actuator 可实现此功能。
在 config-client 添加依赖:
```xml
org.springframework.boot
spring-boot-actuator
```
添加刷新范围注解 `@RefreshScope` 至获取配置的 Controller 类,如果需要动态刷新配置,在对应的类上加上该注解即可。
```java
@RefreshScope
@RestController
public class ConfigController {
@Value("${from}")
private String from;
@GetMapping("from")
public String getFrom() {
return from;
}
}
```
启动 eureka-server 和 config-server、config-client,使用 Postman 访问 `localhost:8089/from`,可得到期望的返回值:
```json
git-dev-1.0
```
由于 config 中的 `spring.cloud.config.profile` 值为 dev,所以修改 microservice-dev.properties 中的 from 值为 `git-dev-3.0`,提交并推送至远端仓库。
然后用 POST 请求访问 `localhost:8089/actuator/refresh`,如果返回以下数据说明刷新成功:
```json
[
"config.client.version",
"from"
]
```
再次访问 `localhost:8089/from` ,返回修改后的 from 值:
```
git-dev-3.0
```
#### Webhook
每次手动刷新客户端也很麻烦,有没有什么办法只要提交代码就自动调用客户端来更新呢,Github 的 Webhook 是一个好的办法。
[Webhook](https://developer.github.com/webhooks/),也就是人们常说的钩子,是一个很有用的工具。你可以通过定制 Webhook 来监测你在 Github.com 上的各种事件,最常见的莫过于 **push** 事件。如果你设置了一个监测 push 事件的 Webhook,那么每当你的这个项目有了任何提交,这个 Webhook 都会被触发,这时 Github 就会发送一个 HTTP POST 请求到你配置好的地址。
如此一来,你就可以通过这种方式去自动完成一些重复性工作;比如,你可以用 Webhook 来自动触发一些持续集成(CI)工具的运作,比如 Travis CI;又或者是通过 Webhook 去部署你的线上服务器。
## Bus 消息总线
如果需要客户端获取到最新的配置信息需要执行 `refresh`,我们可以利用 Webhook 的机制每次提交代码发送请求来刷新客户端,当客户端越来越多的时候,需要每个客户端都执行一遍,这种方案就不太适合了。使用 Spring Cloud Bus 可以完美解决这一问题。
在微服务架构的系统中,我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都能连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线。在总线上的各个实例都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息,例如配置信息的变更或者其他一些管理操作等。
由于消息总线在微服务架构系统的广泛使用,所以它同配置中心一样,几乎是微服务架构中的必备组件。Spring Cloud 作为微服务架构综合性的解决方案,对此自然也有自己的实现,这就是 Spring Cloud Bus。通过 Spring Cloud Bus,可以非常容易的搭建起消息总线,同时实现了一些消息总线中的常用功能,比如配合 Spring Cloud Config 实现微服务应用配置信息的动态更新等。
消息总线的作用是管理和传播所有分布式项目中的消息,其实本质是利用了 MQ 的广播机制在分布式的系统中传播消息,目前常用的有 Kafka 和 RabbitMQ。利用 Bus 的机制可以做很多的事情,其中配置中心客户端刷新就是典型的应用场景之一,我们用一张图来描述bus在配置中心使用的机制。

根据此图我们可以看出利用Spring Cloud Bus做配置更新的步骤:
1. 提交代码触发远端仓库向其中一个客户端 A 发送 POST 请求 bus-refresh
2. 客户端 A 接收到请求从Server端更新配置并且发送给 Spring Cloud Bus
3. Spring Cloud Bus 接到消息并通知给其它客户端
4. 其他客户端接收到通知,请求Server端获取最新配置
5. 全部客户端均获取到最新的配置
先安装 RabbitMQ,具体可参考:[RabbitMQ 集群搭建与运维](https://blog.csdn.net/qq_15329947/article/details/84101852)
利用 Maven 将 config-client 改造为 config-client1 和 config-client2 两个 Spring Boot 应用。
- config-client (parent)
- config-client1 (8089)
- config-client2 (8090)
在 config-client 中加入依赖:
```xml
org.springframework.cloud
spring-cloud-starter-bus-amqp
2.1.3.RELEASE
```
依次启动 eureka-server、config-server 和 config-client1、config-client2,前往 RabbitMQ 的后台管理页面`localhost:15672`,在会发现 Exchanges 中新增了一个 springCloudBus,Queues 中增加了一条临时队列 `springCloudBus.anonymous.Ow49xn_8SB-OyZ5P8XWOcQ`,Connections 中多了两个 config-client 到 RabbitMQ 的 `rabbitConnectionFactory`。然后通过以下步骤检验基于 RabbitMQ 的 Spring Cloud Bus 是否生效:
- 先访问两个 config-client 的 /from 请求,会返回当前 config-repo/microservice-dev.properties 中的 from 属性值。
- 接着,修改 config-repo/microservice-dev.properties 中的 from 属性值,并发现 POST 请求到其中一个 config-client 的 /actuator/bus-refresh。
比如,在 Postman 中发送 POST 请求:`localhost:8089/actuator/bus-refresh`,正常情况下在 config-client1 的控制台中会打印出:`Received remote refresh request. Keys refreshed []`
- 最后,再分别访问两个 config-client 的 /from 请求,此时这两个请求都会返回最新的 config-repo/microservice-dev.properties 中的 from 属性值。
此时,我们已经确认可以通过 Spring Cloud Bus 来实时更新总线上的属性配置了。
## Stream 消息驱动的微服务
Spring Cloud Stream 是一个用于构建消息驱动的微服务应用程序的框架,这些应用程序由一个常见的消息传递代理(如 RabbitMQ、Apache Kafka 等)连接。
Spring Cloud Stream 构建在现有 Spring 框架(如 Spring Messaging 和 Spring Integration)之上。尽管这些框架经过了实战测试,工作得非常好,但其实现仍然与其使用的 message broker 紧密耦合。此外,有时对特定用例进行扩展是很难的。
Spring Cloud Stream 背后的想法是一个非常典型的 Spring Boot 概念。抽象地讲,让 Spring 根据配置和依赖关系管理在运行时找出实现自动注入。这意味着您可以通过更改依赖项和配置文件来更改 message broker。可以在 [Spring 官网](https://spring.io/projects/spring-cloud-stream#binder-implementations)找到目前已经支持的各种消息代理。
本章将使用 RabbitMQ 作为 message broker。在此之前,让我们了解一下 broker(代理)的一些基本概念,以及为什么要在面向微服务的体系架构中需要它。
### 微服务中的消息
在微服务体系架构中,存在许多相互通信以完成请求的小型应用程序。它们的主要优点之一是改进了的可伸缩性。一个请求从多个下游微服务传递到完成是很常见的。例如,假设我们有一个 Service-A 内部调用 Service-B 和 Service-C 来完成一个请求:

当然,微服务架构中还会有其他组件,比如 [Spring Cloud Eureka](https://stackabuse.com/spring-cloud-service-discovery-with-eureka/)、[Spring Cloud Zuul](https://stackabuse.com/spring-cloud-routing-with-zuul-and-gateway/) 等等,但我们还是专注关心这类架构的特有问题。
假设由于某种原因 Service-B 需要更多的时间来响应。也许它正在执行 I/O 操作或长时间的 DB 事务,或者进一步调用其它导致 Service-B 变得更慢的服务,这些都使其无法更具效率。
现在,我们可以启动更多的 Service-B 实例来解决这个问题,这样很好,但是 Service-A 实际上是响应很快的,它需要等待 Service-B 的响应来进一步处理。这将导致 Service-A 无法接收更多的请求,这意味着我们还必须启动 Service-A 的多个实例。
另一种解决方法是使用事件驱动的微服务体系架构。这基本上意味着 Service-A 不直接通过 HTTP 调用 Service-B 或 Service-C,而是将请求或事件发布给 message broker 。 Service-B 和 Service-C将成为 message broker(消息代理)上此事件的订阅者。

与依赖 HTTP 用的传统微服务体系架构相比,这有许多优点:
- 提高可伸缩性和可靠性——现在我们知道哪些服务是整个应用程序中的真正瓶颈。
- 鼓励松散耦合——Service-A 不需要了解 Service-B 和 Service-C。它只需要连接到 message broker 并发布事件。事件如何进一步编排取决于代理设置。通过这种方式,Service-A 可以独立地运行,这是微服务的核心概念之一。
- 与遗留系统交互——通常我们不能将所有东西都移动到一个新的技术堆栈中。我们仍然必须使用遗留系统,虽然速度很慢,但是很可靠。
将 stream-rabbitmq 改造为父子模块:
- stream-rabbitmq (parent)
- stream-producer
- stream-consumer
在 stream-rabbitmq 中引入依赖:
```xml
org.springframework.cloud
spring-cloud-starter-stream-rabbit
2.2.1.RELEASE
```
### 生产者
将消息从发布者传递到队列的整个过程是通过通道完成的。因此,让我们创建一个 `HelloBinding` 接口,其中包含我们的消息机制 `greetingChannel`:
```java
public interface HelloBinding {
@Output("greetingChannel")
MessageChannel greeting();
}
```
因为这将发布消息,所以我们使用 `@Output` 注解。方法名可以是我们想要的任意名称,当然,我们可以在一个接口中有多个 `Channel`(通道)。
现在,让我们创建一个 `REST`,它将消息推送到这个 `Channel`(通道)。
```java
@RestController
public class ProducerController {
private MessageChannel greet;
public ProducerController(HelloBinding binding) {
greet = binding.greeting();
}
@GetMapping("/greet/{name}")
public void publish(@PathVariable String name) {
String greeting = "Hello, " + name + "!";
Message msg = MessageBuilder.withPayload(greeting)
.build();
this.greet.send(msg);
}
}
```
上面,我们创建了一个 `ProducerController` 类,它有一个 `MessageChannel` 类型的属性 `greet`。这是通过我们前面声明的方法在构造函数中初始化的。
然后,我们有一个简单的`REST`接口,它接收 `PathVariable` 的 `name`,并使用 `MessageBuilder` 创建一个 `String` 类型的消息。最后,我们使用 `MessageChannel` 上的 `.send()` 方法来发布消息。
现在,我们将在的主类中添加 `@EnableBinding` 注解,传入 `HelloBinding` 告诉 `Spring` 加载。
```java
@EnableBinding(HelloBinding.class)
@SpringBootApplication
public class StreamProducer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
最后,我们将 `greetingChannel` 连接到一可用的消费者,并将应用注册到 eureka-server。这两个都是在 `application.yml` 中定义的:
```yaml
spring:
cloud:
stream:
bindings:
greetingChannels:
destinations: greetings
application:
name: stream-producer
server:
port: 8091
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
```
### 消费者
现在,我们需要监听之前创建的通道 `greetingChannel`。让我们为它创建一个绑定:
```java
public interface HelloBinding {
String GREETING = "greetingChannel";
@Input(GREETING)
SubscribableChannel greeting();
}
```
与生产者绑定的两个非常明显区别。因为我们正在消费消息,所以我们使用 `SubscribableChannel` 和 `@Input` 注解连接到 `greetingChannel`,消息数据将被推送这里。
现在,让我们创建处理数据的方法:
```java
@EnableBinding(HelloBinding.class)
@Slf4j
public class HelloListener {
@StreamListener(target = HelloBinding.GREETING)
public void processHelloChannelGreeting(String msg) {
log.info("Listen RabbitMQ: {}", msg);
}
}
```
在这里,我们创建了一个 `HelloListener` 类,在 `processHelloChannelGreeting` 方法上添加 `@StreamListener` 注解。这个方法需要一个字符串作为参数,我们刚刚在控制台打印了这个参数。我们还在类添加 `@EnableBinding` 启用了 `HelloBinding`。
同样,我们在这里使用 `@EnableBinding`,而不是主类,以便告诉我们如何使用。
看看我们的主类,我们没有任何修改:
```java
@SpringBootApplication
public class StreamConsumer {
public static void main(String[] args) {
SpringApplication.run(StreamConsumer.class, args);
}
}
```
最后,我们将 `greetingChannel` 连接到一可用的消费者,并将应用注册到 eureka-server。这两个都是在 `application.yml` 中定义的:
```yaml
spring:
cloud:
stream:
bindings:
greetingChannels:
destinations: greetings
application:
name: stream-consumer
server:
port: 8092
eureka:
client:
service-url:
defaultZone: http://peer1:8081/eureka/, http://peer2:8082/eureka/
```
### 发送消息
首先启动 eureka-server1 和 eureka-server2,在启动 stream-producer 和 stream-consumer。在 `localhost:15672` 中可以查看到 Exchanges 中多了一条 `greetingChannel`。
并使用 Postman 发送 GET 请求至 stream-producer 的接口`localhost:8091/greet/Jake`,接着,在 stream-consumer 的日志中可以查看到打印日志:
```json
Listen RabbitMQ: Hello, Jake!
```
## Sleuth 分布式服务跟踪
随着业务发展,系统拆分导致系统调用链路愈发复杂一个前端请求可能最终需要调用很多次后端服务才能完成,当整个请求变慢或不可用时,我们是无法得知该请求是由某个或某些后端服务引起的,这时就需要解决如何快读定位服务故障点,以对症下药。于是就有了分布式系统调用跟踪的诞生。
现今业界分布式服务跟踪的理论基础主要来自于 Google 的一篇论文[《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》](https://research.google.com/pubs/pub36356.html),使用最为广泛的开源实现是 Twitter 的 Zipkin,为了实现平台无关、厂商无关的分布式服务跟踪,CNCF 发布了布式服务跟踪标准 Open Tracing。国内,淘宝的“鹰眼”、京东的“Hydra”、大众点评的“CAT”、新浪的“Watchman”、唯品会的“Microscope”、窝窝网的“Tracing”都是这样的系统。
一般的,一个分布式服务跟踪系统,主要有三部分:数据收集、数据存储和数据展示。根据系统大小不同,每一部分的结构又有一定变化。譬如,对于大规模分布式系统,数据存储可分为实时数据和全量数据两部分,实时数据用于故障排查(troubleshooting),全量数据用于系统优化;数据收集除了支持平台无关和开发语言无关系统的数据收集,还包括异步数据收集(需要跟踪队列中的消息,保证调用的连贯性),以及确保更小的侵入性;数据展示又涉及到数据挖掘和分析。虽然每一部分都可能变得很复杂,但基本原理都类似。