微服务学习计划——SpringCloud
在学习并掌握了众多基础框架之后,我们的项目繁杂且难以掌握,那么我们就需要开启一门新的课程,也就是我们常说的微服务架构
随着互联网行业的发展,对服务的要求也越来越高,服务架构也从单体架构逐渐演变为现在流行的微服务架构。
这篇文章我们将会概括到下面几个知识:
- 认识微服务
- 服务拆分和远程调用
- Eureka注册中心
- Ribbon负载均衡
- Nacos注册中心
- Nacos配置管理
- Feign远程调用
- GateWay服务网关
认识微服务
首先我们需要去了解这么一个宏观的概念——微服务
单体架构
在微服务没有出现之前,也就是我们之前的项目书写,一般都是采用单体架构:
- 将业务的所有功能集中在一个项目中开发,打成一个包部署。
我们可以给出单体架构的直观图:
但是单体架构的优缺点也十分明显:
- 优点:
- 架构简单
- 部署成本低
- 缺点:
- 耦合度高(维护困难、升级困难)
分布式架构
当项目逐渐庞大之后,我们就开始使用分布式架构去处理项目:
- 根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
我们给出分布式架构的直观图:
同样我们也可以很直观的获得分布式架构的优缺点:
-
优点:
- 降低服务耦合
- 有利于服务升级和拓展
-
缺点:
- 服务调用关系错综复杂
微服务架构
我们从单体架构升级到分布式架构自然会存在一些我们目前无法解决的问题:
- 服务拆分的粒度如何界定?
- 服务之间如何调用?
- 服务的调用关系如何管理?
- 服务集群地址如何维护?
- 服务健康状态如何感知
而我们的微服务架构为我们的上述问题提供了一个统一的标准,因而微服务就此而生!
下面我们就来介绍微服务,微服务架构实际上是分布式架构的一种细化,我们给出微服务的架构特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
- 自治:团队独立、技术独立、数据独立,独立部署和交付
- 面向服务:服务提供统一标准的接口,与语言和技术无关
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
我们同样给出微服务的一张直观图:
微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。
因此,可以认为微服务是一种经过良好架构设计的分布式架构方案 。
微服务扩展
微服务这种方案需要技术框架来落地,目前国内知名度较高的就是SpringCloud和阿里巴巴的Dubbo
我们针对三种微服务技术做一个简单的对比:
Dubbo | SpringCloud | SpringCloudAlibaba | |
---|---|---|---|
注册中心 | zookeeper、Redis | Eureka、Consul | Nacos、Eureka |
服务远程调用 | Dubbo协议 | Feign(http协议) | Dubbo、Feign |
配置中心 | 无 | SpringCloudConfig | SpringCloudConfig、Nacos |
服务网关 | 无 | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul |
服务监控和保护 | dubbo-admin,功能弱 | Hystix | Sentinel |
最后我们再给出目前企业所常使用的微服务组合:
- SpringCloud + Feign
- 使用SpringCloud技术栈
- 服务接口采用Restful风格
- 服务调用采用Feign方式
- SpringCloudAlibaba + Feign
- 使用SpringCloudAlibaba技术栈
- 服务接口采用Restful风格
- 服务调用采用Feign方式
- SpringCloudAlibaba + Dubbo
- 使用SpringCloudAlibaba技术栈
- 服务接口采用Dubbo协议标准
- 服务调用采用Dubbo方式
- Dubbo原始模式
- 基于Dubbo老旧技术体系
- 服务接口采用Dubbo协议标准
- 服务调用采用Dubbo方式
SpringCloud
最后我们介绍一下SpringCloud:
- SpringCloud是目前国内使用最广泛的微服务框架。
- 官网地址:https://spring.io/projects/spring-cloud。
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
其中SpringCloud常用组件包括有:
- 服务注册发现:Eureka,Nacos,Consul
- 服务远程调用:OpenFeign,Dubbo
- 服务链路监控:Zipkin,Sleuth
- 统一配置管理:SpringCloudConfig,Nacos
- 统一网关路由:SpringCloudGateway,Zuul
- 流控降级保护:Hystix,Sentinel
服务拆分和远程调用
下面一个小节我们来学习服务拆分和远程调用两方面
服务拆分原则
我们前面提及到了分布式架构需要将功能拆分出来并分离开发,那么我们该如何进行拆分:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
服务拆分案例
我们给出一个简单的案例来展示服务拆分操作:
- 我们给出一个简单逻辑项目
- 首先我们存在一个父工程,名为cloud-demo
- 此外我们还有两个子工程,分别为order-service控制订单信息,user-service控制用户信息
我们首先给出图示逻辑:
我们需要满足一下需求:
- 订单微服务和用户微服务都必须有各自的数据库,相互独立
- 订单服务和用户服务都对外暴露Restful的接口
- 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库
那么我们给出案例书写:
- 导入数据库
# order订单数据库
-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '商品价格',
`num` int(10) NULL DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);
SET FOREIGN_KEY_CHECKS = 1;
# user用户数据库
-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '郑爽爽', '辽宁省沈阳市大东区');
INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');
SET FOREIGN_KEY_CHECKS = 1;
- IDEA代码书写
我们会创建一个如下框架的IDEA框架:
我们对上述信息进行讲解:
/* cloud-demo:父工程,携带pom.xml */
/* order-service订单工程 user-service用户工程 */
// 具有完整的dao,mapper,service,Controller层并完整书写Application启动类
// 具有yml配置文件,包含有port端口信息,mysql数据库信息等
远程调用案例
当我们运行程序后,我们可以在浏览器中查询到order相关数据:
但是我们会发现我们是无法查询到user的详细信息的,这是因为我们的order是没有user的数据库信息的
所以我们在完成了服务拆分之后就需要去了解远程调用:
- 目前我们的order数据库和user数据库是分开的,我们如果在查询order时还希望获得到order对应的user信息就需要采用远程调用
那么我们该如何实现远程调用:
- 借助RestTemplate类去完成远程调用
下面我们给出具体步骤及相关代码:
- 获得RestTemplate对象
// 我们需要将RestTemplate设置为Bean对象(这里直接在Application中设置Bean)
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 创建RestTemplate并注入Spring容器
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 借助RestTemplate对象去发送远程调用信息
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
// 这里在Service业务层获得信息
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 这里自动装填RestTemplate对象
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate发起http请求,查询用户
// 2.1.url路径
String url = "http://localhost/8081/user/" + order.getUserId();
// 2.2.发送http请求,实现远程调用
// 这里restTemplate具有getForObject方法和postForObject分别针对get和post请求,后面参数分别为url和对应的class
User user = restTemplate.getForObject(url, User.class);
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
- 查询页面获得完整信息
提供者和消费者
最后我们针对服务拆分和远程调用给出两个理论角色概念:
- 服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
- 服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
我们需要注意的是:
- 提供者和消费者的概念是具有相对性的,一个对象可能既是提供者也是消费者
- 例如A使用B,B使用C,那么B既是A的提供者也是C的消费者,这个概念并不是固定的
Eureka注册中心
下面我们来介绍一种注册中心EUreka
Eureka问题
首先我们需要知道Eureka是什么:
- Eureka是一种注册中心
- 假设我们的一个消费者需要去使用提供者,但是提供者的地址具有多个,那么我们的消费者该如何选择并使用对应的提供者
我们给出一个简单的图示展示:
例如上图:
- 我们的order-service需要使用user-service
- 但是user-service一共有三个,如果我们固定书写url地址信息,那么只会在一台服务器中获取,导致其他服务器空闲
那么我们就需要注意到三个问题:
- order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口?
- 有多个user-service实例地址,order-service调用时该如何选择?
- order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
Rureka结构与作用
首先我们给出Eureka的具体结构并对其分析:
我们对上图进行简单介绍:
- 首先我们的Eureka属于一个子工程,我们需要对其进行配置
- 其余的消费者或者提供者均属于Eureka-client,属于客户队列,我们采用Eureka对其服务
- 一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端
- 服务提供者在启动时会向Eureka-server提供一个注册信息,那么Eureka-server就会记录下对应信息保存
- 当我们的消费者需要对某些提供者进行消费时,会向Eureka-server索要对应信息,并根据Rureka对其提供者进行选择并调用
- 我们的Eureka-server可以判断服务提供者是否还存在或是否出现新的服务提供者,默认每30s去更新一次数据
那么我们就可以回答上述问题:
/* 问题1:order-service如何得知user-service实例地址? */
// - user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
// - eureka-server保存服务名称到服务实例地址列表的映射关系
// - order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取
/* 问题2;order-service如何从多个user-service实例中选择具体的实例? */
// - order-service从实例列表中利用负载均衡算法选中一个实例地址
// - 向该实例地址发起远程调用
/* 问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机? */
// - user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
// - 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
// - order-service拉取服务时,就能将故障实例排除了
Eureka-server服务搭建
下面我们逐渐介绍搭建Eureka-server的操作:
- 以cloud-demo为父工程,创建项目Eureka-server,引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
- 编写启动类,并添加@EnableEurekaServer注解
package cn.itcast.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
// 表示启动Eureka
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
- 添加yml文件,编写配置信息
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver # eureka的服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
- 启动Eureka并打开对应页面即可
Eureka服务注册
接下来我们来进行服务注册功能:
- 在user-service项目中引入Eureka-client依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demoartifactId>
<groupId>cn.itcast.demogroupId>
<version>1.0version>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>user-serviceartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
<build>
<finalName>appfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
- 在yml配置文件下配置相关信息
spring:
application: # 配置服务名称
name: userservice
eureka:
client:
service-url: # 配置对应注册的Rureka地址
defaultZone: http://127.0.0.1:10086/eureka
- 启动多个user-service实例对象
// 1.复制一份user-service启动配置(在启动第一份后在左下角可以找到user-service,右键Copy Configuration)
// 2.在复制界面的VMoptions修改端口:-Dserver.port=8082
// 3.启动即可在Eureka页面看到两个user-service
Eureka服务拉取
我们在前面的注册环境已经将两个user-service设置在同一服务中
那么我们的order-service如果想要调用user-service的接口,我们就需要稍微修改代码使其在两个服务器中拉取数据:
- 修改OrderService代码,修改url路径,用服务名代替url
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
// url路径(这里的路径直接修改为服务名userservice,使其在相同服务名的服务器中选择)
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}
}
- 修改RestTemplate的注解,使其采用负载均衡选择服务器
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
// @LoadBalanced表示负载均衡
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Ribbon负载均衡
下面我们来对上述的@Loadbalanced负载均衡进行部分介绍
Ribbon负载均衡流程
我们首先给出负载均衡流程图:
下面我们采用另一张图来详细解释上述图:
我们对其进行简单解释:
- 拦截我们的RestTemplate请求http://userservice/user/1
- RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
- DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求
Ribbon负载均衡策略
我们在上面的解释中提及到了一个词汇:
- IRule:负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类
我们给出一张IRule的继承图:
我们对其部分规则进行解释:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
其中默认的实现就是ZoneAvoidanceRule,是一种轮询方案
除此之外我们还可以去自定义实现负载均衡策略,下面我们来介绍两种实现方法:
- 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule
@Bean
public IRule randomRule(){
return new RandomRule();
}
- 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
Ribbon饥饿加载策略
我们的Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
我们可以采用代码去修改使其变为饿汉式加载:
ribbon:
eager-load:
enabled: true # 是否自动加载
clients: userservice # 针对的client
Nacos注册中心
国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos下载
Nacos是阿里巴巴的技术产品了,我们需要先对其进行下载才可使用:
- 打开官网,下载jar包(下载链接:https://github.com/alibaba/nacos)
- 下载后在全英路径下压缩,在bin路径下使用startup.cmd即可
# 跳转路径
cd 目录名
# 启动startup.cmd
startup.cmd -m standalone
# 这里注意:下载后默认路径8848,可以在conf的properties文件修改port
- 打开页面http://127.0.0.1:8848/nacos,账号密码都是nacos
Nacos服务注册
我们来介绍nacos的服务注册过程:
- 导入坐标
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.6.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
- 配置Nacos坐标
# 在user-service和order-service的application.yml中添加nacos地址:
# 同样和Eureka冲突,记得注释掉
spring:
cloud:
nacos:
server-addr: localhost:8848
- 启动并测试即可
Nacos服务分级储存模型
我们首先给出一张服务分级存储模型图并对其解释:
我们对其进行解释:
- 服务就是我们的消费者,实例就是我们的提供者
- 我们的提供者会被根据所在地点不同被划分到不同的集群中去
- 如果我们本地的服务采用本地集群的提供者来进行操作,由于路程近其速度也会更快
- 微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。
下面我们来介绍如何设置集群:
- 修改user-service的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
- 当我们重启其user-service实例后,我们可以在Nacos控制中心查看到相关集群
但是我们默认的ZoneAvoidanceRule无法实现同集群优先负载均衡操作,所以我们需要对其进行设置:
- 给order-service配置集群信息
# 修改order-service的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
- 修改负载均衡规则
# 修改order-service的application.yml文件,修改负载均衡规则:
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
Nacos权重配置
在我们的Nacos控制系统中的对象都会有一个权重设置:
我们对权重进行简单解释:
- 我们可以通过编辑按钮修改其权重大小
- 权重越大被使用的概率越高;权重越小被使用的概率越小
- 当我们将权重设置为0后,该服务器将不会在被使用,我们可以借此更新该服务器
Nacos环境隔离
Nacos提供了namespace来实现环境隔离功能:
- nacos中可以有多个namespace
- namespace下可以有group、service等
- 不同namespace之间相互隔离,例如不同namespace的服务互相不可见
首先我们先来了解如何在Nacos中新创namespace:
- 打开Nacos,页面跳转至namespace,点击右上角的新创空间
- 填入命名空间名和描述即可,注意命名空间ID,我们后续会使用
在新创命名空间之后,我们如果希望数据上传到指定命名空间,需要手动修改部分代码:
# 例如我们在order-service的application.yml文件中进行修改,那么后面的order服务就会到达新的命名空间中
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 478f4b7c-c1a5-4dee-a7c7-84774766654d # 命名空间,填ID
Nacos和Eureka
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异
我们首先给出Eureka的展示图:
我们再给出Nacos的展示图:
我们可以发现Nacos相比于Eureka有些许不同之处,首先是临时实例和非临时实例:
- 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型
- 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例
此外还有Nacos关于服务消费者的区别:
- 当Nacos中发生改变,会主动向服务消费者推送消息以加快消费者数据读取
最后我们给出Nacos和Eureka的相同点与不同点
Nacos与Eureka的共同点:
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
Nacos与Eureka的不同点:
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
Nacos配置管理
在前面我们学习了Nacos去完成微服务注册功能,下面我们来学习Nacos的配置管理功能
Nacos统一配置管理
首先我们先来学习Nacos中统一配置管理的基本内容:
- 打开Nacos网页,新创配置管理页面
- 进入新创页面后,书写相关信息
下面我们需要知道一些关于Nacos和application的相关信息:
- 上面我们书写的nacos热配置会和我们的application配置合并在一起,然后形成总配置
- 但是如果我们没有提前得知application.yaml配置文件,我们无法得知Nacos地址及服务名称,环境等信息
- 所以我们需要一个新的配置文件,就是我们目前的这个文件bootstrap.yaml文件,我们会在里面书写最基本的信息让其文件合并
下面我们继续开始统一配置管理的内容:
- 首先导入Nacos-config的依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
- 新创bootstrap.yaml文件,并书写基本信息
# 注:在userservice服务下
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
- 测试是否接收到Nacos配置
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 这里采用@Value注入配置信息,输出成功证明收到Nacos配置信息
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
Nacos配置热更新
我们首先来介绍一下热更新:
- 我们只需要在Nacos配置中修改配置,服务中的程序就会即时修改对应的配置信息
下面我们来介绍两种方法来实现热更新:
- 在使用@Value的类中采用注解注释
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope // 热更新注解,使用后Nacos的配置信息即时更新
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
- 单独创建一个类,用于存储Nacos热更新配置,并采用注解标识
// 配置属性实体类
@Component
@Data
@ConfigurationProperties(prefix = "pattern") // ConfigurationProperties表示热更新注解,prefix表示共用前缀
public class PatternProperties {
private String dateformat;
}
// 实时使用
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
// 略
}
Nacos配置共享
我们之前采用了特定环境配置,其具体表示为:
- 【服务名称】-【环境名称】.yaml
[spring.application.name]-[spring.profiles.active].yaml
,例如:userservice-dev.yaml
当我们不使用环境名称时,其配置就会变为共享配置:
- 【服务名称】.yaml
[spring.application.name].yaml
,例如:userservice.yaml
我们给出一个简单的示例:
- 首先在Nacos中创建新配置
- 在IDEA代码中书写对应的属性使用
// 配置属性实体类
@Component
@Data
@ConfigurationProperties(prefix = "pattern") // ConfigurationProperties表示热更新注解,prefix表示共用前缀
public class PatternProperties {
private String dateformat;
private String envSharedValue;
}
// Controller层
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties properties;
@GetMapping("prop")
public PatternProperties properties(){
return properties;
}
}
- 在IDEA中以不同的环境运行两个userService服务
// 在启动userService时
// 以Edit Configuration打开,并在Active profiles中修改名称以修改环境
Nacos配置管理优先级
最后我们给出配置管理的优先级展示:
- 当存在相同属性时,我们以下述顺序左侧为优,右侧为良
- Nacos(服务名-环境名.yaml)<- Nacos(服务名.yaml) <- 本地配置
Feign远程调用
下面我们来介绍一下Feign
Feign简单介绍
首先我们简单介绍一下Feign:
- Feign是一个声明式的http客户端
- 我们可以借助Feign来替代掉RestTemplate的复杂远程调用方法
我们这里回忆一下RestTemplate的远程调用方法:
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
// 需要手动书写url,并且加入id参数
String url = "http://userservice/user/" + order.getUserId();
// 需要调用restTemplate的固定方法并指定类class
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}
}
Feign快速入门
我们下面给出Feign的基本使用:
- 引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
- 启动类注解,表示开启Feign远程调用
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
- 编写Feign客户端
package cn.itcast.order.client;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 整体就是SpringMVC的REST风格
// @FeignClient类似RequestMapping,后面跟上具体的服务名
@FeignClient("userservice")
public interface UserClient {
// 这里就是具体的方法,采用REST
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
/*
这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:userservice
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
*/
- 测试
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getUserId());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
Feign自定义配置
下面我们来介绍一下Feign的自定义配置:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
大部分内容我们只需要使用默认就足以满足我们日常需求了
我们简单介绍一下Logger日志级别:
-
日志大体分为四种
-
NONE:不记录任何日志信息,这是默认值
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
我们给出两种修改默认配置的方法:
- 修改配置文件
# 修改yaml配置文件
# 可以针对某个微服务修改
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
# 也可以针对全部微服务修改
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
- Java代码方式
// 声明一个类,然后声明一个Logger.Level的对象
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
// 此外我们还需要将该类配置给该服务:
// 如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
// 如果是局部生效,则把它放到对应的@FeignClient这个注解中
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
Feign使用优化
Feign底层发起http请求,依赖于其它的框架,我们这里给出一些底层框架:
-
URLConnection:默认实现,不支持连接池
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
我们给出使用连接池的示例:
- 引入依赖
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
- 配置连接池
# 在对应的yml文件中配置连接池信息(这里就是order-service服务)
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
Feign使用技巧
我们可以发现Feign实际上和Controller的代码十分相似:
// Feign
@FeignClient("userservice")
public interface UserClient {
// 这里就是具体的方法,采用REST
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
// Controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth: " + truth);
return userService.queryById(id);
}
}
我们给出一种抽取方法来减少相同代码的书写:
-
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
-
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
我们给出具体实例:
- 抽取Feign内容,形成独立模块
- 引入对应的Feign依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
- 在order-service中使用feign-api
<dependency>
<groupId>cn.itcast.demogroupId>
<artifactId>feign-apiartifactId>
<version>1.0version>
dependency>
- 解决扫描包问题
/*
由于UserClient现在在cn.itcast.feign.clients包下,
而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient
所以我们需要手动扫描包,其中可以采用两种方法:
- 指定Feign应该扫描的包:@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
- 指定需要加载的Client接口:@EnableFeignClients(clients = {UserClient.class})
*/
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
GateWay服务网关
最后我们来介绍一下GateWay服务网关
GateWay简述
我们首先介绍一下GateWay:
- Gateway网关是我们所有微服务的统一入口
我们给出一张GateWay的示意图:
其中GateWay大致存在三种主要用途:
- 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
- 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
- 路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
关于网关大致包括两种:
- Zuul:基于Servlet的实现,属于阻塞式编程。
- SpringCloudGateway:基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
GateWay快速入门
下面我们通过一个简单的案例来介绍GateWay的基本使用:
- 创建SpringBoot工程gateway,引入网关依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
- 编写启动类
package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
- 编写基础配置和路由规则
# 路由配置包括
# 1. 路由id:路由的唯一标示
# 2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
# 3. 路由断言(predicates):判断路由的规则,
# 4. 路由过滤器(filters):对请求或响应做处理
# 我们将符合`Path` 规则的一切请求,都代理到 `uri`参数指定的地址。
# 本例中,我们将 `/user/**`开头的请求,代理到`lb://userservice`,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- 重启测试
/*
GateWay网关的port是10010
所以我们访问http://localhost:10010/user/1时,符合`/user/**`规则,请求转发到uri:http://userservice/user/1
*/
最后我们给出一张网关过程展示图:
GateWay断言工厂
下面我们来介绍一下断言:
- 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
我们下面给出几个简单的断言工厂(我们目前只需要PATH断言工厂即可):
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
GateWay过滤器工厂
我们先简单介绍一下GateWay过滤器:
- GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
我们给出一张GateWay过滤器展示图:
其中Spring提供了31种过滤器,这里仅仅介绍几种:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
然后我们给出过滤器的使用方法:
# 在yaml中进行过滤器配置,我们可以通过各种过滤器达到不同目的,例如添加请求头AddRequestHeader
# 我们可以采用服务uri路由名称单独给某个微服务设置过滤器
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
# 我们也可以采用全局过滤器对所有微服务进行过滤(default-filters)
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Itcast is freaking awesome!
GateWay全局过滤器
我们在前面学习了过滤器工厂,但是过滤器工厂只能实现已经设计好的方法
如果我们希望拦截业务来完成自己的功能增强或拦截,我们就需要设计过滤器:
- 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。
- 区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
全局过滤器的底层原理是实现了GlobalFilter接口:
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono} 返回标示当前过滤器业务结束
*/
Mono filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
我们给出一个简单的业务逻辑:
/*
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
- 参数中是否有authorization,
- authorization参数值是否为admin
*/
package cn.itcast.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Order(-1) // Order表示执行优先级,越小优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}
目前我们已经接触了三种过滤器:
- 当前路由的过滤器
- DefaultFilter
- GlobalFilter
最后我们需要思考GateWay过滤器的整体优先级:
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
- 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行
结束语
这篇文章中介绍了SpringCloud的整体框架及其知识点,属于微服务的入门内容,下面我们会继续学习微服务内容~
附录
该文章属于学习内容,具体参考B站黑马程序员的SpringCloud课程
这里附上视频链接:微服务技术栈导学1_哔哩哔哩_bilibili