1. 环境介绍
- IDEA 2019.3.4
- SpringBoot 2.0.1.RELEASE
- Spring Cloud Finchley.M9
2. 微服务快速入门
2.1 应用架构对比
2.1.1 单体应用
单体应用架构

单体应用架构的优缺点
单体模式的优势
- 易于开发
- 易于测试
- 易于部署
单体模式的不足
- 应用工程变得又大又复杂
- 敏捷开发和部署举步维艰
- 启动时间长
- 可靠性差
- 难以采用新技术新语言
2.1.2 微服务应用
微服务分布式架构

微服务架构的特点
- 由多个独立运行的微小服务构成
- 通过轻量级的机制通信
- 独立构建部署
- 每个服务可以使用不同语言
- 每个服务可以使用不同数据库
微服务架构的优缺点
微服务架构的优点
- 单一职责(业务单一)
- 轻量级通信(HTTP REST JSON)
- 独立性( 独立开发,测试,部署)
- 敏捷性(代码运行速度快,需求更新应变快)
微服务架构的缺点
- 过度关注微服务大小
- 分布式系统中难以构建和部署
- 分区的数据库架构
- 测试的复杂度
- 改动带来的沟通成本
2.1.3 微服务架构流行技术(Java)


2.2 Spring Boot快速入门
2.2.1 什么是SpringBoot
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通俗点说,spring boot不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架.
2.2.2 SpringBoot的好处
其实就是简单、快速、方便!平时如果我们需要搭建一个spring web项目的时候需要怎么做呢?
1)配置web.xml,加载spring和spring mvc
2)配置数据库连接、配置spring事务
3)配置加载配置文件的读取,开启注解
4)配置日志文件…
配置完成之后部署tomcat 调试
但是如果使用spring boot呢?
很简单,我仅仅只需要非常少的几个配置就可以迅速方便的搭建起来一套web项目或者是构建一个微服务!
2.2.3 SpringBoot快速搭建用户微服务
使用SpringBoot快速搭建一个用户微服务microservice_user
建立Maven项目
导入SpringBoot依赖
编写application.yml 或 application.properties
server: port: 9101 spring: application: name: microservice-user # 服务名称, 暂时没有用, springcloud微服务调用才会用到
- 编写启动类
cn.sm1234.user.UserApplication@SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class); } }
- 编写UserController
cn.sm1234.user.controller.UserController// @RestController是@Controller和@ResponseBody两个注解的结合体 @RestController @RequestMapping("/user") public class UserController { private Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 查询所有用户 * @return */ @RequestMapping(value = "/findAll", method = RequestMethod.GET) public List<User> findAll() { List<User> users = new ArrayList<>(); users.add(new User(1, "张三", "123456", "男", 1000.0)); users.add(new User(2, "李四", "123456", "女", 2000.0)); users.add(new User(3, "王五", "123456", "男", 3000.0)); users.add(new User(4, "陈六", "123456", "女", 4000.0)); return users; } }采用Maven多模块化方式搭建项目!(中大型项目推荐)
因为后续还会新建多个微服务应用, 所以上面步骤中首先会创建一个父工程sm1234_parent, 将公共依赖提取到父工程中进行管理, 父工程pom.xml依赖内容:
<?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>cn.sm1234</groupId>
<artifactId>sm1234_parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<!--微服务模块-->
<modules>
<module>microservice_user</module>
<module>microservice_movie</module>
<module>eureka_server</module>
<module>hystrix_monitor</module>
<module>microservice_gateway</module>
<module>microservice_config</module>
<module>zipkin_server</module>
</modules>
<!--springboot项目必须导入的parent依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<!--定义常用的属性-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--springboot 对web的支持, springmvc相关功能, json转换等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- 定义SpringCloud版本 -->
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<!-- 锁定SpringCloud版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2.2.4 用户微服务简单测试
启动用户微服务, 访问http://localhost:9101/user/findAll, 返回json结构的数据.(我的测试数据改过, 所以会和上面代码中模拟数据不一样)

另外, 推荐两款测试工具, postman 和 插件RestfulToolKit.
postman我就不多说了, 测试必备工具. 我推荐大家在IDEA中安装RestfulToolKit插件, 它可以自动扫描出controller中的api接口, 选中api能够自动生成请求参数结构并自动填充模拟数据, 自己修改数据即可发送http请求. 如下图所示:

2.3 快速在Docker搭建MySQL
在Linux系统上安装docker, 通过docker镜像实现快速启动mysql容器. 如果没有Linux系统的小伙伴可以使用windows系统的Mysql, 不影响后面的学习. 但还是建议大家通过虚拟机方式搭建Linux系统, 开启自己的Docker服务之旅 , 这里推荐大家看我的虚拟机搭建的博客 CentOs7系统安装及使用问题总结.
2.3.1 docker简介
Docker 是一个开源项目,诞生于 2013 年初,最初是 dotCloud 公司内部的一个业余项目。它基于 Google 公司推出的 Go 语言实现。项目后来加入了 Linux 基金会,遵从了 Apache 2.0 协议,项目代码在GitHub 上进行维护。
作为一种新兴的虚拟化方式,Docker 跟传统的虚拟化方式相比具有众多的优势。
Docker 容器的启动可以在秒级实现,这相比传统的虚拟机方式要快得多。Docker 对系统资源的利用率很高,一台主机上可以同时运行数千个 Docker 容器。
Docker 可以很好地和微服务结合起来。从概念上来说,一个微服务便是一个提供一整套应用程序的部分功能,Docker 便可以在开发、测试和部署过程中一直充当微服务的容器。甚至生产环境也可以在 Docker 中部署微服务。
2.3.2 快速搭建Docker环境
这个步骤需要大家已经搭建好了Linux系统环境, docker安装和mysql镜像下载.
## 安装docker
yum install docker
## 启动docker
systemctl start docker
## 停止docker
systemctl stop docker
## 设置开机自启动
systemctl enable docker
## 搜索mysql镜像
docker search mysql
## 下载mysql镜像
docker pull mysql:5.7
## 查看下载的mysql镜像
docker images mysql
在Docker下运行MySQL服务
## 运行第一个mysql01容器, 将本机端口3306映射到mysql01容器的3306端口
docker run --name mysql01 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
## 运行第二个mysql02容器, 将本机端口3307映射到mysql01容器的3306端口
docker run --name mysql02 -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
## 查看运行的容器
docker ps -a
## 停止mysql01容器
docker stop mysql01
## 启动mysql01容器
docker start mysql01
## 删除mysql01容器
docker rm mysql01
## 进入mysql01容器
docker exec -it mysql01 /bin/bash
连接mysql数据库
可以先使用docker exec -it mysql01 /bin/bash进入本机的mysql容器, 测试mysql数据库连接.

然后, 测试客户端连接Mysql数据库. 我使用的是SqlYog工具, 也可以使用其他mysql客户端工具连接.

