SpringCloud 学习笔记
档为 SpringCloud 微服务架构学习笔记,涵盖技术选型、单体项目构建、Consul 服务注册与发现、LoadBalancer 负载均衡、OpenFeign 服务接口调用及 CircuitBreaker 和 Resilience4J 熔断降级机制。内容包括环境配置、代码示例、常见问题解答及面试知识点总结。重点讲解了微服务拆分后的服务治理方案,包括服务注册、配置管理、远程调用及容错处理。

档为 SpringCloud 微服务架构学习笔记,涵盖技术选型、单体项目构建、Consul 服务注册与发现、LoadBalancer 负载均衡、OpenFeign 服务接口调用及 CircuitBreaker 和 Resilience4J 熔断降级机制。内容包括环境配置、代码示例、常见问题解答及面试知识点总结。重点讲解了微服务拆分后的服务治理方案,包括服务注册、配置管理、远程调用及容错处理。

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

需求说明:下订单,调用支付接口
要求:
1.先做一个通用的 boot 微服务
2.逐步引入 cloud 组件,最后编程 cloud 架构
建库建表,表名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='支付交易表';
INSERT INTO t_pay(pay_no,order_no) VALUES('pay17203699','6544bafb424a');
SELECT * FROM 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</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springboot 3.2.0-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloud 2023.0.0-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloud alibaba 2022.0.0.0-RC2-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringBoot 集成 mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!--Mysql 数据库驱动 8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--SpringBoot 集成 druid 连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--通用 Mapper4 之 tk.mybatis-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>${persistence-api.version}</version>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- swagger3 调用方式 http://你的主机 IP 地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger3.version}</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
检查 java 编译版本

检查注解支撑是否打开

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

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

