SpringCloud学习笔记【尚硅谷2024版】
文章目录
- 一、笔记内容技术选型
- 二、Spring Cloud介绍
-
- 1.为什么需要Spring Cloud?
- 2.相关组件介绍
- 三、单体项目构建
-
- 1.SpringBoot单体服务
-
- 1.1 项目构建
- 1.2 MyBatis逆向工程
- 1.3 编写业务逻辑
- 1.4 整合Swager3
- 1.5 统一返回结果Result
- 1.6 优化时间格式
- 1.7 异常处理
- 1.8 编写订单模块【模块构建参考上述步骤】
- 1.9 服务调用RestTemplate
- 1.10 重复代码抽取
- 2.问题引入
- 四、Consul服务注册和发现
-
- 1.基本介绍
- 2.下载运行
- 3.服务注册与发现
- 4.服务配置
- 5.动态刷新
- 6.配置数据持久化
- 六、LoadBlancer负载均衡
-
- 1.基本介绍
- 2.基本使用
- 3.基本原理
- 4.负载均衡算法
- 5.负载均衡算法切换
- 七、OpenFeign服务接口调用
-
- 1.基本介绍
- 2.能干什么
- 3.基本使用
- 4.最佳实践
- 5.超时控制
-
- 5.1 全局配置
- 5.2 指定配置
- 6.重试机制
- 7.连接池
- 8.请求/响应压缩
- 9.日志打印
- 八、CircuitBreaker断路器
-
- 1.基本介绍
- 2.CircuitBreaker和Resilience4的关系
- 3.CircuitBreaker的实现原理
- 九、Resilience4J
-
- 1.基本介绍
- 2.基本功能
-
- 2.1 熔断【服务熔断+服务降级】
-
- 2.1.1 断路器3大状态
- 2.1.2 断路器状态转换
- 2.2 限速
- 2.3 隔离
- 五、面试题
-
- 1.常见的注册中心和他们的特点
- 2.客户端负载均衡和服务器负载均衡有什么区别?
一、笔记内容技术选型
技术
版本
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就暂停开发相关技术了,虽然他提供的那些技术依旧可以使用,但是已经不推荐了。
因此该笔记只学习新的架构,对于老的技术栈,如果老项目中需要用到,请去B站继续学习
- 注册与发现
- 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 项目构建
-
新建一个Maven工程,除了
pom.xml
、.idea
其他的东西都删了 -
检查项目的编码格式,统一为UTF-8
-
检查注解支撑是否打开
-
检查java编译版本
-
父工程的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>
-
建库建表,表名
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;
1.2 MyBatis逆向工程
本次使用Mapper4,可以不用写单表操作了
-
在父工程下面创建一个子模块,给子模块导入依赖
说明:这个工程只是为了暂时存储生成的代码,等到业务工程使用的时候,会将对应的类复制过去
<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>
-
在子模块的resources下新建文件
config.properties
,将内容改成自己的#t_pay表包名 package.name=com.atguigu.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
-
在子模块的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>
-
双击运行Maven中的插件
1.3 编写业务逻辑
-
创建一个业务逻辑模块
cloud-provider-payment8001
-
给模块导入依赖
<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://你的主机IP地址:5555/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>
-
编写yaml配置文件
server: port: 8001 # ==========applicationName + druid-mysql8 driver=================== 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=================== mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.atguigu.cloud.entities configuration: map-underscore-to-camel-case: true
-
创建启动类
@SpringBootApplication @MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan; public class Main8001 { public static void main(String[] args) { SpringApplication.run(Main8001.class, args); } }
-
将逆向工程生成的代码拷贝到业务工程中,删除原本逆向工程中生成的代码
-
编写Service、Controller层的增删改查方法
-
启动项目,测试接口
1.4 整合Swager3
注解
标注位置
@Tag
Controller类
@Operation
方法上
@Schema
model层的bean和bean的方法上
-
添加依赖
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>${swagger3.version}</version> </dependency>
-
Controller加上@Tag注解
@Tag(name ="支付模块")
-
Controller的方法上加@Operation注解
@Operation(summary="查询所有订单")
-
编写配置类,配置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")) .externalDocs(new ExternalDocumentation() .description("www.atguigu.com") .url("https://yiyan.baidu.com/")); } }
-
启动项目,访问swagger的地址,调试接口
localhost:8001/swagger-ui/index.html
1.5 统一返回结果Result
-
定义一个枚举类,用于状态码的返回【枚举类的书写方法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; } }
-
定义统一返回类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; } }
-
修改原来接口的返回值
@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+"条记录"); }
1.6 优化时间格式
有两种解决方式:
-
方式一:在实体类的时间属性上加@JsonFormat注解
@JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss" ,timezone = "GMT+8") private Date createTime;
-
方式二:SpringBoot项目在yml中进行配置
spring: jackson: date-format: yyyy-MM-dd HH-mm-ss time-zone: GMT+8
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();
//将实体转化为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"));
}
}
}
1.9 服务调用RestTemplate
RestTemplate是一套封装好的客户端工具,能够发起HTTP请求。类似于okHttp、HttpClient,但是相较于他们,做了更进一步的封装,简化了发送请求的过程。
-
导入依赖
SpringBootWeb中自带,所以如果是一个web项目就不需要导入额外的依赖了
-
创建对象
//两种方式 //1.使用的时候直接new RestTemplate restTemplate = new RestTemplate(); //2.在容器中配置一个 @Configuration public class OrderConfiguration { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
-
发送请求
//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.10 重复代码抽取
问题:两个模块中有很多重复的代码。例如实体类、返回结果、异常处理类等。
解决方法:将公共代码抽取到一个模块中,其他模块引用公共模块
-
创建一个模块
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>
-
将前面两个模块中的公共代码抽取出来放到这个新的模块中
例如:entities包、utils包、exception包
-
将模块打成jar包,放到本地仓库中
-
支付和订单模块在pom文件中引入公共模块的jar包
<dependency> <groupId>com.atguigu.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
启动项目,测试功能
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版本的】
-
windows使用下面的命令启动,然后缩放到最小化就行
consul agent -dev
-
访问8500端口,进入ui界面
localhost:8500
3.服务注册与发现
需求说明:将前面单体服务中的支付模块、订单模块注册到Consul中
-
对应模块的pom文件中引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
-
编写配置文件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
-
启动类加上
@EnableDiscoveryClient
注解,开启服务发现功能【有人说可以不加这个注解了】@EnableDiscoveryClient
-
启动boot项目
-
去consul的ui页面查看是否注册成功
-
将订单接口中支付模块的url地址改为consul中注册的名字
private String url="http://cloud-payment-service";
-
因为consul默认支持负载均衡,所以http客户端加上
@LoadBalanced
注解@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
-
测试接口调用是否成功
4.服务配置
问题说明:
系统拆分之后,会产生大量的微服务。每个微服务都有其对应的配置文件yml。如果其中的某个配置项发生了修改,一个一个微服务修改会很麻烦。因此一套集中式的、动态的配置管理设施是必不可少的。从而实现一次修改,处处生效。
案例:给班里同学通知下节课不上了
麻烦的方法:一个个发送消息
简单的方法:直接在班级群@所有人
思路:既然是全局配置信息,那么可以把信息注册到Consul中,需要什么就去Consul中获取
-
给对应的模块添加服务配置的依赖
<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>
-
在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://altman.fun:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true #读取consul中的配置到这里 username: ${mysql.username} password: ${mysql.password}
-
-
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: fun.altman.pojo configuration: map-underscore-to-camel-case: true
-
打开consul的ui界面,找到key-value,点击右上角的create,创建文件夹
-
在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
如果创建的是文件夹那么以
/
结尾 -
创建data文件,随便输入数据库的账号和密码,测试项目是否能读取成功,并连接数据库
-
编写代码,查看项目能否读取到consul中的k-v值
//从application.yml中获取 @Value("${server.port}") private String port; @GetMapping(value = "/pay/get/consul") //从consul中获取 public ResultData getConsul(@Value("${altman.info}") String info) { return ResultData.success(info+"当前端口号"+port); }
-
说明:在consul配置数据源,项目读取之后启动,然后改变consul数据源的值没作用,不知道为什么。
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存储到指定文件夹。假如真是用到了可以去网上查。
六、LoadBlancer负载均衡
1.基本介绍
LoadBlancer的前身是Ribbon,是一套负责负载均衡的客户端工具。
主要功能:LoadBlancer的主要作用就是提供客户端软件的负载均衡,然后由OpenFeign去调用具体的微服务
- 负载均衡:通过算法,将请求平均分摊到多个服务上
2.基本使用
场景:订单模块通过负载均衡访问支付模块的8001/8002/8003服务
使用步骤:
- 先从注册中心拉取可调用的服务列表,了解他有多少个服务
- 按照指定的负载均衡策略,从服务列表中选择一个地址,进行调用
-
使用前提:已经使用了注册中心
-
启动两个支付模块的项目【为了方便,就不启动三个了】
-
因为spring-cloud-starter-consul-discovery 中已经集成了spring-cloud-starter-loadbalancer,所以不需要额外加注解了
如果没有loadbalancer的依赖,那就自己加上
-
在订单模块的RestTemplate客户端上加
@LoadBalanced
,开启负载均衡RestTemplate和WebClient支持使用@LoadBalanced注解实现负载均衡,而HttpClient不支持使用@LoadBalanced注解实现负载均衡
-
将调用的url改成在注册中心注册的名称
public static final String PaymentSrv_URL = "http://cloud-payment-service";
-
测试接口
3.基本原理
- 会在项目中创建一个DiscoveryClient对象
- 通过DiscoveryClient对象,就能够获取注册中心中所有注册的服务
- 然后将获取的服务与调用地址中传入的微服务名称进行对比
- 如果一致,就会将微服务集群的相关信息返回
- 然后通过负载均衡算法,选择出其中一个服务进行调用
4.负载均衡算法
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 如此类推......
-
随机算法【LoadBlancer中也包含】
随机给一个数,然后请求下标对应的微服务
-
支持自定义负载均衡算法
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.基本使用
-
引入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>
-
启动类加上@EnableFeignClients注解,启动OpenFeign功能
//如果FeignClient不在SpringBootApplication的扫描范围内可以在@EnableFeignClients中指定扫描范围 @EnableFeignClients(basePackages="com.atguigu.cloud")
-
在项目中创建一个api包,专门存放OpenFegin接口
这里以订单模块调用支付模块的接口为例,因此在订单模块中创建
-
创建OpenFeign的接口,加上
@FeignClient
注解,注解的值就是被调用微服务的name//例如:被调用的模块是支付模块,支付模块在注册中心的名字叫cloud-payment-service @FeignClient("cloud-payment-service") public interface PayFeignApi { }
-
编写接口中的方法
@FeignClient("cloud-payment-service") public interface PayFeignApi { //方法上的注解就是被调用方法的请求类型和地址 //这样他就合成了http://cloud-payment-service/pay/getall @GetMapping("/pay/getall") //这里的返回值需要和被调用接口的返回值一致 ResultData getOrders(); }
-
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); }
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
-
引入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>
-
在配置文件中开启hc5
spring: cloud: openfeign: httpclient: hc5: enabled: true
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的日志级别
public class DefaultFeignConfig { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.FULL;//日志级别 } }
-
配置文件中设置feign所在包的打印级别
logging: level: #下面是feign接口的包 com: atguigu: cloud: apis: PayFeignApi: debug
八、CircuitBreaker断路器
1.基本介绍
分布式系统存在的问题:复杂的分布式应用程序,调用关系复杂,往往有数十个调用关系,调用关系在某些时候将不可避免的失败。比如:超时、异常等。因此我们需要一个框架保证在调用出现问的情况下,不会导致整体服务的失败,避免级联故障,从而提高分布式系统的弹性。
解决思路:对于有问题的节点/服务,不再接受请求(快速返回失败处理,或者返回默认的兜底处理结果)
断路器就是这种开关装置。可以想象成家里的保险丝,假如家里真有某个电器发生了故障,能保证及时跳闸,别把整个家给烧了。
他的功能:
-
服务熔断:当达到最大访问后,直接拒绝访问,此时调用方会接收到服务降级的处理并返回有好的兜底提示【就好像电闸直接跳了】
服务熔断会调用服务降级
-
服务降级:让用户的体验变差【返回简单的提示】,但是不会导致服务的雪崩
-
服务限流:限制访问微服务的请求的并发量,避免服务因流量激增出现故障【实现方法:前面加了一个限流器】
-
服务限时:只能在指定时间访问,其他时间均不可访问
-
服务预热:请求一点点放通,别一口气全进来
例如:学校开门了,门缝由小变大 一开始让10个请求进来,后面够20个请求的空间了,再后来门全打开了,可以一次让100个请求进来了
-
实时监控
-
兜底的处理动作
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.客户端负载均衡和服务器负载均衡有什么区别?
Nging是服务器负载均衡,所有请求都交给Nginx,由Nginx决定去访问哪个服务器的接口【类似于中介】
LoadBlancer是服务端负载均衡,他在本地自己决定调用哪个服务器的接口【没有中间商赚差价】
https://www.bilibili.com/video/BV1gW421P7RDp=35&spm_id_from=pageDriver&vd_source=b246a40ef435cdf32c518bf3f296775d