如果遇到客户端无法连接成功, 可能是防火墙的原因. 下面是操作***Linux***系统防火墙的相关命令. 可以关闭防火墙.
查看防火墙状态
# CentOs7
systemctl status firewalld
# 其他Linux系统
service iptables status
暂时关闭防火墙
systemctl stop firewalld
service iptables stop
永久关闭防火墙
systemctl disable firewalld
chkconfig iptables off
重启防火墙
systemctl enable firewalld
service iptables restart
chkconfig iptables on
2.4 整合Spring Data JPA
2.4.1 设计用户表
| 字段英文名称 | 字段中文名称 | 数据类型/长度 | 约束 |
|---|---|---|---|
| id | 用户编号 | int(11) | PRIMARY |
| money | 薪资 | double | |
| password | 密码 | varchar(255) | |
| sex | 性别 | varchar(15) | |
| username | 用户名 | varchar(255) |
DDL语句
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`money` double DEFAULT NULL,
`password` varchar(15) DEFAULT NULL,
`sex` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=innodb AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
2.4.2 导入spring data jpa依赖
在用户微服务microservice_user中导入mysql和spring data jpa依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
2.4.3 配置数据源
在用户微服务microservice_user的配置文件application.yml中添加数据源配置:
spring:
datasource:
url: jdbc:mysql://192.168.65.129:3306/springcloud?characterEncoding=UTF8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
jpa:
show-sql: true # 打印sql语句
generate-ddl: true # 自动建表
database: mysql # jpa连接的数据类型
2.4.4 Pojo实体映射
创建User实体类
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
// 上面是lombok注解
// 下面是spring data jpa注解
@Entity
@Table(name = "tb_user")
public class User {
/**
* 用户id
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 性别
*/
private String sex;
/**
* 薪资
*/
private Double money;
// getter, setter 使用了Lombok注解自动生成
}
提示: 因为在父工程中引入了lombok依赖, 所以pojo可以使用lombok语法糖简化getter/setter/toString等方法.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<scope>provided</scope>
</dependency>
2.4.5 Dao层
创建UserDao类, 因为使用spring data jpa数据模板, 所以需要继承JpaRepository.
@Repository
public interface UserDao extends JpaRepository<User, Integer> {
}
2.4.6 Service层
创建UserService接口, 我偷懒了, 直接写类了.
@Service
public class UserService {
@Autowired
private UserDao userDao;
/**
* 查询所有用户
* @return
*/
public List<User> findAll() {
return userDao.findAll();
}
/**
* 根据id查询用户
* @param id
* @return
*/
public User findById(Integer id) {
return userDao.findById(id).get();
}
/**
* 添加用户
*/
public void add(User user) {
userDao.save(user);
}
/**
* 修改用户
* @param user
*/
public void update(User user) {
userDao.save(user);
}
/**
* 删除用户
* @param id
*/
public void delete(Integer id) {
userDao.deleteById(id);
}
}
2.4.7 Controller层
完善cn.sm1234.user.controller.UserController类
@RequestMapping("/user")
@RestController // @RestController 是 @Controller 和 @ResponseBody 两个注解的结合体
public class UserController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserService userService;
/**
* 查询所有用户
* @return
*/
@RequestMapping(value = "/findAll", method = RequestMethod.GET)
public List<User> findAll() {
return userService.findAll();
}
/**
* 根据id查询用户
* @param id
* @return
*/
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User findById(@PathVariable Integer id) {
logger.info("用户微服务11111111");
return userService.findById(id);
}
/**
* 添加用户
* @param user
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
public void add(@RequestBody User user) {
userService.add(user);
}
/**
* 修改用户
* @param user
* @param id
*/
@RequestMapping(value = "/update", method = RequestMethod.PUT)
public void update(@RequestBody User user, Integer id) {
user.setId(id);
userService.update(user);
}
/**
* 删除用户
* @param id
*/
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public void delete(@PathVariable Integer id) {
userService.delete(id);
}
}
2.4.8 功能测试
省略…
2.5 SpringBoot搭建电影微服务
2.5.1 创建电影微服务应用
在父工程sm1234_parent下面新增microservice_movie模块, 过程和用户微服务类似,只是不需要连接数据库.
2.5.2 编写配置
创建application.yml文件, 添加如下配置信息:
server:
port: 9002
spring:
application:
name: microservice-movie
2.5.3 编写Controller
cn.sm1234.movie.controller.MovieController
@RequestMapping("/movie")
@RestController
public class MovieController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 购票方法
@GetMapping(value = "/order")
public String order() {
// 模拟当前用户
Integer id = 2;
logger.info("正在购票.....");
return "购票成功";
}
}
2.6 RPC与Http远程调用方法对比
2.6.1 远程调用方式
常见的远程调用方式有以下几种:
- RPC:Remote Produce Call远程过程调用。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,就是RPC的典型
- HTTP:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。
现在热门的Rest风格,就可以通过http协议来实现。
2.6.2 深入认识RPC
RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。说得通俗一点就是:A计算机提供一个服务,B计算机可以像调用本地服务那样调用A计算机的服务。

2.6.3 深入认识HTTP
Http协议:超文本传输协议,是一种应用层协议。规定了网络传输的请求格式、响应格式、资源定位和操作的方式等。但是底层采用什么网络传输协议,并没有规定,不过现在都是采用TCP协议作为底层传输协议。

2.6.4 Http与RPC的差异
Http与RPC的远程调用非常像,都是按照某种规定好的数据格式进行网络通信,有请求,有响应。在这点来看,两者非常相似,但是还是有一些细微差别。
- RPC并没有规定数据传输格式,这个格式可以任意指定,不同的RPC协议,数据格式不一定相同(格式随意)。而Http是有比较规范格式要求(***格式规范***)。
- RPC需要满足像调用本地服务一样调用远程服务,也就是对调用过程在API层面进行封装(***限制了开发语言***)。Http协议没有这样的要求,因此请求、响应等细节需要我们自己去实现(***和开发语言无关***)。
2.6.5 远程调用方式选择
既然两种方式都可以实现远程调用,该如何选择呢?
速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。
难度来看,RPC实现较为复杂,http相对比较简单。
灵活性来看,http更胜一筹,因为它不关心实现细节,跨平台、跨语言。
因此,两者都有不同的使用场景:
如果对效率要求更高,并且开发过程使用统一的技术栈,那么用RPC还是不错的。
如果需要更加灵活,跨语言、跨平台,显然http更合适。
微服务架构,更加强调的是独立、自治、灵活。而RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。
2.7 RestTemplate实现远程调用
2.7.1 电影微服务调用用户微服务
电影微服务里面有购票方法,需要远程调用用户微服务的方法(根据id查询用户信息).
在电影微服务启动类中cn.sm1234.movie.MovieApplication初始化RestTemplate
@SpringBootApplication
public class MovieApplication {
public static void main(String[] args) {
SpringApplication.run(MovieApplication.class);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
在电影微服务的购票方法中,使用RestTemplate远程调用用户微服务方法
@RequestMapping("/movie")
@RestController
public class MovieController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
// 购票方法, 没有使用Eureka, 没有负载均衡
@GetMapping(value = "/order")
public String order() {
// 模拟当前用户
Integer id = 2;
// 使用RestTemplate远程调用用户微服务, 获取用户具体信息
User user = restTemplate.getForObject("http://localhost:9101/user/" + id,
User.class);
logger.info(user.getUsername() + "正在购票.....");
return user.getUsername() + "购票成功";
}
}
2.7.2 RestTemplate远程调用的问题
以上电影微服务调用用户微服务的案例中,存在以下问题:
- 在电影微服务中,我们把
url地址硬编码到了代码中,不方便后期维护 - 电影微服务需要记忆用户微服务的地址,如果出现变更,可能得不到通知,地址将失效
- 电影微服务不清楚用户微服务的状态,服务宕机也不知道
- 用户微服务只有1台服务,不具备高可用性
- 即便用户微服务形成集群,电影微服务还需自己实现负载均衡
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
- 如何自动注册和发现服务?
- 如何实现状态监管?
- 服务如何实现负载均衡?
- 服务如何解决容灾问题?等等其他问题…
3. Spring Cloud服务注册
3.1 SpringCloud简介
3.1.1 什么是SpringCloud
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
3.1.2 SpringCloud与SprinBoot关系
Spring Boot专注简化单个微服务开发,而Spring Cloud专注多个微服务开发。
Spring Boot不需要依赖Spring Cloud,而Spring Cloud必须依赖Spring Boot。
3.1.3 SpringCloud主要框架
Spring Cloud Netflix
Netflix Eureka
Netflix OpenFeign
Netflix Hystrix
Netflix Zuul
…
Spring Cloud Config
Spring Cloud Bus
Spring Cloud Sleuth
…
3.1.4 SpringCloud版本

3.2 服务注册组件Eureka简介
Eureka 是 Netflix 出品的用于实现服务注册和发现的工具。 SpringCloud 集成了 Eureka,并提供了开箱即用的支持。其中, Eureka 又可细分为 Eureka Server 和 Eureka Client。
在之前的案例中,用户微服务对外提供服务,需要对外暴露自己的地址。而电影微服务(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦。
Eureka负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
总结: Eureka实现了服务的自动注册、发现、状态监控。
3.3 搭建Eureka Server
在父工程sm1234_parent下面创建一个新模块eureka_server
在父工程导入SpringCloud的依赖, 锁定SpringCloud的版本, 我使用的版本是Finchley.M9
在子工程eureka_server导入Eureka Server的依赖
<dependencies>
<!-- Eureka服务端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
编写application.yml配置Eureka Server
server:
port: 8888
spring:
application:
name: eureka-server
# 单机版配置
eureka:
client:
fetch-registry: false # 是否需要从Eureka获取注册信息
register-with-eureka: false # 是否需要把该服务注册到Eureka
service-url: # 暴露Eureka注册地址
defaultZone: http://127.0.0.1:${server.port}/eureka
server:
# 修改扫描失效服务间隔时间为5s(默认是60s,单位毫秒)
evication-interval-timer-in-ms: 5000
# 取消自我保护机制(默认值true)
enable-self-preservation: true
编写启动类(添加@EnableEurekaServer注解)
cn.sm1234.eureka.EurekaApplication
@SpringBootApplication
@EnableEurekaServer // 开启Eureka服务端自动配置
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class);
}
}
3.4 搭建Eureka Client
电影微服务(服务调用者)和用户微服务(服务提供者)都作为Eureka Client注册到Eureka Server.
在电影微服务(服务调用者)和用户微服务(服务提供者)中导入Eureka Client的依赖
<!--导入Eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在电影微服务(服务调用者)和用户微服务(服务提供者)添加连接Eureka Server的配置:
eureka:
client:
register-with-eureka: true # 是否需要把该服务注册到Eureka
fetch-registry: true # 是否需要从Eureka获取注册信息
service-url: # 客户端注册地址
defaultZone: http://127.0.0.1:8888/eureka
instance:
# 优先使用该服务的IP地址注册到Eureka, 在生产环境建议改为true
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5 # 修改续约间隔时间, 默认30s
lease-expiration-duration-in-seconds: 10 # 修改服务失效时间,默认90s
在启动类添加@EnableEurekaClient注解
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
public class MovieApplication {...}
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
public class UserApplication {...}
依次启动eureka_server, microservice_user, microservice_movie, 访问 http://localhost:8888/ , 进入Eureka Server控制台, 查看服务注册列表, 电影微服务和用户微服务已经注册到了Eureka服务中心.

