跳到主要内容
SpringCloud 学习笔记 | 极客日志
Java java
SpringCloud 学习笔记 综述由AI生成 档为 SpringCloud 微服务架构学习笔记,涵盖技术选型、单体项目构建、Consul 服务注册与发现、LoadBalancer 负载均衡、OpenFeign 服务接口调用及 CircuitBreaker 和 Resilience4J 熔断降级机制。内容包括环境配置、代码示例、常见问题解答及面试知识点总结。重点讲解了微服务拆分后的服务治理方案,包括服务注册、配置管理、远程调用及容错处理。
SecGuard 发布于 2026/3/29 更新于 2026/5/28 27 浏览一、笔记内容技术选型
技术 版本 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 项目构建 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;
<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 >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-parent</artifactId >
<version > ${spring.boot.version}</version >
<type > pom</type >
<scope > import</scope >
</dependency >
<dependency >
<groupId > org.springframework.cloud</groupId >
<artifactId > spring-cloud-dependencies</artifactId >
<version > ${spring.cloud.version}</version >
<type > pom</type >
<scope > import</scope >
</dependency >
<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 >
<dependency >
<groupId > org.mybatis.spring.boot</groupId >
<artifactId > mybatis-spring-boot-starter</artifactId >
<version > ${mybatis.springboot.version}</version >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
<version > ${mysql.version}</version >
</dependency >
<dependency >
<groupId > com.alibaba</groupId >
<artifactId > druid-spring-boot-starter</artifactId >
<version > ${druid.version}</version >
</dependency >
<dependency >
<groupId > tk.mybatis</groupId >
<artifactId > mapper</artifactId >
<version > ${mapper.version}</version >
</dependency >
<dependency >
<groupId > javax.persistence</groupId >
<artifactId > persistence-api</artifactId >
<version > ${persistence-api.version}</version >
</dependency >
<dependency >
<groupId > com.alibaba.fastjson2</groupId >
<artifactId > fastjson2</artifactId >
<version > ${fastjson2.version}</version >
</dependency >
<dependency >
<groupId > org.springdoc</groupId >
<artifactId > springdoc-openapi-starter-webmvc-ui</artifactId >
<version > ${swagger3.version}</version >
</dependency >
<dependency >
<groupId > cn.hutool</groupId >
<artifactId > hutool-all</artifactId >
<version > ${hutool.version}</version >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
<version > ${spring.boot.test.version}</version >
<scope > test</scope >
</dependency >
</dependencies >
</dependencyManagement >
新建一个 Maven 工程,除了pom.xml、.idea其他的东西都删了
1.2 MyBatis 逆向工程 在子模块的 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 >
<dependency >
<groupId > org.mybatis</groupId >
<artifactId > mybatis</artifactId >
<version > 3.5.13</version >
</dependency >
<dependency >
<groupId > org.mybatis.generator</groupId >
<artifactId > mybatis-generator-core</artifactId >
<version > 1.4.2</version >
</dependency >
<dependency >
<groupId > tk.mybatis</groupId >
<artifactId > mapper</artifactId >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
</dependency >
<dependency >
<groupId > javax.persistence</groupId >
<artifactId > persistence-api</artifactId >
</dependency >
<dependency >
<groupId > cn.hutool</groupId >
<artifactId > hutool-all</artifactId >
</dependency >
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
<optional > true</optional >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
<scope > test</scope >
<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 >
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);
}
}
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 >
<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 > com.alibaba</groupId >
<artifactId > druid-spring-boot-starter</artifactId >
</dependency >
<dependency >
<groupId > org.springdoc</groupId >
<artifactId > springdoc-openapi-starter-webmvc-ui</artifactId >
</dependency >
<dependency >
<groupId > org.mybatis.spring.boot</groupId >
<artifactId > mybatis-spring-boot-starter</artifactId >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
</dependency >
<dependency >
<groupId > javax.persistence</groupId >
<artifactId > persistence-api</artifactId >
</dependency >
<dependency >
<groupId > tk.mybatis</groupId >
<artifactId > mapper</artifactId >
</dependency >
<dependency >
<groupId > cn.hutool</groupId >
<artifactId > hutool-all</artifactId >
</dependency >
<dependency >
<groupId > com.alibaba.fastjson2</groupId >
<artifactId > fastjson2</artifactId >
</dependency >
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
<version > 1.18.28</version >
<scope > provided</scope >
</dependency >
<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 >
1.4 整合 Swager3 注解 标注位置 @Tag Controller 类 @Operation 方法上 @Schema model 层的 bean 和 bean 的方法上
localhost:8001/swagger-ui/index.html
@Configuration
public class SwaggerConfiguration {
@Bean
public GroupedOpenApi PayApi () {
return GroupedOpenApi.builder()
.group("支付微服务模块" )
.pathsToMatch("/pay/**" )
.build();
}
@Bean
public GroupedOpenApi OtherApi () {
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="查询所有订单")
<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 + "条记录" );
}
@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 {
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" , "不支持的认证模式" );
private final String code;
private final String message;
ReturnCodeEnum(String code, String message) {
this .code = code;
this .message = message;
}
public static ReturnCodeEnum getReturnCodeEnum (String code) {
for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
if (element.getCode().equalsIgnoreCase(code)) {
return element;
}
}
return null ;
}
}
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()
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" ;
@GetMapping("/consumer/pay/add")
public ResultData addOrder (PayDTO payDTO) throws IOException {
CloseableHttpClient aDefault = HttpClients.createDefault();
HttpPost httpPost = new HttpPost (url + "/pay/add" );
String jsonString = JSON.toJSONString(payDTO);
StringEntity stringEntity = new StringEntity (jsonString);
stringEntity.setContentType("application/json" );
stringEntity.setContentEncoding("UTF-8" );
httpPost.setEntity(stringEntity);
CloseableHttpResponse execute = aDefault.execute(httpPost);
HttpEntity entity = execute.getEntity();
String string = EntityUtils.toString(entity);
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" ));
}
}
}
1.9 服务调用 RestTemplate
RestTemplate 是一套封装好的客户端工具,能够发起 HTTP 请求。类似于 okHttp、HttpClient,但是相较于他们,做了更进一步的封装,简化了发送请求的过程。
Result result = restTemplate.getForObject("http://localhost:8080/order/getPayResult" , Result.class);
ResponseEntity<Result> forEntity = restTemplate.getForEntity("http://localhost:8001/pay/get/all" , Result.class);
Result result = restTemplate.postForObject("http://localhost:8080/order/getPayResult" , pay, Result.class);
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity("http://localhost:8080/order/getPayResult" , pay, String.class);
RestTemplate restTemplate = new RestTemplate ();
@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 >
<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 >
<dependency >
<groupId > cn.hutool</groupId >
<artifactId > hutool-all</artifactId >
</dependency >
<dependency >
<groupId > javax.persistence</groupId >
<artifactId > persistence-api</artifactId >
</dependency >
</dependencies >
2.问题引入 回答:我们刚才那样将每一个模块拆成一个个微服务之后,使用 http 调用起来很麻烦,而且地址是写死的。如果我们的项目地址变了,我们的代码不得不修改。而且后面如果每个模块以集群部署,每个模块都会有多个地址,那地址该怎么写呢?
四、Consul 服务注册和发现
1.基本介绍
Consul 能干什么?服务发现 :提供 HTTP 和 DNS 两种发现方式健康检测 KV 存储多数据中心可视化 WEB 界面
为什么不使用 Eureka 了?Eureka 停更了,不在开发新版本了
Eureka 对初学者不友好
我们希望注册中心能够从项目中分离出来,单独运行,而 Eureka 做不到这一点
2.下载运行 windows 使用下面的命令启动,然后缩放到最小化就行
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注解,开启服务发现功能
编写配置文件 yaml【健康检查那里不配置 consul 会爆红,不知道为什么】
spring:
application:
name: cloud-pay-service
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true
ttl: 10s
<dependency >
<groupId > org.springframework.cloud</groupId >
<artifactId > spring-cloud-starter-consul-discovery</artifactId >
</dependency >
4.服务配置
问题说明 :
系统拆分之后,会产生大量的微服务。每个微服务都有其对应的配置文件 yml。如果其中的某个配置项发生了修改,一个一个微服务修改会很麻烦。因此一套集中式的、动态的配置管理设施是必不可少的。从而实现一次修改,处处生效。
思路 :既然是全局配置信息,那么可以把信息注册到 Consul 中,需要什么就去 Consul 中获取
说明:在 consul 配置数据源,项目读取之后启动,然后改变 consul 数据源的值没作用,不知道为什么。
编写代码,查看项目能否读取到 consul 中的 k-v 值
@Value("${server.port}")
private String port;
@Value("${altman.info}")
String info;
@GetMapping(value ="/pay/get/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}
heartbeat:
enabled: true
ttl: 10s
config:
profile-separator: '-'
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
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
watch:
wait-time: 1
6.配置数据持久化
场景:如果我们把 Consul 关了,下次启动的时候,之前配置的 yaml 数据就会全丢了。我们现在需要解决这个问题。
解决方法:写了一个脚本,让 k-v 存储到指定文件夹。假如真是用到了可以去网上查。
五、LoadBalancer 负载均衡
1.基本介绍
LoadBlancer 的前身是 Ribbon,是一套负责负载均衡的客户端工具。
主要功能:LoadBlancer 的主要作用就是提供客户端软件的负载均衡,然后由 OpenFeign 去调用具体的微服务
负载均衡:通过算法,将请求平均分摊到多个服务上
2.基本使用
场景:订单模块通过负载均衡访问支付模块的 8001/8002/8003 服务
使用步骤:先从注册中心拉取可调用的服务列表,了解他有多少个服务按照指定的负载均衡策略,从服务列表中选择一个地址,进行调用
使用前提:已经使用了注册中心
启动两个支付模块的项目【为了方便,就不启动三个了】
测试接口
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 默认包含两种负载均衡算法,轮询算法和随机算法,同时还可以自定义负载均衡算法。默认使用轮询算法。
实际调用服务器位置下标=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
@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 接口的方法
@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 {
@GetMapping("/pay/getall")
ResultData getOrders () ;
}
创建 OpenFeign 的接口,加上@FeignClient注解,注解的值就是被调用微服务的 name
@FeignClient("cloud-payment-service" )
public interface PayFeignApi {}
在项目中创建一个 api 包,专门存放 OpenFegin 接口
这里以订单模块调用支付模块的接口为例,因此在订单模块中创建
启动类加上@EnableFeignClients 注解,启动 OpenFeign 功能
@EnableFeignClients(basePackages="com.example.cloud" )
引入 OpenFeign 和 LoadBlancer 的依赖
哪个服务需要调用其他服务的接口,就在哪个服务中引用【例如:订单服务调用支付服务的接口,就在订单服务中引入依赖】
引入 LoadBlancer 的依赖,是因为它使用 LoadBlancer 实现负载均衡
<dependency >
<groupId > org.springframework.cloud</groupId >
<artifactId > spring-cloud-starter-openfeign</artifactId >
</dependency >
<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:
read-timeout: 3000
connect-timeout: 3000
5.2 指定配置
指定配置能够控制指定微服务的接口超时时间。
如果全局配置和指定配置同时存在,指定配置生效
spring:
cloud:
openfeign:
client:
config:
cloud-payment-service:
read-timeout: 3000
connect-timeout: 3000
6.重试机制
超时之后不会直接结束请求,而是会重新尝试连接
重试机制默认是关闭的,如何开启呢?只需要编写一个配置类,配置 Retryer 对象
@Configuration
public class RetryerConfig {
@Bean
public Retryer retryer () {
return new Retryer .Default(100 , 1 , 3 );
}
}
7.连接池
OpenFeign 允许指定连接方式,但是默认方式使用 jdk 自带的 HttpURLConnection,但是 HttpURLConnection 不支持连接池,因此性能较低。
HttpClient 和 OkHttp 都支持连接池,因此为了提升 OpenFeign 的性能,可以改成使用 HttpClient5
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
引入 HttpClient5 和 Feign-hc5 依赖
<dependency >
<groupId > org.apache.httpcomponents.client5</groupId >
<artifactId > httpclient5</artifactId >
<version > 5.3</version >
</dependency >
<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 :记录所有请求和响应的明细,包括头信息、请求体、元数据。
logging:
level:
com.example.cloud.apis.PayFeignApi: debug
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 是一个轻量级的容错库,专门做服务熔断、降级等工作。
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 是客户端负载均衡 ,他在本地自己决定调用哪个服务器的接口【没有中间商赚差价】
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online