本次使用 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}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"></jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="t_pay" domainObjectName="Pay">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>
在子模块的 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</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
cloud-provider-payment8001创建启动类
@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 整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql 数据库驱动 8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用 Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.2.0</version>
</plugin>
</plugins>
</build>
| 注解 | 标注位置 |
|---|---|
| @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>
修改原来接口的返回值
@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;
}
//3.遍历
public static ReturnCodeEnum getReturnCodeEnum(String code) {
//传入一个状态码,如果有,就返回整个枚举信息,如果没有就返回空
for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
if (element.getCode().equalsIgnoreCase(code)) {
return element;
}
}
return null;
}
}
有两种解决方式:
方式二: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;
@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());
}
}
这个模块的 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();
//将实体转化为 json 字符串
String string = EntityUtils.toString(entity);
//将字符串转化为 json 对象
JSONObject jsonObject = JSON.parseObject(string);
//从对象中获取对应的参数
String code = (String) jsonObject.get("code");
String data = (String) jsonObject.get("data");
if (code.equals("200")) {
return ResultData.success("调用成功 data=" + data);
} else {
return ResultData.fail(code, (String) jsonObject.get("message"));
}
}
@GetMapping("/consumer/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id) throws IOException {
CloseableHttpClient aDefault = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url + "/pay/get/" + id);
CloseableHttpResponse execute = aDefault.execute(httpGet);
HttpEntity entity = execute.getEntity();
String string = EntityUtils.toString(entity);
JSONObject jsonObject = JSON.parseObject(string);
String code = (String) jsonObject.get("code");
Object data = jsonObject.get("data");
if (code.equals("200")) {
return ResultData.success(data);
} else {
return ResultData.fail(code, (String) jsonObject.get("message"));
}
}
}
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 项目就不需要导入额外的依赖了
问题:两个模块中有很多重复的代码。例如实体类、返回结果、异常处理类等。
解决方法:将公共代码抽取到一个模块中,其他模块引用公共模块
支付和订单模块在 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>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
</dependencies>
问题:为什么一定要引入 SpringCloud?
回答:我们刚才那样将每一个模块拆成一个个微服务之后,使用 http 调用起来很麻烦,而且地址是写死的。如果我们的项目地址变了,我们的代码不得不修改。而且后面如果每个模块以集群部署,每个模块都会有多个地址,那地址该怎么写呢?
Consul 是什么?Consul 是一款开源的分布式服务发现与配置管理系统,由 HashiCorp 公司使用 Go 语言开发。官方:http://consul.io/
Consul 能干什么?服务发现:提供 HTTP 和 DNS 两种发现方式健康检测 KV 存储多数据中心可视化 WEB 界面
为什么不使用 Eureka 了?Eureka 停更了,不在开发新版本了 Eureka 对初学者不友好 我们希望注册中心能够从项目中分离出来,单独运行,而 Eureka 做不到这一点
访问 8500 端口,进入 ui 界面
localhost:8500
windows 使用下面的命令启动,然后缩放到最小化就行
consul agent -dev
需求说明:将前面单体服务中的支付模块、订单模块注册到 Consul 中
因为 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>
问题说明:
系统拆分之后,会产生大量的微服务。每个微服务都有其对应的配置文件 yml。如果其中的某个配置项发生了修改,一个一个微服务修改会很麻烦。因此一套集中式的、动态的配置管理设施是必不可少的。从而实现一次修改,处处生效。
思路:既然是全局配置信息,那么可以把信息注册到 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>
需求说明:希望 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
场景:如果我们把 Consul 关了,下次启动的时候,之前配置的 yaml 数据就会全丢了。我们现在需要解决这个问题。
解决方法:写了一个脚本,让 k-v 存储到指定文件夹。假如真是用到了可以去网上查。
LoadBlancer 的前身是 Ribbon,是一套负责负载均衡的客户端工具。
主要功能:LoadBlancer 的主要作用就是提供客户端软件的负载均衡,然后由 OpenFeign 去调用具体的微服务 负载均衡:通过算法,将请求平均分摊到多个服务上
场景:订单模块通过负载均衡访问支付模块的 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 的依赖,那就自己加上
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
如此类推......
默认的轮询足够开发中使用,这里只是简单说明一下
@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 编写了一套声明式的 Web 服务客户端,使用 LoadBlancer 实现负载均衡,从而使 WEB 服务的调用变得很简单。
OpenFeign 已经是当前微服务调用最常用的技术
前面的 LoadBalancer 章节,我们在使用 LoadBalancer+RestTemplate 实现了微服务的负载均衡调用,但是在实际开发中,一个接口往往会被多处调用,这就需要多次定义重复的代码,而 OpenFeign 简化了这个过程。
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>
上面的基本使用步骤只是基本用法,他暴露了几个基本问题。
如果某个接口需要在不同的微服务中被多次调用,那我们上面的这个写法就需要写多次,从而造成代码的冗余。
因此我们可以把所有的 Feign 接口抽取成一个公共的模块,然后其他模块引入这个 Feign 模块调用它里面的方法
添加 openfeign 的依赖和公共模块的依赖【@FeignClient 注解需要 Feign 依赖】
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
问题引入:比较简单的业务使用默认配置是没有问题的,但是如果是复杂业务需要进行很多操作,就可能会出现 Read Timeout 异常。因此学习定制化超时时间是有必要的 OpenFeign 客户端的默认等待时间 60S,超过这个时间就会报错(这个时间太长了,我们应该设置短一点)
通过两个参数控制超时时间:connectTimeout:连接超时时间【多长时间内必须建立链接】readTimeout:请求处理超时时间【多长时间内必须处理完成】【默认 60S】
全局配置能直接控制所有的 Feign 超时时间
直接修改 yaml 文件
spring:
cloud:
openfeign:
client:
config:
default:
#指定超时时间最大:3S
read-timeout: 3000
#指定连接时间最大:3S
connect-timeout: 3000
指定配置能够控制指定微服务的接口超时时间。
如果全局配置和指定配置同时存在,指定配置生效
spring:
cloud:
openfeign:
client:
config:
#这里将 default 换成微服务的名称
cloud-payment-service:
#指定超时时间最大:3S
read-timeout: 3000
#指定连接时间最大:3S
connect-timeout: 3000
超时之后不会直接结束请求,而是会重新尝试连接
重试机制默认是关闭的,如何开启呢?只需要编写一个配置类,配置 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);
}
}
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>
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
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 的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
Resilience4J 是一个轻量级的容错库,专门做服务熔断、降级等工作。
实现了 CircuitBreaker 规范。
Resilience4J 2 要求使用 Java17。

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

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

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online