使用Eureka方式改造电影微服务的购票方法
cn.sm1234.movie.controller.MovieController#order2
@RequestMapping("/movie")
@RestController
public class MovieController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
/**
* 购票方法, 使用Eureka, 没有负载均衡
* 需要注释掉{@link MovieApplication#restTemplate()}的负载均衡@LoadBalanced
* @return
*/
@GetMapping(value = "/order2")
public String order2() {
// 模拟当前用户
Integer id = 2;
// 使用Eureka远程调用用户微服务, 获取用户具体信息
// 获取所有同名的微服务(服务发现)
// 参数, 需要发现的微服务名称
List<ServiceInstance> instances = discoveryClient.getInstances(
"microservice-user");
if (CollectionUtils.isEmpty(instances)) {
return "购票失败, 没有用户微服务可以使用";
}
// 没有使用负载均衡,只能获取第一个服务(默认算法:轮询)
ServiceInstance instance = instances.get(0);
User user = restTemplate.getForObject("http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id, User.class);
logger.info(user.getUsername() + "正在购票.....");
return user.getUsername() + "购票成功";
}
}
重新启动电影微服务, 访问 http://localhost:9002/movie/order2, 测试成功. 服务调用方可以通过Eureka访问到服务提供方.
3.5 高可用(集群)Eureka Server
上面的单机版Eureka服务如果宕机了, 就无法提供服务注册和发现了, 所以需要考虑集群多实例的方式部署.
因为eureka_server已经导入了Eureka Server的依赖, 而Eureka Server依赖中已包含了Eureka Client的依赖, 所以不用重复导入.
<!-- Eureka服务端(内含Eureka Client) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

集群版配置Eureka实例1, 将8889端口注册到8888的服务中心.
server:
port: 8889
spring:
application:
name: eureka-server
# 集群版配置
eureka:
client:
fetch-registry: true # 是否需要从Eureka获取注册信息
register-with-eureka: true # 是否需要把该服务注册到Eureka
service-url: # 暴露Eureka注册地址
defaultZone: http://127.0.0.1:8888/eureka
instance:
# 优先使用该服务的IP地址注册到Eureka, 在生产环境建议改为true
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5 # 修改续约间隔时间, 默认30s
lease-expiration-duration-in-seconds: 10 # 修改服务失效时间,默认90s
server:
# 修改扫描失效服务间隔时间为5s (默认是60s,单位毫秒)
eviction-interval-timer-in-ms: 5000
# 取消自我保护机制 (默认值true)
enable-self-preservation: false
集群版配置Eureka实例2, 将8888端口注册到8889的服务中心.
server:
port: 8888
spring:
application:
name: eureka-server
# 集群版配置
eureka:
client:
fetch-registry: true # 是否需要从Eureka获取注册信息
register-with-eureka: true # 是否需要把该服务注册到Eureka
service-url: # 暴露Eureka注册地址
defaultZone: http://127.0.0.1:8889/eureka
instance:
# 优先使用该服务的IP地址注册到Eureka, 在生产环境建议改为true
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5 # 修改续约间隔时间, 默认30s
lease-expiration-duration-in-seconds: 10 # 修改服务失效时间,默认90s
server:
# 修改扫描失效服务间隔时间为5s (默认是60s,单位毫秒)
eviction-interval-timer-in-ms: 5000
# 取消自我保护机制 (默认值true)
enable-self-preservation: false
电影微服务(服务调用者)和用户微服务(服务提供者)修改配置为注册到8888, 8889两台Eureka_Server中.
eureka:
client:
register-with-eureka: true # 是否需要把该服务注册到Eureka
fetch-registry: true # 是否需要从Eureka获取注册信息
registry-fetch-interval-seconds: 5 # 每隔5s重新获取并更新服务注册信息
service-url: # 客户端注册地址
defaultZone: http://127.0.0.1:8888/eureka,http://127.0.0.1:8889/eureka
instance:
prefer-ip-address: true # 优先使用该服务的IP地址注册到Eureka, 在生产环境建议改为true
lease-renewal-interval-in-seconds: 5 # 修改续约间隔时间, 默认30s
lease-expiration-duration-in-seconds: 10 # 修改服务失效时间,默认90s
依次启动eureka_server, microservice_user, microservice_movie, 访问 http://localhost:8888/ , http://localhost:8889/进入Eureka Server控制台, 查看服务注册列表, 电影微服务和用户微服务已经注册到了两台Eureka服务中心.
http://localhost:8888/ 查看结果:
http://localhost:8889/ 查看结果:
测试电影微服务远程调用用户微服务
3.6 服务提供者与服务调用者配置细节
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务调用者需要从EurekaServer发现服务, 并完成服务调用等工作.
3.6.1 服务提供者-服务注册
服务注册:eureka.client.register-with-erueka=true
eureka:
client:
register-with-eureka: true # 是否需要把该服务注册到Eureka
3.6.2 服务提供者-服务续约
服务续约:在注册服务完成以后,服务提供者会定时向EurekaServer发起Rest请求,告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew)心跳,续约参数:
eureka:
instance:
lease-renewal-interval-in-seconds: 5 # 修改续约间隔时间, 默认30s
lease-expiration-duration-in-seconds: 10 # 修改服务失效时间,默认90s
lease-renewal-interval-in-seconds: 服务续约(renew)的间隔, 默认为30s.
lease-expiration-duration-in-seconds: 服务失效时间, 默认为90s.
3.6.3 服务调用者-服务发现
获取服务注册信息:eureka.client.fetch-registry=true
eureka:
client:
fetch-registry: true # 是否需要从Eureka获取注册信息
默认每隔30秒重新获取并更新注册信息,修改参数:
eureka:
client:
registry-fetch-interval-seconds: 5 # 每隔5s重新获取并更新服务注册信息
3.7 Eureka Server失效剔除机制和自我保护机制
3.7.1 失效剔除
默认情况下,Eureka Server每隔60秒对失效的服务(超过90秒未续约的服务)进行剔除。以下参数可以修改剔除时间. eureka.server.eviction-interval-timer-in-ms
eureka:
server:
# 修改扫描失效服务间隔时间为5s (默认是60s,单位毫秒)
eviction-interval-timer-in-ms: 5000
3.7.2 自我保护
有时关停服务时会看到Eureka Server出现一行红色字, 这是Eureka的自我保护机制。
Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了15% , 在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
可以通过下面的配置项关闭自我保护, eureka.server.enable-self-preservation
eureka:
server:
# 修改扫描失效服务间隔时间为5s (默认是60s,单位毫秒)
eviction-interval-timer-in-ms: 5000
# 取消自我保护机制 (默认值true)
enable-self-preservation: false
修改后重启eureka_server, 可以看到一行红字, 表示已经关闭自我保护机制.
关闭用户微服务后, 可以看到服务注册的状态已经关闭.
每隔5s扫描失效服务, 5s后Eureka就从服务注册列表中将其剔除.

