一、笔记内容技术选型
| 技术 | 版本 |
|---|---|
| Java | jdk17+ |
| boot | 3.2.0 |
| cloud | 2023.0.0 |
| cloud alibaba | 2022.0.0.0-RC2 |
| Maven | 3.9+ |
| MySQL | 8.0+ |
二、Spring Cloud 介绍
1.为什么需要 Spring Cloud?
传统的单体架构足以满足中小型项目的需求,但是如果对于一个用户量庞大的系统就会出现各种问题。
例如:如果只有一个支付系统,那么系统崩溃了整个系统就运作不了了。
而分布式系统解决了这个问题,它允许系统以集群的形式部署,形成负载均衡,尽量减少系统崩溃带来的问题。
2.相关组件介绍
在 2019 年之前,使用的大部分技术都是 Netflix 提供的,但是由于开发 SpringCloud 的相关技术不挣钱,因此 Netflix 就暂停开发相关技术了,虽然他提供的那些技术依旧可以使用,但是已经不推荐了。
因此该笔记只学习新的架构,对于老的技术栈,如果老项目中需要用到,请去官方文档继续学习。

- 注册与发现
- Eureka【Netflix 最后的火种,不推荐】
- Consul【推荐使用】
- Etcd【可以使用】
- Nacos【推荐使用,阿里巴巴提供的】
- 服务调用和负载均衡
- Ribbon【Netflix 提供的,建议直接弃用】
- OpenFeign
- LoadBalancer
- 分布式事务
- Seata【推荐使用,阿里巴巴的】
- LCN
- Hmily
- 服务熔断和降级
- Hystrix【已经停更了,不推荐】
- Circuit Breaker【这只一套规范,使用的是它的实现类】
- Resilience4J【CircuitBreaker 的实现类,可以使用】
- Sentinel【阿里巴巴的,推荐使用】
- 服务链路追踪
- Sleuth+Zipkin【逐渐被替代了,不推荐】
- Micrometer Tracing【推荐使用】
- 服务网关
- Zuul【不推荐使用】
- Gate Way
- 分布式配置管理
- Config+Bus【不推荐了】
- Consul
- Nacos
三、单体项目构建
需求说明:下订单,调用支付接口
要求:
1.先做一个通用的 boot 微服务
2.逐步引入 cloud 组件,最后编程 cloud 架构
1.SpringBoot 单体服务
1.1 项目构建
建库建表,表名t_pay
CREATE DATABASE db2024;
USE db2024;
DROP TABLE IF EXISTS `t_pay`;
CREATE TABLE `t_pay` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`pay_no` VARCHAR(50) NOT NULL COMMENT '支付流水号',
`order_no` VARCHAR(50) NOT NULL COMMENT '订单流水号',
`user_id` INT(10) DEFAULT '1' COMMENT '用户账号 ID',
`amount` DECIMAL(8,2) NOT NULL DEFAULT '9.9' COMMENT '交易金额',
`deleted` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标志,默认 0 不删除,1 删除',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT;
t_pay(pay_no,order_no) (,);
t_pay;
父工程的 pom 文件导入依赖,然后刷新
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.3</mybatis.springboot.version>
<mysql.version>8.0.11</mysql.version>
<swagger3.version>2.2.0</swagger3.version>
<mapper.version>4.2.3</mapper.version>
<fastjson2.version>2.0.40</fastjson2.version>
<persistence-api.version>1.0.2</persistence-api.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0
2022.0.0.0-RC2
org.springframework.boot
spring-boot-starter-parent
${spring.boot.version}
pom
import
org.springframework.cloud
spring-cloud-dependencies
${spring.cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring.cloud.alibaba.version}
pom
import
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.springboot.version}
mysql
mysql-connector-java
${mysql.version}
com.alibaba
druid-spring-boot-starter
${druid.version}
tk.mybatis
mapper
${mapper.version}
javax.persistence
persistence-api
${persistence-api.version}
com.alibaba.fastjson2
fastjson2
${fastjson2.version}
org.springdoc
springdoc-openapi-starter-webmvc-ui
${swagger3.version}
cn.hutool
hutool-all
${hutool.version}
org.springframework.boot
spring-boot-starter-test
${spring.boot.test.version}
test
检查 java 编译版本

检查注解支撑是否打开

检查项目的编码格式,统一为 UTF-8

新建一个 Maven 工程,除了pom.xml、.idea其他的东西都删了