4. SpringCloud服务调用与负载均衡
负载均衡的两种方式
(1) RestTemplate(服务调用)+Ribbon(负载均衡)
(2) OpenFeign(服务调用)+ 内置Ribbon(负载均衡)
推荐使用第二种方式,简单易用!
4.1 负载均衡组件Ribbon
实际环境中,我们往往会开启很多个用户微服务的集群(下图: 9001 和 9101 两台提供服务.)。此时我们获取的服务列表中就会有多个,到底电影微服务该访问哪一个呢?Eureka中已经帮我们集成了负载均衡组件:Ribbon
Ribbon是Netflix发布的负载均衡器, 它有助于控制HTTP和TCP客户端的行为. 为Ribbon配置服务提供者地址列表后, Ribbon就可基于某种负载均衡算法, 自动地帮助服务消费者去请求. Ribbon为我们提供了很多的负载均衡算法, 例如轮询, 随机等, 我们也可以为Ribbon实现自定义的负载均衡算法. Ribbon默认使用轮询算法!!!
4.2 Ribbon实现用户微服务负载均衡
修改电影微服务
在RestTemplate实例方法上添加@LoadBalance注解
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
public class MovieApplication {
public static void main(String[] args) {
SpringApplication.run(MovieApplication.class);
}
@Bean
@LoadBalanced // 添加负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
修改购票方法, 使用LoadBalancerClient实现负载均衡.
package cn.sm1234.movie.controller;
import cn.sm1234.movie.MovieApplication;
import cn.sm1234.movie.pojo.User;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.beans.factory.annotation.Autowired;
@RequestMapping("/movie")
@RestController
public class MovieController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient client;
/**
* 购票方法, 使用Ribbon负载均衡组件
* 需要注释掉{@link MovieApplication#restTemplate()}的负载均衡@LoadBalanced
* @return
*/
@GetMapping(value = "/order3")
public String order3() {
// 模拟当前用户
Integer id = 2;
// 使用Ribbon帮助我们选择一个合适的服务实例
ServiceInstance instance = client.choose("microservice-user");
User user = restTemplate.getForObject("http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id, User.class);
logger.info(user.getUsername() + "正在购票.....");
return user.getUsername() + "购票成功";
}
}
修改购票方法 ,当调用的微服务实例名称和服务注册的名称一样时 , 直接使用用户微服务名称即可!
server:
port: 9001
spring:
application:
name: microservice-user # 用户微服务名称
package cn.sm1234.movie.controller;
import cn.sm1234.movie.MovieApplication;
import cn.sm1234.movie.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
@RequestMapping("/movie")
@RestController
public class MovieController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
/**
* 购票方法, 使用Ribbon负载均衡组件-简化版
* 需要在{@link MovieApplication#restTemplate()}添加负载均衡@LoadBalanced
* @return
*/
@GetMapping(value = "/order4")
public String order4() {
// 模拟当前用户
Integer id = 2;
// 当调用的微服务实例名称和注册的名称一样时可以直接使用实例名称拼接url
// Ribbon会帮助我们选择一个合适的服务实例
User user = restTemplate.getForObject("http://microservice-user/user/" + id, User.class);
logger.info(user.getUsername() + "正在购票.....");
return user.getUsername() + "购票成功";
}
}
4.3 Ribbon负载均衡原理及修改规则
4.3.1 Ribbon负载均衡原理(源码)
LoadBalancerInterceptor#intercept
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(
serviceName, requestFactory.createRequest(request, body, execution));
}
RibbonLoadBalancerClient#execute(String, LoadBalancerRequest<T>)
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
RibbonLoadBalancerClient#getServer(ILoadBalancer)
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
BaseLoadBalancer#chooseServer
//...默认轮询算法
private final static IRule DEFAULT_RULE = new RoundRobinRule();
// 可以修改负载均衡算法 IRule有不同的实现类
protected IRule rule = DEFAULT_RULE;
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}",
name, key, e);
return null;
}
}
}
com.netflix.loadbalancer.PredicateBasedRule#choose
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server =
getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
AbstractServerPredicate#chooseRoundRobinAfterFiltering(List<Server>, Object)
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers,
Object loadBalancerKey) {
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
com.netflix.loadbalancer.AbstractServerPredicate#incrementAndGetModulo
// 默认的轮询算法逻辑
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
4.3.2 修改负载均衡算法
Ribbon默认的负载均衡策略是 轮询(RoundRobinRule) , 可以修改.
方式1: 通过配置文件修改电影微服务负载均衡算法为随机( RandomRule )
serviceId:
ribbon:
NFLoadBalancerRuleClassName: xxxxx
microservice-user:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
方式2: 通过在配置类或启动类中覆盖默认的负载均衡策略 IRule
@Bean
public IRule iRule(){
return RandomRule(); // 随机策略
}
4.4 OpenFeign简介
OpenFeign(简称Feign)是远程服务调用组件, 可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
Feign是Netflix开发的声明式, 模板化的HTTP客户端, 其灵感来自Retrofit, JAXRS-2.0及WebSocket. Feign可以帮助我们更加快捷, 优雅地调用HTTP API. 在SpringCloud中, 使用Feign非常简单. 创建一个接口, 并在接口上添加一些注解即可.
4.5 OpenFeign实现远程调用
实践: 电影微服务调用用户微服务
修改服务调用者-电影微服务
(1) 导入OpenFeign的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2) 编写代理接口(底层是动态代理,类似mybatis的mapper接口),代理接口使用@FeignClient指定调用的服务名.
cn.sm1234.movie.client.UserFeignClient
package cn.sm1234.movie.client;
import cn.sm1234.movie.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 类描述: 用户微服务远程接口
* 三个注意事项:
* 1) 使用@FeignClient, 声明需要调用的微服务名
* 2) 检查@RequestMapping注解, value值(路径)是否完整
* 3) @PathVariable注解的value值不能省略
* @Author wang_qz
* @Date 2021/9/25 21:34
* @Version 1.0
*/
@FeignClient(value = "microservice-user")
public interface UserFeignClient {
@GetMapping(value = "/user/{id}")
User findById(@PathVariable(value = "id") Integer id);
}
(3) 使用代理接口调用远程服务方法
package cn.sm1234.movie.controller;
import cn.sm1234.movie.MovieApplication;
import cn.sm1234.movie.client.UserFeignClient;
import cn.sm1234.movie.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
@RequestMapping("/movie")
@RestController
public class MovieController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserFeignClient userFeignClient;
/**
* 购票方法(使用OpenFeign组件, 内置Ribbon负载均衡器)
* @return
*/
@GetMapping(value = "/order5")
public String order5() {
// 模拟当前用户
Integer id = 2;
User user = userFeignClient.findById(id);
logger.info(user.getUsername() + "正在购票.....");
return user.getUsername() + "购票成功";
}
}
(4) 在启动类添加@EnableFeignClients注解
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
@EnableFeignClients // 开启OpenFeign自动配置
public class MovieApplication {
public static void main(String[] args) {
SpringApplication.run(MovieApplication.class);
}
}
4.6 OpenFeign负载均衡
Feign中本身已经集成了Ribbon依赖和自动配置,所以只要使用了Feign,就自动具备负载均衡啦! 也可以跟之前一样通过配置项修改Ribbon的负载均衡算法.
5. Spring Cloud熔断器
5.1 雪崩效应
在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。
5.2 熔断器简介
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败(防止雪崩效应)。
熔断机制的原理
比如家里的电路熔断器, 如果电路发生短路能立刻熔断电路, 避免发生灾难. 在分布式系统中应用这种模式后, 服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时, 能够主动熔断, 防止整个系统被拖垮.
不同于电路熔断的是, Hystrix可以实现弹性容错, 当情况发生好转之后, 可以自动重连. 通过断路的方式, 可以将后续请求直接拒绝掉, 一段时间之后允许部分请求通过, 如果调用成功则回到电路闭合状态, 否则继续断开.
5.3 Ribbon添加Hystrix熔断器
修改电影微服务(调用方添加熔断器)
(1) 导入hystrix的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2) 使用@HystrixCommand声明fallback方法, 进行服务熔断处理.
cn.sm1234.movie.controller.MovieController#order4
/**
* 购票方法, 使用Ribbon负载均衡组件-简化版
* 需要在{@link MovieApplication#restTemplate()}添加负载均衡@LoadBalanced
* @return
*/
@GetMapping(value = "/order4")
@HystrixCommand(fallbackMethod = "fallback")
public String order4() {
// 模拟当前用户
Integer id = 2;
// 当调用的微服务实例名称和注册的名称一样时可以直接使用实例名称拼接url
// Ribbon会帮助我们选择一个合适的服务实例
User user = restTemplate.getForObject("http://microservice-user/user/" + id, User.class);
logger.info(user.getUsername() + "正在购票.....");
return user.getUsername() + "购票成功";
}
(3) 编写fallback方法逻辑
cn.sm1234.movie.controller.MovieController#fallback
/**
* 熔断回滚方法
* @return
*/
private String fallback() {
return "服务暂时不可用, 发生熔断了.";
}
(4) 在启动类添加@EnableHystrix注解
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
@EnableHystrix // 开启Hystrix熔断器的自动配置
public class MovieApplication {
public static void main(String[] args) {
SpringApplication.run(MovieApplication.class);
}
@Bean
@LoadBalanced // 添加负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
注意:
@HystrixCommand(fallbackMethod = "fallback")服务熔断处理方式存在调用逻辑和熔断逻辑高度耦合的缺陷, 而且当调用的api方法很多时, 每个方法都要配置一个服务熔断的fallback方法, 会造成方法臃肿.
如何解决呢? 下面的Feign提供了面向接口编程的解决方案:服务降级
5.4 Feign中开启Hystrix熔断器
Feign默认就有对Hystix的集成.
默认情况下Hystix是关闭的。我们需要通过下面的配置参数来开启:
feign:
hystrix:
enabled: true
启动类上不要忘记了@EnableFeignClients注解.
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
@EnableFeignClients // 开启OpenFeign自动配置
@EnableHystrix // 开启Hystrix熔断器的自动配置
public class MovieApplication {.....}
编写服务降级Fallback处理类 cn.sm1234.movie.client.UserFeignClientImpl
// 熔断器类
@Component
public class UserFeignClientImpl implements UserFeignClient {
@Override
public User findById(Integer id) {
System.out.println("执行了熔断器的类... ");
return null;
}
}
在UserFeignClient中,通过属性fallback指定服务降级的逻辑处理类 cn.sm1234.movie.client.UserFeignClientImpl
// 使用OpenFeign的内置Hystrix,指定熔断器类
@FeignClient(name = "microservice-user", fallback = UserFeignClientImpl.class)
public interface UserFeignClient {
@GetMapping(value = "/user/{id}")
User findById(@PathVariable(value = "id") Integer id);
}
补充:
@FeignClient还提供了属性fallbackFactory配置服务降级的逻辑处理类. 但服务降级类的实现方式稍微有所不同.@Component public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> { @Override public DeptFeignClientService create(Throwable throwable) { // 服务降级的逻辑处理... } }使用
fallbackFactory@FeignClient(name = "microservice-user", fallbackFactory = UserFeignClientFallbackFactory.class) public interface UserFeignClient { // ...... @GetMapping(value = "/user/{id}") User findById(@PathVariable(value = "id") Integer id); }注意: 需要理解
**服务熔断**和**服务降级**的区别.
- 服务熔断 @HystrixCommand的fallback
- 服务降级 @FeignClient的fallback或fallbackFactory
5.5 Hystrix Dashboard监控面板
搭建Hystrix Dashboard工程, 在父工程sm1234_parent下面新增hystrix_monitor模块.
(1) 导入
hystrix dashboard的依赖<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> </dependencies>(2) 启动类添加
@EnableHystrixDashboard注解// Hystrix Dashboard启动类 @SpringBootApplication @EnableHystrixDashboard // 开启Hystrix监控面板 public class HystrixApplication { public static void main(String[] args) { SpringApplication.run(HystrixApplication.class); } }
在服务消费方(电影微服务)的启动类,添加监听方法的Servlet.
// 电影微服务启动类 @SpringBootApplication @EnableEurekaClient // 开启Eureka客户端自动配置 @EnableFeignClients // 开启OpenFeign自动配置 @EnableHystrix // 开启Hystrix熔断器的自动配置 public class MovieApplication { public static void main(String[] args) { SpringApplication.run(MovieApplication.class); } @Bean @LoadBalanced // 添加负载均衡 public RestTemplate restTemplate() { return new RestTemplate(); } // 监听消费方法的Servlet @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } }
添加配置信息
server: port: 9003 spring: application: name: hystrix-monitor
启动hystrix_monitor微服务, 访问 http://localhost:9003/hystrix/
监控消费方(电影微服务)http://localhost:9002/hystrix.stream
进入监控面板, 触发电影微服务的购票方法, 查看监控详情.

监控图中的实心圆有两种含义. 它通过颜色的变化代表了微服务实例的健康程度, 它的健康从绿色<黄色<橙色<红色递减.
实心圆除了颜色的变化之外, 它的大小也会根据实例的请求流量发生变化, 流量越大实心圆就越大. 所以通过实心圆的展示, 就可以在大量的微服务实例中快速发现故障实例和高压实例.
6. Spring Cloud网关
6.1 微服务为什么需要网关
在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。
6.2 Spring Cloud Zuul简介
Spring Cloud Zuul路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。
Zuul是Netflix开源的微服务网关,它可以和Eureka, Ribbon, Hystrix等组件配合使用.
Zuul的核心是一系列的过滤器, 这些过滤器可以完成以下功能:
(1) 身份认证与安全: 识别每个资源的验证要求, 并拒绝那些与要求不符的请求.
(2) 审查与监控: 在边缘位置追踪有意义的数据和统计结果, 从而带来精确的生产视图.
(3) 动态路由: 动态的将请求路由到不同的后端集群.
(4) 压力测试: 逐渐增加指向集群的流量, 以了解性能.
(5) 负载分配: 为每一种负载类型分配对应容量, 并弃用超出限定值的请求.
(6) 静态响应处理: 在边缘位置直接建立部分响应, 从而避免其转发到内部集群.
(7) 多区域弹性: 跨越AWS Region进行请求路由, 旨在实现ELB(Elastic Load Balancing).
SpringCloud对zuul进行了整合和增强, 目前, Zuul使用的默认HTTP客户端是Apache HTTP Client, 也可以使用RestClient或okhttp3.OkHttpClient . 可以通过配置项设置使用哪种HTTP客户端.
RestClient, 可以设置
ribbon.restclient.enabled=true;okhttp3.OkHttpClient, 可以设置
ribbon.okhttp.enabled=true
6.3 搭建网关微服务(基于Zuul实现)
在父工程下面新增独立的网关微服务模块, 创建步骤同前面其他微服务模块一样.
导入zuul和eureka的依赖(网关服务本身也需要注册到Eureka)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
启动类添加@EnableZuulProxy注解
@SpringBootApplication
@EnableZuulProxy // 开启网关代理功能
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class);
}
}
application.yml配置
server:
port: 9004
spring:
application:
name: microservice-gateway
eureka:
client:
register-with-eureka: true # 是否需要从Eureka获取注册信息
fetch-registry: true # 是否需要把该服务注册到Eureka
service-url: # 客户端注册地址
defaultZone: http://127.0.0.1:8888/eureka,http://127.0.0.1:8889/eureka
instance:
prefer-ip-address: true # 优先使用该服务的IP地址注册到Eureka, 在生产环境建议改为true
# zuul的动态路由配置
# 转发的路径path和转发的微服务名称serviceId一致时,可以省略zuul路由配置
zuul:
routes:
microservice-user:
path: /microservice-user # 需要转发的路径
serviceId: microservice-user # 最终转发的微服务(名称)
microservice-movie:
path: /microservice-movie # 需要转发的路径
serviceId: microservice-movie # 最终转发的微服务(名称)
提示: 转发的路径path和转发的微服务名称serviceId一致时, 完全可以省略zuul的动态路由配置
通过网关(Zuul)转发测试:
访问
http://localhost:9004/microservice-user/user/2
访问
http://localhost:9004/microservice-movie/movie/order5
6.4 Zuul动态路由
6.4.1 基本原理
外部请求经过API网关的时候,它是如何被解析并转发到服务具体实例的呢?
在Spring Cloud Netflix中,Zuul巧妙的整合了Eureka来实现面向服务的路由。实际上,我们可以直接将API网关也看做是Eureka服务治理下的一个普通微服务应用。它除了会将自己注册到Eureka服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。所以,在Eureka的帮助下,API网关服务本身就已经维护了系统中所有serviceId与实例地址的映射关系。当有外部请求到达API网关的时候,根据请求的URL路径找到最佳匹配的path规则,API网关就可以知道要将该请求路由到哪个具体的serviceId上去。由于在API网关中已经知道serviceId对应服务实例的地址清单,那么只需要通过Ribbon的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。
6.4.2 负载均衡
通过上面的Zuul动态路由配置,已经具备负载均衡能力,Zuul是通过Ribbon实现负载均衡,所以默认策略是:轮询。
6.5 Zuul过滤器
6.5.1 初识Zuul过滤器
动态路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的Zuul过滤器完成的。其中,路由映射主要通过pre类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;而请求转发的部分则是由route类型的过滤器来完成,对pre类型过滤器获得的路由地址进行转发。所以,Zuul过滤器可以说是Zuul实现API网关功能最为核心的部件。
6.5.2 Zuul过滤器-方法说明
上面Zuul过滤器中方法含义与功能总结如下:
(1)
filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
- pre:可以在请求被路由之前调用。
- route:在路由请求时候被调用。
- post:在routing和error过滤器之后被调用。
- error:处理请求时发生错误时被调用。
(2)
filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先级越高。(3)
shouldFilter:返回一个boolean类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。(4)
run:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。
6.5.3 Zuul过滤器-生命周期

正常流程:
请求到达首先会经过pre过滤器进行请求路径与路由规则的映射, 而后到达routing过滤器进行路由转发, 请求就到达真正的服务提供者, 执行请求, 返回结果, 会到达post过滤器后, 完成响应.
异常流程:
- 整个过程中,
pre或者routing过滤器出现异常, 都会直接进入error过滤器, 在error过滤器处理完毕后, 会将请求交给post过滤器, 最后返回给用户. - 如果是
error过滤器自己出现异常, 最终也会进入post过滤器, 然后返回给用户. - 如果是
post过滤器出现异常, 会跳转到error过滤器, 但是与pre和routing过滤器不同的是, 请求不会再到达post过滤器, 而是直接返回给用户.
6.5.4 编写Zuul过滤器
编写一个Zuul过滤器 cn.sm1234.gateway.filter.MyFilter
// 自定义Zuul过滤器
@Component
public class MyFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public String filterType() {
// 过滤器类型
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 过滤器执行顺序, 数值越大,优先级越低
return 1;
}
@Override
public boolean shouldFilter() {
// 是否让该过滤器生效
return true;
}
@Override
public Object run() throws ZuulException {
// 过滤器逻辑代码
logger.info("执行MyFilter过滤器...");
return null;
}
}
6.5.5 Zuul过滤器-实现统一权限认证
cn.sm1234.gateway.filter.AuthFilter
// 权限控制过滤器
@Component
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
// 模拟异常
// int n = 10 / 0;
//1. 获取当前请求的参数: token=user
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = String.valueOf(request.getParameter("token"));
if (!Objects.equals("user", token)) {
// 不转发到微服务, 给用户响应
ctx.setSendZuulResponse(false);
// 设置响应码401
ctx.getResponse().setStatus(401);
// ctx.setResponseStatusCode(401);
return null;
}
// 继续转发
return null;
}
}
测试权限认证:
访问
http://localhost:9004/microservice-user/user/2
访问
http://localhost:9004/microservice-user/user/2?token=user
6.6 Zuul异常处理
模拟异常, 在权限验证过滤器cn.sm1234.gateway.filter.AuthFilter#run中添加异常.
@Override
public Object run() throws ZuulException {
// 模拟异常
int n = 10 / 0;
// ......
// 继续转发
return null;
}
再次访问 http://localhost:9004/microservice-user/user/2?token=user, 发现Zuul将异常信息转发到了默认的异常页面.
现在的需求, 将异常信息以Json格式输出到界面. 需要我们自定义一个异常处理过滤器, 并将Zuul默认的异常处理过滤器org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter置为失效.
自定义异常过滤器处理器cn.sm1234.gateway.filter.MyErrorFilter
// 自定义异常处理器
@Component
public class MyErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("进入自定义异常处理器");
//1. 捕获异常信息
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
// ZuulException: 封装其他zuul过滤器执行过程中发生的异常信息
ZuulException exception = (ZuulException) ctx.get("throwable");
// 2. 把异常信息以json格式输出给前端
// 2.1 构建错误信息
Result result = Result.builder()
.flag(false)
.message("执行失败:" + exception.getMessage())
.build();
//2.2 转换Result为json字符串
ObjectMapper objectMapper = new ObjectMapper();
try {
String jsonStr = objectMapper.writeValueAsString(result);
// 2.3 把json字符串写回给用户
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonStr);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
封装响应数据的pojo: cn.sm1234.gateway.pojo.Result
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class Result {
private Boolean flag;
private String message;
}
修改配置项,将zuul默认的异常处理器置为失效.
zuul:
# 让zuul预定义的异常过滤器失效
SendErrorFilter:
error:
disable: true
重启网关微服务后, 再次访问 http://localhost:9004/microservice-user/user/2?token=user
6.7 Swagger基本使用
6.7.1 什么是Swagger
Swagger 是一个规范且完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务.
Swagger 的目标是对 REST API 定义一个标准且和语言无关的接口,可以让人和计算机拥有无须访问源码、文档或网络流量监测就可以发现和理解服务的能力。当通过 Swagger 进行正确定义,用户可以理解远程服务并使用最少实现逻辑与远程服务进行交互。
手写Api文档的几个痛点:
文档需要更新的时候,需要再次发送一份给前端,也就是文档更新交流不及时。
接口返回结果不明确
不能直接在线测试接口,通常需要使用工具,比如postman
接口文档太多,不好管理
Swagger也就是为了解决这个问题
Swagger的优势
- 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
- 提供 Web 页面在线测试 API:光有文档还不够,Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。
Swagger常用注解
- @Api:修饰整个类,描述Controller的作用
- @ApiOperation:描述一个类的一个方法,或者说一个接口
- @ApiParam:单个参数描述
- @ApiModel:用对象来接收参数
- @ApiProperty:用对象接收参数时,描述对象的一个字段
- @ApiResponse:HTTP响应其中1个描述
- @ApiResponses:HTTP响应整体描述
- @ApiIgnore:使用该注解忽略这个API
- @ApiError :发生错误返回的信息
- @ApiImplicitParam:一个请求参数
- @ApiImplicitParams:多个请求参数
6.7.2 整合Swagger
网关微服务整合Swagger, 首先导入相关依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
网关微服务编写配置类 cn.sm1234.gateway.DocumentationConfig
/**
* 通过配置资源文档,在首页下拉框选择订单系统时,
* 会请求 http://localhost:8081/order-server/v2/api-docs 获取文档详情,
* zuul根据路由配置,会将/order-server/**请求转发到路由serviceId为order-server的系统上
*/
@Component
@Primary
public class DocumentationConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
public DocumentationConfig(RouteLocator routeLocator) {
this.routeLocator = routeLocator;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<Route> routes = routeLocator.getRoutes();
routes.forEach(route -> {
resources.add(
swaggerResource(
route.getId(), route.getFullPath().replace("**", "v2/api-docs"),
"1.0"));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location,
String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
其他微服务模块整合Swagger, 也是先导入上面的两个依赖, 然后编写配置类 cn.sm1234.movie.SwaggerConfig
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket buildDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(buildApiInf()) // .apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage(""))// 需要生成文档的包的位置
.paths(PathSelectors.any())
.build();
}
private ApiInfo buildApiInf() {
return new ApiInfoBuilder()
.title("电影接口详情")
.description("Zuul+Swagger2构建RESTful APIs")
.version("1.0")
.build();
}
}
所有整合了Swagger的微服务模块启动类上添加开启Swagger功能的注解配置@EnableSwagger2.
// 网关微服务启动类
@SpringBootApplication
@EnableZuulProxy // 开启网关代理功能
@EnableSwagger2 // 开启Swagger2功能
public class GatewayApplication {....}
// 电影微服务启动类
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
@EnableFeignClients // 开启OpenFeign自动配置
@EnableHystrix // 开启Hystrix熔断器的自动配置
@EnableSwagger2 // 开启Swagger2功能
public class MovieApplication {.....}
// 用户微服务启动类
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端自动配置
@EnableSwagger2 // 开启Swagger2功能
public class UserApplication {.....}
在微服务源码中进行api描述
电影微服务的cn.sm1234.movie.controller.MovieController
@RequestMapping("/movie")
@RestController
@Api(value = "电影服务控制器")
public class MovieController {
// .......省略大部分逻辑
@ApiOperation(value = "远程方法(RestTemplate): 根据用户ID查询用户的方法")
@GetMapping(value = "/order")
public String order() {...}
@GetMapping(value = "/order2")
@ApiOperation(value = "远程方法(Eureka+RestTemplate): 根据用户ID查询用户的方法")
public String order2() {...}
@ApiOperation(value = "远程方法(Ribbon): 根据用户ID查询用户的方法")
@GetMapping(value = "/order3")
public String order3() {...}
@ApiOperation(value = "远程方法(Ribbon-简化): 根据用户ID查询用户的方法")
@GetMapping(value = "/order4")
@HystrixCommand(fallbackMethod = "fallback")
public String order4() {...}
@ApiOperation(value = "远程方法(OpenFeign): 根据用户ID查询用户的方法")
@GetMapping(value = "/order5")
public String order5() {...}
}
用户微服务的cn.sm1234.user.controller.UserController
@RequestMapping("/user")
@RestController // @RestController 是 @Controller 和 @ResponseBody 两个注解的结合体
@Api(value = "用户服务控制器")
public class UserController {
// .......省略大部分逻辑
@ApiOperation(value = "根据主键查询用户信息")
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User findById(@PathVariable Integer id) {...}
@ApiOperation(value = "添加用户信息")
@RequestMapping(value = "/add", method = RequestMethod.POST)
public void add(@RequestBody User user) {...}
@ApiOperation(value = "修改用户信息")
@RequestMapping(value = "/update", method = RequestMethod.PUT)
public void update(@RequestBody User user, Integer id) {...}
@ApiOperation(value = "删除用户信息")
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public void delete(@PathVariable Integer id) {...}
}
6.7.3 Swagger测试
访问http://localhost:9004/swagger-ui.html, 进入到Swagger的web界面
测试用户接口api



测试电影接口api



注意: Zuul过滤器的权限认证token会拦截swagger的请求, 需要使用
zuul.ignored-patterns=/**/v2/**排除掉拦截, 我没有成功, 不知道原因. 但swagger的请求被拦截导致无法获取服务的api文档问题, 需要查找解决方案.
7. Spring Cloud config集中配置
7.1 Spring Cloud Config简介
为什么需要Spring Cloud Config?
随着线上项目变的日益庞大,每个项目都散落着各种配置文件,如果采用分布式的开发模式,需要的配置文件随着服务增加而不断增多。某一个基础服务信息变更,都会引起一系列的更新和重启,运维苦不堪言也容易出错。配置中心便是解决此类问题的灵丹妙药。
认识Spring Cloud Config
Spring Cloud Config是Spring Cloud团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。
7.2 Git管理配置
首先在父工程下面创建microservice_config微服务模块, 创建过程和上面其他微服务一样.
导入配置中心服务端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
首先需要在gitee或github上面新建一个配置仓库 , 上传电影微服务和用户微服务的配置文件. (application.yml文件重命名为xxx-dev.yml)
配置中心微服务的application.yml添加配置项
server:
port: 12002
spring:
application:
name: microservice-config
cloud:
config:
server:
git:
uri: https://gitee.com/wang-qz/springcloud-config.git # 远程配置仓库地址
编写启动类cn.sm1234.config.ConfigApplication
@SpringBootApplication
@EnableConfigServer // 开启SpringCloud Config配置中心功能
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class);
}
}
电影微服务和用户微服务需要导入配置中心的客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
电影微服务和用户微服务的配置文件已经上传到远程仓库, 所以需要删除本地工程的application.yml文件, 同时新增一个引导文件bootstrap.yml, 添加引导项配置:
spring:
cloud:
config:
uri: http://127.0.0.1:12001 # spring cloud config服务端地址
name: user # 配置文件的前缀
profile: dev # 配置文件的后缀
label: master # 远程分支
spring:
cloud:
config:
uri: http://127.0.0.1:12001 # spring cloud config服务端地址
name: movie # 配置文件的前缀
profile: dev # 配置文件的后缀
label: master # 远程分支
补充:
application.yml是用户级的资源配置项.
bootstrap.yml是系统级的, 优先级更加高.
Spring Cloud会创建一个Bootstrap Context, 作为Spring应用的Application Context的父上下文. 初始化的时候,Bootstrap Context负
责从外部加载配置属性并解析配置. 这两个上下文共享一个从外部获取的Enviroment.Boostrap属性有优先级, 默认情况下, 它们
不会被本地配置覆盖.Bootstrap Context和Application Context有着不同的约定. 所以新增了一个bootstrap.yml文件, 保证Bootstrap Context和Application Context配置的分离.
启动配置中心微服务, 重启用户微服务, 访问 http://localhost:12002/user-dev.yml, 可以通过配置中心微服务读取到远程仓库的用户微服务配置内容.
通过配置中心微服务访问远程配置的方式2:
http://localhost:12002/user/dev/master
重启电影微服务 , 访问http://localhost:12001/movie-dev.yml, 可以通过配置中心微服务读取到远程仓库的电影微服务配置内容.
访问方式2, http://localhost:12002/movie/dev/master
分别访问http://localhost:9004/microservice-user/user/2 和 http://localhost:9004/microservice-movie/movie/order5 也可以成功返回, 说明远程配置有被使用生效.

7.3 SVN管理配置
微服务配置的远程仓库除了使用git外, 也支持SVN做远程配置仓库. 其他配置都一样, 只需要修改配置中心微服务的远程配置项.
首先需要在配置中心微服务导入SVN的依赖
<dependency>
<groupId>org.tmatesoft.svnkit</groupId>
<artifactId>svnkit</artifactId>
<version>1.8.10</version>
</dependency>
我就不详细演示了, 下面是SVN的配置方式
7.4 搭建高可用Spring Cloud Config
上面的配置中心还有很多弊端, 无法实现高可用, 如果配置中心微服务停机了, 会导致用户微服务和电影微服务在启动过程中无法获取到远程仓库的配置内容, 而无法启动成功. 所以需要搭建高可用的配置中心微服务集群.
首先, 在配置中心微服务导入Eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在配置中心微服务修改配置项, 将配置中心微服务注册到Eureak. 需要添加Eureka客户端配置
server:
port: 12001
eureka:
client:
register-with-eureka: true # 是否需要从Eureka获取注册信息
fetch-registry: true # 是否需要把该服务注册到Eureka
service-url: # 客户端注册地址
defaultZone: http://127.0.0.1:8888/eureka,http://127.0.0.1:8889/eureka
instance:
prefer-ip-address: true # 优先使用该服务的IP地址注册到Eureka, 在生产环境建议改为true
spring:
application:
name: microservice-config
cloud:
config:
server:
git:
uri: https://gitee.com/wang-qz/springcloud-config.git
配置中心微服务启动类上添加@EnableEurekaClient注解
@SpringBootApplication
@EnableConfigServer // 开启SpringCloud Config配置中心功能
@EnableEurekaClient // 开启Eureka配置
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class);
}
}
电影微服务和用户微服务修改引导文件bootstrap.yml配置项, 将上面的配置中心url配置修改为从Eureka自动发现可用的配置中心微服务.
spring:
cloud:
config:
## uri: http://127.0.0.1:12001 # spring cloud config服务端地址
name: user # 配置文件的前缀
profile: dev # 配置文件的后缀
label: master # 远程分支
discovery:
enabled: true
service-id: microservice-config
# Eureka客户端配置
eureka:
client:
register-with-eureka: true
fetch-registry: true # 从Eureka拉取注册服务列表
service-url:
defaultZone: http://127.0.0.1:8888/eureka,http://127.0.0.1:8889/eureka
instance:
prefer-ip-address: true
spring:
cloud:
config:
## uri: http://127.0.0.1:12001 # spring cloud config服务端地址
name: movie # 配置文件的前缀
profile: dev # 配置文件的后缀
label: master # 远程分支
discovery:
enabled: true
service-id: microservice-config
# Eureka客户端配置
eureka:
client:
register-with-eureka: true
fetch-registry: true # 从Eureka拉取注册服务列表
service-url:
defaultZone: http://127.0.0.1:8888/eureka,http://127.0.0.1:8889/eureka
instance:
prefer-ip-address: true
启动配置中心微服务集群, 重启电影微服务和用户微服务,查看Eureka控制台服务注册列表:

从上图发现, 配置中心微服务已经成功注册到Eureka服务中, 如果其中一台停机, 还可以从另外一台配置中心微服务拉取远程仓库的配置信息.
7.5 引入Spring Cloud Bus实现配置实时更新
上面即使实现了高可用的集群配置, 但只是静态的远程配置, 无法实时刷新修改的远程配置内容. Spring Cloud提供了动态配置实现方案: 消息总线 Spring Cloud Bus.
Spring Cloud Bus可实现实时更新远程配置, 需要借助消息队列MQ实现. 这里我使用的是rabbitMQ.
7.5.1 docker安装运行rabbmitMQ
使用docker安装运行rabbmitMQ相关的命令
# 下载rabbitMQ镜像
docker pull rabbitmq:management
# 运行rabbitMQ容器
docker run --name rabbitmq -p 5671:5671 -p 5672:5672 -p 15671:15671 -p 15672:15672 -p 4369:4369 -p 25672:25672 rabbitmq:management
# 查看运行的容器
docker ps -a
# 停止rabbitMQ容器
docker stop rabbitmq
# 启动rabbitMq容器
docker start rabbitmq
# 移除rabbitMQ容器
docker rm rabbitmq
启动rabbitmq后, 访问http://192.168.65.129:15672/ , 查看rabbitmq控制台.
输入默认用户密码 guest/guest , 进入rabbitmq控制台查看.
7.5.2 微服务整合rabbitmq
配置中心微服务, 用户微服务和电影微服务添加springCloud Bus依赖.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
配置中心微服务的application.yml文件添加如下配置内容:
spring:
# rabbitmq服务端地址
rabbitmq:
host: 192.168.65.129
# 暴露触发消息总线的地址
management:
endpoints:
web:
exposure:
include: bus-refresh
另外, 用户微服务和电影微服务还需要暴露端点, 需要添加actuator依赖.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
用户微服务和电影微服务在引导文件bootstrap.yml中配置rabbitmq
spring:
# rabbitmq服务端地址
rabbitmq:
host: 192.168.65.129
重启配置中心微服务, 电影微服务和用户微服务.
7.5.3 测试SpringCloud Bus
访问http://localhost:12001/user-dev.yml或http://localhost:12002/user-dev.yml, 从远程仓库读取的配置信息中数据库配置的mysql端口是3306.
我们访问 http://localhost:9004/microservice-user/user/2, 查看返回结果.
现在, 我们在Gitee上将远程仓库的user-dev.yml文件中的mysql端口修改为3307(事先已经运行了mysql02:3307的容器, 创建了与3306一样的数据库表, 但数据不一样).
修改完后, 访问 http://localhost:12001/user-dev.yml或http://localhost:12002/user-dev.yml, 从远程仓库读取的配置信息中数据库配置的mysql端口已经成功修改为3307.
当我们再次访问http://localhost:9004/microservice-user/user/2, 发现还是端口3306数据库的数据, 因为还需要通过触发暴露的消息总线来更新用户微服务和电影微服务的配置. 在postman访问http://127.0.0.1:12001/actuator/bus-refresh和http://127.0.0.1:12002/actuator/bus-refresh 即可. 重新访问http://localhost:9004/microservice-user/user/2, 数据返回正常.
8. Spring Cloud分布式链路追踪
8.1 初识Spring Cloud Sleuth
8.1.1 应用背景
微服务架构下,一个请求可能会经过多个服务才会得到结果,如果在这个过程中出现了异常,就很难去定位问题。所以,必须要实现一个分布式链路跟踪的功能,直观的展示出完整的调用过程。
8.1.2 Spring Cloud Sleuth简介
微服务架构下,一个请求可能会经过多个服务才会得到结果,如果在这个过程中出现了异常,就很难去定位问题。所以,必须要实现一个分布式链路跟踪的功能,直观的展示出完整的调用过程。
如果想要诊断复杂操作,通常的解决方案是在请求中传递唯一的ID到每个方法来识别日志。而Sleuth可以与日志框架Logback、SLF4J轻松地集成,通过添加独特的标识符来使用日志跟踪和诊断问题。
8.2 整合Spring Cloud Sleuth
在网关微服务, 电影微服务和用户微服务中添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
添加日志输出代码
private Logger logger = LoggerFactory.getLogger(this.getClass());
logger.info("Doing some work");
日志的格式为:[application name, traceId, spanId, export]
- application name — 应用的名称,也就是application.properties中的spring.application.name参数配置的属性。
- traceId — 为一个请求分配的ID号,用来标识一条请求链路。
- spanId — 表示一个基本的工作单元,一个请求可以包含多个步骤,每个步骤都拥有自己的spanId。一个请
- 包含一个TraceId,多个SpanId
- export — 布尔类型。表示是否要将该信息输出到类似Zipkin这样的聚合器进行收集和展示。
- 可以看到,TraceId和SpanId在两条日志中是相同的,即使消息来源于两个不同的类。这就可以在不同的日志通过寻找traceid来识别一个请求。
请求微服务,这里我通过网关访问电影微服务http://localhost:9004/microservice-movie/movie/order5 ,然后查询日志. 可以看到有一个日志追踪号ff728007ec7fe75b贯穿了整个请求周期.
网关微服务日志追踪
电影微服务日志追踪
用户微服务日志追踪
8.3 Spring Cloud Sleuth整合Zipkin
8.3.1 Zipkin简介
单独使用Spring Cloud Sleuth依靠控制台追踪日志的方法非常不方便,不直观!这时可以使用Zipkin来收集所有日志信息,更加直观.
Zipkin分布式跟踪系统, 它可以帮助收集时间数据,解决在微服务架构下的延迟问题;它管理这些数据的收集和查找;Zipkin的设计是基于谷歌的Google Dapper论文。
每个应用程序向Zipkin报告定时数据,Zipkin UI呈现了一个依赖图表来展示多少跟踪请求经过了每个应用程序;如果想解决延迟问题,可以过滤或者排序所有的跟踪请求,并且可以查看每个跟踪请求占总跟踪时间的百分比。
8.3.2 搭建Zipkin服务
在父工程中新增zipkin_server微服务模块, 创建过程和其他微服务一样.
在zipkin微服务中导入zipkin依赖
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.9.4</version>
<!--排除-->
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.9.4</version>
</dependency>
application.yml添加配置信息
server:
port: 9041
spring:
application:
name: zipkin-server
# 去除控制台异常
management:
metrics:
web:
server:
auto-time-requests: false
编写启动类 cn.sm1234.zipkin.ZipkinApplication, 启动类上添加@EnableZipkinServer注解
// zipkin服务的启动类
@SpringBootApplication
@EnableZipkinServer
public class ZipkinApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplication.class);
}
}
8.3.3 注册Zipkin服务
修改网关及服务消费者,生产者所有微服务,让它们注册到Zipkin中,以便让这些微服务产生的日志能发送被Zipkin收集到.
网关微服务, 电影微服务和用户微服务需要做如下修改.
导入zipkin启动依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
因为zipkin启动依赖已经包含了spring cloud sleuth, 所以要删除之前的spring cloud sleuth依赖.
网关微服务, 电影微服务和用户微服务的application.yml添加zipkin注册配置
spring:
zipkin:
base-url: http://127.0.0.1:9041
sender:
type: web
sleuth:
sampler:
probability: 1
注意: 因为电影微服务和用户微服务使用的配置中心读取远程配置, 所以上面的配置项要加在远程仓库的movie-dev.yml和user-dev.yml文件中.
8.3.4 Zipkin日志采集
启动zipkin微服务, 网关微服务, 电影微服务和用户微服务, 访问http://localhost:9004/microservice-movie/movie/order5.
然后查看zipkin控制台, http://localhost:9041/zipkin/
链路追踪

依赖分析
个人博客
欢迎各位访问我的个人博客: https://www.crystalblog.xyz/