1.2 MyBatis 逆向工程
本次使用 Mapper4,可以不用写单表操作了
双击运行 Maven 中的插件

在子模块的 resources 下新建文件generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}" = = =>
在子模块的 resources 下新建文件config.properties,将内容改成自己的
#t_pay 表包名 package.name=com.example.cloud
# mysql8.0 jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
在父工程下面创建一个子模块,给子模块导入依赖
说明:这个工程只是为了暂时存储生成的代码,等到业务工程使用的时候,会将对应的类复制过去
<dependencies>
<!--Mybatis 通用 mapper tk 单独使用,自己独有 + 自带版本号-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Mybatis Generator 自己独有 + 自带版本号-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--通用 Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--mysql8.0-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java
javax.persistence
persistence-api
cn.hutool
hutool-all
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
${basedir}/src/main/java
**/*.xml
${basedir}/src/main/resources
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
org.mybatis.generator
mybatis-generator-maven-plugin
1.4.2
${basedir}/src/main/resources/generatorConfig.xml
true
true
mysql
mysql-connector-java
8.0.33
tk.mybatis
mapper
4.2.3
1.3 编写业务逻辑
- 创建一个业务逻辑模块
cloud-provider-payment8001 - 将逆向工程生成的代码拷贝到业务工程中,删除原本逆向工程中生成的代码
- 编写 Service、Controller 层的增删改查方法
- 启动项目,测试接口
创建启动类
@SpringBootApplication
@MapperScan("com.example.cloud.mapper")
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class, args);
}
}
编写 yaml 配置文件
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.cloud.entities
configuration:
map-underscore-to-camel-case: true
给模块导入依赖
<dependencies>
<!--SpringBoot 通用依赖模块-->
<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>
<!--SpringBoot 集成 druid 连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://localhost:8001/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis 和 springboot 整合-->
<>
org.mybatis.spring.boot
mybatis-spring-boot-starter
mysql
mysql-connector-java
javax.persistence
persistence-api
tk.mybatis
mapper
cn.hutool
hutool-all
com.alibaba.fastjson2
fastjson2
org.projectlombok
lombok
1.18.28
provided
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
3.2.0
1.4 整合 Swager3
| 注解 | 标注位置 |
|---|---|
| @Tag | Controller 类 |
| @Operation | 方法上 |
| @Schema | model 层的 bean 和 bean 的方法上 |
启动项目,访问 swagger 的地址,调试接口
localhost:8001/swagger-ui/index.html
编写配置类,配置 Swagger
@Configuration
public class SwaggerConfiguration {
@Bean
public GroupedOpenApi PayApi() {
//以/pay开头的请求都是支付模块
return GroupedOpenApi.builder()
.group("支付微服务模块")
.pathsToMatch("/pay/**")
.build();
}
@Bean
public GroupedOpenApi OtherApi() {
//以/other开头的都是其他模块的请求
return GroupedOpenApi.builder()
.group("其它微服务模块")
.pathsToMatch("/other/**", "/others")
.build();
}
@Bean
public OpenAPI docsOpenApi() {
return new OpenAPI().info(new Info().title("cloud2024").description("通用设计 rest").version("v1.0"));
}
}
Controller 的方法上加@Operation 注解
@Operation(summary="查询所有订单")
Controller 加上@Tag 注解
@Tag(name ="支付模块")
添加依赖
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger3.version}</version>
</dependency>
1.5 统一返回结果 Result
修改原来接口的返回值
@Operation(summary ="添加支付记录")
@PostMapping(value ="/pay/add")
public ResultData<String> addPay(@RequestBody Pay pay) {
System.out.println(pay.toString());
int add = payService.add(pay);
return ResultData.success("添加成功" + add + "条记录");
}
定义统一返回类 Result
@Data
@Accessors(chain = true)
public class ResultData<T> {
private String code;
private String message;
private T data;
private long timestamp; //调用方法的时间戳
public ResultData() {
this.timestamp = System.currentTimeMillis();
}
public static <T> ResultData<T> success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(ReturnCodeEnum.RC200.getCode());
resultData.setMessage(ReturnCodeEnum.RC200.getMessage());
resultData.setData(data);
return resultData;
}
public static <T> ResultData<T> fail(String code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(code);
resultData.setMessage(message);
return resultData;
}
}
定义一个枚举类,用于状态码的返回【枚举类的书写方法 1.举值 2.构造 3.遍历】
@Getter
public enum ReturnCodeEnum {
//1.举值
RC999("999", "操作 XXX 失败"),
RC200("200", "success"),
RC201("201", "服务开启降级保护,请稍后再试!"),
RC202("202", "热点参数限流,请稍后再试!"),
RC203("203", "系统规则不满足要求,请稍后再试!"),
RC204("204", "授权规则不通过,请稍后再试!"),
RC403("403", "无访问权限,请联系管理员授予权限"),
RC401("401", "匿名用户访问无权限资源时的异常"),
RC404("404", "404 页面找不到的异常"),
RC500("500", "系统异常,请稍后重试"),
RC375("375", "数学运算异常,请稍后重试"),
INVALID_TOKEN("2001", "访问令牌不合法"),
ACCESS_DENIED("2003", "没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED("1001", "客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR("1002", "用户名或密码错误"),
BUSINESS_ERROR("1004", "业务逻辑异常"),
UNSUPPORTED_GRANT_TYPE("1003", "不支持的认证模式");
//2.构造
private final String code; //自定义状态码,对应前面枚举的第一个参数
private final String message; //自定义信息,对应前面枚举的第二个参数
ReturnCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
ReturnCodeEnum {
(ReturnCodeEnum element : ReturnCodeEnum.values()) {
(element.getCode().equalsIgnoreCase(code)) {
element;
}
}
;
}
}
1.6 优化时间格式
有两种解决方式:
方式二:SpringBoot 项目在 yml 中进行配置
spring:
jackson:
date-format: yyyy-MM-dd HH-mm-ss
time-zone: GMT+8
方式一:在实体类的时间属性上加@JsonFormat 注解
@JsonFormat(pattern ="yyyy-MM-dd HH-mm-ss", timezone ="GMT+8")
private Date createTime;
1.7 异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
//注解的参数是处理的异常信息类型,什么都不加就是全局异常处理
//@ExceptionHandler(SQlException.class) 这个就是专门处理 sql 异常
@ExceptionHandler()
public ResultData<String> globalException(Exception e) {
e.printStackTrace();
return ResultData.fail(ReturnCodeEnum.RC500.getCode(), ReturnCodeEnum.RC500.getMessage());
}
}
1.8 编写订单模块【模块构建参考上述步骤】
这个模块的 controller 使用 http 请求调用 pay 模块的方法就行。
因此将 entities、utils 包中的代码复制过去即可,然后编写 controller。
@RestController
public class OrderController {
private String url ="http://localhost:8001";
//使用 httpclient 调用 pay 模块的相关接口
@GetMapping("/consumer/pay/add")
public ResultData addOrder(PayDTO payDTO) throws IOException {
//创建 httpclient 客户端
CloseableHttpClient aDefault = HttpClients.createDefault();
//创建一个 post 请求
HttpPost httpPost = new HttpPost(url + "/pay/add");
//将本方法的参收构建为 json 字符串
String jsonString = JSON.toJSONString(payDTO);
//将 json 字符串构建为 StringEntity
StringEntity stringEntity = new StringEntity(jsonString);
//设置请求头和编码格式
stringEntity.setContentType("application/json");
stringEntity.setContentEncoding("UTF-8");
//将参数传入 post 请求
httpPost.setEntity(stringEntity);
//httpclient 客户端执行请求
CloseableHttpResponse execute = aDefault.execute(httpPost);
//获取响应实体
HttpEntity entity execute.getEntity();
EntityUtils.toString(entity);
JSON.parseObject(string);
(String) jsonObject.get();
(String) jsonObject.get();
(code.equals()) {
ResultData.success( + data);
} {
ResultData.fail(code, (String) jsonObject.get());
}
}
ResultData IOException {
HttpClients.createDefault();
(url + + id);
aDefault.execute(httpGet);
execute.getEntity();
EntityUtils.toString(entity);
JSON.parseObject(string);
(String) jsonObject.get();
jsonObject.get();
(code.equals()) {
ResultData.success(data);
} {
ResultData.fail(code, (String) jsonObject.get());
}
}
}
1.9 服务调用 RestTemplate
RestTemplate 是一套封装好的客户端工具,能够发起 HTTP 请求。类似于 okHttp、HttpClient,但是相较于他们,做了更进一步的封装,简化了发送请求的过程。
发送请求
//1.get 请求【两个方法任选一个】
//两个方法的参数一样:①url 请求地址 ②请求参数【可以省略】 ③返回值接收对象类型
//只接受返回对象用这个:restTemplate.getForObject("请求地址",参数,返回值对象类型)
Result result = restTemplate.getForObject("http://localhost:8080/order/getPayResult", Result.class);
//全部响应体用这个:restTemplate.getForEntity("请求地址",参数,返回值对象类型)
ResponseEntity<Result> forEntity = restTemplate.getForEntity("http://localhost:8001/pay/get/all", Result.class);
//2.post 请求【两个方法任选一个】
//两个方法的参数一样:①url 请求地址 ②请求参数 ③返回值接收对象类型
//只接受返回对象用这个:restTemplate.postForObject("请求地址",参数,返回值对象类型)
Result result = restTemplate.postForObject("http://localhost:8080/order/getPayResult", pay, Result.class);
//全部响应体用这个:restTemplate.postForEntity("请求地址",参数,返回值对象类型)
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity("http://localhost:8080/order/getPayResult", pay, String.class);
//3.delete 请求
//方法的参数:①url 请求地址 restTemplate.delete("http://localhost:8080/order/getPayResult");
//4.put 请求
//方法的参数:①url 请求地址 ②请求参数 restTemplate.put("http://localhost:8080/order/getPayResult", pay);
创建对象
//两种方式
//1.使用的时候直接 new
RestTemplate restTemplate = new RestTemplate();
//2.在容器中配置一个
@Configuration
public class OrderConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
导入依赖
SpringBootWeb 中自带,所以如果是一个 web 项目就不需要导入额外的依赖了
1.10 重复代码抽取
问题:两个模块中有很多重复的代码。例如实体类、返回结果、异常处理类等。
解决方法:将公共代码抽取到一个模块中,其他模块引用公共模块
- 将模块打成 jar 包,放到本地仓库中
- 启动项目,测试功能
支付和订单模块在 pom 文件中引入公共模块的 jar 包
<dependency>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
将前面两个模块中的公共代码抽取出来放到这个新的模块中
例如:entities 包、utils 包、exception 包
创建一个模块cloud-api-commons,引入依赖
<dependencies>
<!--SpringBoot 通用依赖模块-->
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
javax.persistence
persistence-api
2.问题引入
问题:为什么一定要引入 SpringCloud?
回答:我们刚才那样将每一个模块拆成一个个微服务之后,使用 http 调用起来很麻烦,而且地址是写死的。如果我们的项目地址变了,我们的代码不得不修改。而且后面如果每个模块以集群部署,每个模块都会有多个地址,那地址该怎么写呢?
四、Consul 服务注册和发现
1.基本介绍
Consul 是什么?Consul 是一款开源的分布式服务发现与配置管理系统,由 HashiCorp 公司使用 Go 语言开发。官方:http://consul.io/
Consul 能干什么?服务发现:提供 HTTP 和 DNS 两种发现方式健康检测 KV 存储多数据中心可视化 WEB 界面
为什么不使用 Eureka 了?Eureka 停更了,不在开发新版本了 Eureka 对初学者不友好 我们希望注册中心能够从项目中分离出来,单独运行,而 Eureka 做不到这一点
2.下载运行
- 下载地址:https://developer.hashicorp.com/consul/install
- 下载对应的版本【adm64 版本的就是 x86_64 版本的,386 就是 x86_32 版本的】
访问 8500 端口,进入 ui 界面
localhost:8500
windows 使用下面的命令启动,然后缩放到最小化就行
consul agent -dev
3.服务注册与发现
需求说明:将前面单体服务中的支付模块、订单模块注册到 Consul 中
- 启动 boot 项目
- 去 consul 的 ui 页面查看是否注册成功
- 测试接口调用是否成功
因为 consul 默认支持负载均衡,所以 http 客户端加上@LoadBalanced注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
将订单接口中支付模块的 url 地址改为 consul 中注册的名字
private String url ="http://cloud-payment-service";
启动类加上@EnableDiscoveryClient注解,开启服务发现功能
@EnableDiscoveryClient
编写配置文件 yaml【健康检查那里不配置 consul 会爆红,不知道为什么】
spring:
#当前服务名
application:
name: cloud-pay-service
#配置注册中心的地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#配置当前服务注册到里面使用的名字
service-name: ${spring.application.name}
#开启 consul 的健康检查
heartbeat:
enabled: true
ttl: 10s
对应模块的 pom 文件中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
4.服务配置
问题说明:
系统拆分之后,会产生大量的微服务。每个微服务都有其对应的配置文件 yml。如果其中的某个配置项发生了修改,一个一个微服务修改会很麻烦。因此一套集中式的、动态的配置管理设施是必不可少的。从而实现一次修改,处处生效。
思路:既然是全局配置信息,那么可以把信息注册到 Consul 中,需要什么就去 Consul 中获取
- 说明:在 consul 配置数据源,项目读取之后启动,然后改变 consul 数据源的值没作用,不知道为什么。
编写代码,查看项目能否读取到 consul 中的 k-v 值
//从 application.yml 中获取
@Value("${server.port}")
private String port;
@Value("${altman.info}")
String info;
@GetMapping(value ="/pay/get/consul")
//从 consul 中获取
public ResultData getConsul(@Value("${altman.info}") String info) {
return ResultData.success(info + "当前端口号" + port);
}
创建 data 文件,随便输入数据库的账号和密码,测试项目是否能读取成功,并连接数据库

在 consul 中创建二级文件夹config/微服务名/,然后创建data文件,供项目测试是否能够读取
说明:配置默认存储到 config/微服务名 - 配置文件版本/data 中,项目启动的时候使用的哪套 application.yaml 文件就会来这里找对应的文件
例如:cloud-payment-service 微服务如果在 application.yml 没有指定启用的配置文件
config/cloud-payment-service/data
例如:cloud-payment-service 微服务如果在 application.yml 指定启用的配置文件是 application-dev.yml
config/cloud-payment-service-dev/data
例如:cloud-payment-service 微服务如果在 application.yml 指定启用的配置文件是 application-prod.yml
config/cloud-payment-service-prod/data
如果创建的是文件夹那么以/结尾


打开 consul 的 ui 界面,找到 key-value,点击右上角的 create,创建文件夹

application.yml 就只剩下没有抽取出去的属于微服务自己的配置了
server:
port: 8001
spring:
jackson:
date-format: yyyy-MM-dd HH-mm-ss
time-zone: GMT+8
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.pojo
configuration:
map-underscore-to-camel-case: true
在 resources 下新建一个bootstrap.yml文件,将公共配置从 application.yml 中抽取出来
说明:bootstrap.yml 和 applicaiton.yml 一样都是配置文件。applicaiton.yml 是用户级的,bootstrap.yml 是系统级的,优先级更加高 Spring Cloud 会创建一个'Bootstrap Context',作为 Spring 应用的
Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。application.yml 和 bootstrap.yml 可以共存,公共的配置项写到 bootstrap.yml 中,项目特有的配置项写到 application.yml bootstrap.yml 比 application.yml 先加载的
spring:
#当前服务名
application:
name: cloud-pay-service
#配置注册中心的地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#配置当前服务注册到里面使用的名字
service-name: ${spring.application.name}
#开启 consul 的健康检查
heartbeat:
enabled: true
ttl: 10s
#服务配置
config:
#这个是配置文件名以 - 连接【consul 的 k-v 存储用到】,例如:cloud-payment-service
profile-separator: '-'
#说明 consul 中 kv 的文本格式
format: YAML
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#读取 consul 中的配置到这里
username: ${mysql.username}
password: ${mysql.password}
给对应的模块添加服务配置的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
5.动态刷新
需求说明:希望 Consul 的配置变动之后,项目读取的内容也能立马改变。
说明:在 consul 配置数据源,项目启动之后,改变数据源没作用,不知道为什么。
- 在主启动类加上
@RefreshScope注解【如果不生效,就放到 controller 上】
然后在 bootstrap.yml 中设置刷新的间隔【这一步不设置也可以,因为官网默认设置了 1s 刷新】
spring:
application:
name: cloud-payment-service
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-'
format: YAML
#设置了这里,1s 刷新
watch:
wait-time: 1
6.配置数据持久化
场景:如果我们把 Consul 关了,下次启动的时候,之前配置的 yaml 数据就会全丢了。我们现在需要解决这个问题。
解决方法:写了一个脚本,让 k-v 存储到指定文件夹。假如真是用到了可以去网上查。
五、LoadBalancer 负载均衡
1.基本介绍
LoadBlancer 的前身是 Ribbon,是一套负责负载均衡的客户端工具。
主要功能:LoadBlancer 的主要作用就是提供客户端软件的负载均衡,然后由 OpenFeign 去调用具体的微服务 负载均衡:通过算法,将请求平均分摊到多个服务上
2.基本使用
场景:订单模块通过负载均衡访问支付模块的 8001/8002/8003 服务
使用步骤:先从注册中心拉取可调用的服务列表,了解他有多少个服务按照指定的负载均衡策略,从服务列表中选择一个地址,进行调用
- 使用前提:已经使用了注册中心
- 启动两个支付模块的项目【为了方便,就不启动三个了】
- 测试接口
将调用的 url 改成在注册中心注册的名称
public static final String PaymentSrv_URL = "http://cloud-payment-service";
在订单模块的 RestTemplate 客户端上加@LoadBalanced,开启负载均衡
RestTemplate 和 WebClient 支持使用@LoadBalanced 注解实现负载均衡,而 HttpClient 不支持使用@LoadBalanced 注解实现负载均衡
因为 spring-cloud-starter-consul-discovery 中已经集成了 spring-cloud-starter-loadbalancer,所以不需要额外加注解了
如果没有 loadbalancer 的依赖,那就自己加上
3.基本原理
- 会在项目中创建一个 DiscoveryClient 对象
- 通过 DiscoveryClient 对象,就能够获取注册中心中所有注册的服务
- 然后将获取的服务与调用地址中传入的微服务名称进行对比
- 如果一致,就会将微服务集群的相关信息返回
- 然后通过负载均衡算法,选择出其中一个服务进行调用
4.负载均衡算法
LoadBlancer 默认包含两种负载均衡算法,轮询算法和随机算法,同时还可以自定义负载均衡算法。默认使用轮询算法。
- 支持自定义负载均衡算法
随机算法【LoadBlancer 中也包含】
随机给一个数,然后请求下标对应的微服务
轮询算法【LoadBlancer 默认使用这个】
实际调用服务器位置下标=rest 接口第几次请求数 % 服务器集群总数量【每次服务重启动后 rest 接口计数从 1 开始】
如:List[0] instances =127.0.0.1:8002
List[1] instances =127.0.0.1:8001
8001+8002 组合成为集群,它们共计 2 台机器,集群总数为 2,按照轮询算法原理:
当总请求数为 1 时:1%2=1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8001
当总请求数位 2 时:2%2=0 对应下标位置为 0,则获得服务地址为 127.0.0.1:8002
当总请求数位 3 时:3%2=1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8001
当总请求数位 4 时:4%2=0 对应下标位置为 0,则获得服务地址为 127.0.0.1:8002
如此类推......
5.负载均衡算法切换
默认的轮询足够开发中使用,这里只是简单说明一下
@Configuration
//下面的 value 值大小写一定要和 consul 里面的名字一样,必须一样
//value 的值是指对哪个微服务生效
@LoadBalancerClient(value ="cloud-payment-service", configuration = RestTemplateConfig.class)
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
//这里切换成了随机算法
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
六、OpenFeign 服务接口调用
1.基本介绍
OpenFeign 编写了一套声明式的 Web 服务客户端,使用 LoadBlancer 实现负载均衡,从而使 WEB 服务的调用变得很简单。
OpenFeign 已经是当前微服务调用最常用的技术
2.能干什么
前面的 LoadBalancer 章节,我们在使用 LoadBalancer+RestTemplate 实现了微服务的负载均衡调用,但是在实际开发中,一个接口往往会被多处调用,这就需要多次定义重复的代码,而 OpenFeign 简化了这个过程。
3.基本使用
controller 中注入 feign 接口对象,然后在需要的地方调用 feign 接口的方法
//注入 feign 对象
@Autowired
private PayFeignApi payFeignApi;
@GetMapping("/feign/pay/getall")
public ResultData getPayInfo() {
ResultData orders = payFeignApi.getOrders();
List<Pay> payList =(List<Pay>) orders.getData();
return ResultData.success(payList);
}
编写接口中的方法
@FeignClient("cloud-payment-service")
public interface PayFeignApi {
//方法上的注解就是被调用方法的请求类型和地址
//这样他就合成了 http://cloud-payment-service/pay/getall
@GetMapping("/pay/getall")
//这里的返回值需要和被调用接口的返回值一致
ResultData getOrders();
}
创建 OpenFeign 的接口,加上@FeignClient注解,注解的值就是被调用微服务的 name
//例如:被调用的模块是支付模块,支付模块在注册中心的名字叫 cloud-payment-service
@FeignClient("cloud-payment-service")
public interface PayFeignApi {}
在项目中创建一个 api 包,专门存放 OpenFegin 接口
这里以订单模块调用支付模块的接口为例,因此在订单模块中创建
启动类加上@EnableFeignClients 注解,启动 OpenFeign 功能
//如果 FeignClient 不在 SpringBootApplication 的扫描范围内可以在@EnableFeignClients 中指定扫描范围
@EnableFeignClients(basePackages="com.example.cloud")
引入 OpenFeign 和 LoadBlancer 的依赖
哪个服务需要调用其他服务的接口,就在哪个服务中引用【例如:订单服务调用支付服务的接口,就在订单服务中引入依赖】
引入 LoadBlancer 的依赖,是因为它使用 LoadBlancer 实现负载均衡
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer 做负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
4.最佳实践
上面的基本使用步骤只是基本用法,他暴露了几个基本问题。
如果某个接口需要在不同的微服务中被多次调用,那我们上面的这个写法就需要写多次,从而造成代码的冗余。
因此我们可以把所有的 Feign 接口抽取成一个公共的模块,然后其他模块引入这个 Feign 模块调用它里面的方法
- 在项目中创建一个模块
- 创建包,创建接口,编写接口中的方法
- 其他模块引用这个模块,调用模块中的方法
添加 openfeign 的依赖和公共模块的依赖【@FeignClient 注解需要 Feign 依赖】
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
5.超时控制
问题引入:比较简单的业务使用默认配置是没有问题的,但是如果是复杂业务需要进行很多操作,就可能会出现 Read Timeout 异常。因此学习定制化超时时间是有必要的 OpenFeign 客户端的默认等待时间 60S,超过这个时间就会报错(这个时间太长了,我们应该设置短一点)
通过两个参数控制超时时间:connectTimeout:连接超时时间【多长时间内必须建立链接】readTimeout:请求处理超时时间【多长时间内必须处理完成】【默认 60S】
5.1 全局配置
全局配置能直接控制所有的 Feign 超时时间
直接修改 yaml 文件
spring:
cloud:
openfeign:
client:
config:
default:
#指定超时时间最大:3S
read-timeout: 3000
#指定连接时间最大:3S
connect-timeout: 3000
5.2 指定配置
指定配置能够控制指定微服务的接口超时时间。
如果全局配置和指定配置同时存在,指定配置生效
spring:
cloud:
openfeign:
client:
config:
#这里将 default 换成微服务的名称
cloud-payment-service:
#指定超时时间最大:3S
read-timeout: 3000
#指定连接时间最大:3S
connect-timeout: 3000
6.重试机制
超时之后不会直接结束请求,而是会重新尝试连接
重试机制默认是关闭的,如何开启呢?只需要编写一个配置类,配置 Retryer 对象
//1.创建一个配置类
@Configuration
public class RetryerConfig {
//2.配置 Retryer
@Bean
public Retryer retryer() {
//3,设置重试机制
//return Retryer.NEVER_RETRY; 这个是默认的
//第一个参数是多长时间后开启重试机制:这里设置 100ms
//第二个参数是重试的间隔:这里设置 1s 一次
//第三个参数是最大请求次数:3 次【这个次数是一共的,也就是最大请求几次,而不是第一次请求失败后再请求几次】
return new Retryer.Default(100, 1, 3);
}
}
7.连接池
OpenFeign 允许指定连接方式,但是默认方式使用 jdk 自带的 HttpURLConnection,但是 HttpURLConnection 不支持连接池,因此性能较低。
HttpClient 和 OkHttp 都支持连接池,因此为了提升 OpenFeign 的性能,可以改成使用 HttpClient5
在配置文件中开启 hc5
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
引入 HttpClient5 和 Feign-hc5 依赖
<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
8.请求/响应压缩
OpenFeign 支持对请求和响应进行 GZIP 压缩,以减少通信过程中的性能损耗。
spring:
cloud:
openfeign:
compression:
request:
#开启请求压缩
enabled: true
#达到多大才触发压缩
min-request-size: 2048
#触发压缩的类型
mime-types: types=text/xml,application/xml,application/json
response:
#开启响应压缩
enabled: true
9.日志打印
OpenFeign 需要输出日志需要符合两个条件:FeignClient 所在的包日志级别为 debug Feign 的日志级别在 NONE 以上
Feign 的日志级别:NONE:不记录任何日志信息,这是默认值。BASIC:仅记录请求的方法,URL 以及响应状态码和执行时间 HEADERS:在 BASIC 的基础上,额外记录了请求和响应的头信息 FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
配置文件中设置 feign 所在包的打印级别
logging:
level:
#下面是 feign 接口的包
com.example.cloud.apis.PayFeignApi: debug
定义一个类定义 Feign 的日志级别
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.FULL; //日志级别
}
}
七、CircuitBreaker 断路器
1.基本介绍
分布式系统存在的问题:复杂的分布式应用程序,调用关系复杂,往往有数十个调用关系,调用关系在某些时候将不可避免的失败。比如:超时、异常等。因此我们需要一个框架保证在调用出现问的情况下,不会导致整体服务的失败,避免级联故障,从而提高分布式系统的弹性。
解决思路:对于有问题的节点/服务,不再接受请求(快速返回失败处理,或者返回默认的兜底处理结果)
断路器就是这种开关装置。可以想象成家里的保险丝,假如家里真有某个电器发生了故障,能保证及时跳闸,别把整个家给烧了。
他的功能:
- 服务限时:只能在指定时间访问,其他时间均不可访问
- 实时监控
- 兜底的处理动作
服务预热:请求一点点放通,别一口气全进来
服务限流:限制访问微服务的请求的并发量,避免服务因流量激增出现故障【实现方法:前面加了一个限流器】

服务降级:让用户的体验变差【返回简单的提示】,但是不会导致服务的雪崩

服务熔断:当达到最大访问后,直接拒绝访问,此时调用方会接收到服务降级的处理并返回有好的兜底提示【就好像电闸直接跳了】
服务熔断会调用服务降级
2.CircuitBreaker 和 Resilience4 的关系
- CircuitBreaker 是一套抽象的规范
- Resilience4J 实现了 CircuitBreaker 的规范
3.CircuitBreaker 的实现原理
CircuitBreaker 的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
- 正常状态处于 close 状态【闸刀闭合】
- 当一个服务或组件出现故障,CircuitBreaker 会迅速切换到 Open 状态(跳闸断电),组织请求发送到该组件或服务,从而避免更多的请求发送到该组件或服务,防止组件或服务的进一步崩溃。
- 等待一段时间之后会尝试闭合 Half_Open,放几个请求过来探探路,如果可以用了,就会转换到 close 状态,如果还不行就还是变成 open 状态。
八、Resilience4J
1.基本介绍
Resilience4J 是一个轻量级的容错库,专门做服务熔断、降级等工作。
实现了 CircuitBreaker 规范。
Resilience4J 2 要求使用 Java17。
2.基本功能
2.1 熔断【服务熔断 + 服务降级】
2.1.1 断路器 3 大状态

2.1.2 断路器状态转换
断路器有三个普通状态:关闭 CLOSE【正常请求】、开启 OPEN【断电不可用】、半开 HALF_OPEN
1.当熔断器处于 CLOSE 关闭状态,所有的请求都会通过熔断器。
2.如果失败率超过设定的阈值,熔断器就会从关闭状态【CLOSE】转换到打开状态【OPEN】,这时所有的请求都会被拒绝
3.当处于开启状态【OPEN】一段时间后,熔断器就会从开启状态转换到半开状态【HALF_OPEN】,这时会有一定数量的请求放入,并重新计算失败率
4.如果失败率超过阈值,则会转成打开状态,如果低于阈值,则会变成关闭状态
还有两个特殊状态:DISABLED【始终允许访问】、FORCED_OPEN【始终拒绝访问】【这两个状态再生产中不会使用】
断路器的滑动窗口用来存储和统计调用的结果,可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口:
1.基于时间的滑动窗口:统计最近 N 秒的调用结果
2.基于数量的滑动窗口:统计最近 N 次调用的结果
2.2 限速
2.3 隔离
九、面试题
1.常见的注册中心和他们的特点
常见的注册中心:Eureka、Consul、Zookeeper、Nacos

Eureka:保证数据的可用性和容错性。为了保证高可用,牺牲了一定程度的数据一致性,这意味着服务列表可能不是实时准确的。同时不支持配置中心
Consul:在设计上更倾向于提供一致性和分区容错性。强一致性模型在某些情况下可能导致更高的延迟,尤其是在写操作频繁或网络状况不佳时。支持配置中心
Zookeep:类似 Consul,Zookeeper 也实现了 CP 原则。
2.客户端负载均衡和服务器负载均衡有什么区别?
Nginx 是服务器负载均衡,所有请求都交给 Nginx,由 Nginx 决定去访问哪个服务器的接口【类似于中介】
LoadBalancer 是客户端负载均衡,他在本地自己决定调用哪个服务器的接口【没有中间商赚差价】


