# SpringCloudAlibabaTutorial
**Repository Path**: ylimhhmily/SpringCloudAlibabaTutorial
## Basic Information
- **Project Name**: SpringCloudAlibabaTutorial
- **Description**: 一站式速学 SpringCloudAlibaba 体系教程
- **Primary Language**: Java
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 22
- **Forks**: 28
- **Created**: 2023-04-05
- **Last Updated**: 2025-06-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# SpringCloudAlibaba 学习课程
## I、大纲
[TOC]
## II、简介
```markdown
一站式速学 SpringCloudAlibaba 体系自学课程,希望帮助更多想学的童鞋快速掌握 SCA 体系。
```
## III、各微服务占用端口情况
| 模块 | 微服务工程名 | 端口 | 功能描述 |
| -------------------------------------- | -------------------------------- | --------------------------------------- | :----------------- |
| sc01-eureka | eureka-server | 8761 | Eureka 服务端 |
| | eureka-provider | 9000 | Eureka 提供方 |
| | eureka-consumer | 9001 | Eureka 消费方 |
| sc02-nacos | nacos-server | 8848 | |
| | nacos-provider | 9010 | Nacos 提供方 |
| | nacos-consumer | 9011 | Nacos 消费方 |
| sc03-cluster | nacos-cluster-provider | 9020
9021 | Nacos 提供方 |
| | nacos-cluster-consumer | 9022
9023 | Nacos 消费方 |
| | nginx | 8888 | Nginx 反向代理 |
| | | | |
| sca01-nacos-regdis | nacos-registry-provider | 9030 | Nacos 提供方 |
| | nacos-discovery-consumer | 9031 | Nacos 消费方 |
| sca02-nacos-config | nacos-dynamic-port | 9032 | Nacos 动态配置端口 |
| | nacos-dynamic-env | 9033 | Nacos 动态环境变量 |
| sca03-openfeign | openfeign-provider | 9040
9041
9042 | Openfeign 提供方 |
| | ribbon-consumer | 9043 | Ribbon 消费方 |
| | openfeign-consumer | 9044 | Openfeign 消费方 |
| | degrade-consumer | 9045 | 降级消费方 |
| sca04-gateway | gateway-server | 9050 | 网关服务端 |
| | gateway-provider | 9051 | 网关提供方 |
| | gateway-consumer | 9052 | 网关消费方 |
| | gateway-filter-server | 9053 | 网关过滤器 |
| sca05-sentinel | sentinel-manual-flow-demo | - | 手动限流 |
| | sentinel-manual-degrade-demo | - | 手动降级 |
| | sentinel-dashboard | 9999 | 限流控制台 |
| | sentinel-auto-flowdeg-consumer | 9061 | 自动限流/降级消费方 |
| | sentinel-flow-degrade-provider | 9062 | 限流/降级提供方 |
| | sentinel-online-flowdeg-consumer | 9063 | 在线修改限流降级消费方 |
| | sentinel-nacos-persist-consumer | 9064 | 限流规则持久化消费方 |
| sca06-dubbo | zookeeper | - | Zookeeper 注册中心 |
| | dubbo-facade | - | dubbo 接口模块 |
| | dubbo-discovery-provider | 9070
29070 | dubbo 提供方 |
| | dubbo-discovery-consumer | 9075
29075 | dubbo 消费方 |
| | dubbo-check-consumer | 9076
29076 | dubbo 依赖检查消费方 |
| | dubbo-loadbalance-consumer | 9076
29076
29071
29072 | dubbo 负载均衡消费方 |
| | dubbo-broadcast-consumer | 9077
29077 | dubbo 广播调用消费方 |
| | dubbo-cache-consumer | 9078
29078 | dubbo 缓存操作消费方 |
| | dubbo-p2p-consumer | 9079
29079 | dubbo 点对点消费方 |
| | dubbo-generic-consumer | 9080
29080 | dubbo 泛化调用消费方 |
| |dubbo-jdk-spi|-|dubbo 和 jdk spi|
| |dubbo-wrapper|-|dubbo wrapper 机制|
## 一、体系介绍
### 1.1 组件介绍
1、解决了什么问题?
2、如何完整的阐述涉及到的组件?
#### 1.1.1 Nacos Registry
#### 1.1.2 Nacos Config
#### 1.1.3 OpenFeign
#### 1.1.4 Gateway
#### 1.1.5 Dubbo
#### 1.1.6 Sentinel
#### 1.1.7 Seata
### 1.2 SpringCloud 最佳实践
1、什么是最佳实践?
2、实践的又是什么?
#### 1.2.1 Provider + Consumer + Eureka 服务搭建
- 相关网址:
- SpringCloud 大致介绍:https://spring.io/projects/spring-cloud
- SpringCloud 版本文档:https://spring.io/projects/spring-cloud#learn(如何选择?)
- Eureka Server 启动
- Netflix:https://spring.io/projects/spring-cloud-netflix
-
- 引入包
```xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
```
- 添加应用启动类、添加Eureka服务端注解、设置Tomcat绑定端口、设置Eureka地址
- 设置 Tomcat 启动端口为 6000(**为什么不行?**)
- Eureka Provider 工程
- 引入包
```xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
```
- 添加应用启动类、添加Eureka客户端注解、设置Tomcat绑定端口、设置Eureka地址
- Eureka Consumer 工程
- 引入包
```xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
```
- 添加应用启动类、添加Eureka客户端注解、设置Tomcat绑定端口、设置Eureka地址
- 怎么进行负载均衡呢?
#### 1.2.2 Provider + Consumer + Nacos 服务搭建
- Nacos Server 启动
- Nacos 的官方文档:https://nacos.io/zh-cn/docs/quick-start.html
- Nacos 安装包下载:https://github.com/alibaba/nacos/releases/download/2.1.0/nacos-server-2.1.0.tar.gz
- Nacos 源码包下载:https://github.com/alibaba/nacos/archive/refs/tags/2.1.0.tar.gz
- Jar 包启动 & 源码启动
-
- Nacos Provider 工程
- 引入包
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
- 添加应用启动类、添加控制器、添加服务发现注解、设置Tomcat绑定端口、设置Nacos地址
- Nacos Consumer 工程
- 引入包
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
- 添加启动类、控制器、RestTemplate、远程调用方法、设置Tomcat绑定端口、设置Nacos地址
#### 1.2.3 Consumer + Provider + nginx 集群搭建
- Cluster Provider:多台节点
- Cluster Consumer:多台节点
- Nginx
- 下载地址:http://nginx.org/
- 访问地址:http://localhost:8888/
- 负载均衡处理、start nginx.exe、nginx.exe -s quit、tasklist /fi "imagename eq nginx.exe"
## 二、Nacos 注册与发现
### 2.1 Nacos 工程案例
#### 2.1.1 Nacos 服务注册案例搭建
- 源码地址:https://github.com/alibaba/spring-cloud-alibaba
- 版本说明:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
- 本地启动 Nacos-Server 工程
- 搭建 SpringCloudAlibaba 骨架的最佳实践
- nacos-registry-provider 关键 pom 引用
- ```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
#### 2.1.2 Nacos 服务订阅案例搭建
- nacos-discovery-consumer 关键 pom 引用
- ```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
- 发起远程调用
### 2.2 Nacos 注册流程源码分析
#### 2.2.1 Nacos Client 发起注册流程
- 怎么快速熟悉注册流程?
- 可以去看百度的一些介绍,通过别人的一些分析源码的思路来学习
- 直接去官网去搜索一些官方的资料,来学习一些流程
- 自己去琢磨,去研究,如果没有一套很好的方法体系的话,那么研究起来就会非常痛苦
- 异常堆栈能给什么启示?
- 注册流程:1.x 版本、2.x 版本
- AbstractAutoServiceRegistration#onApplicationEvent
- NacosServiceRegistry#register
- NamingClientProxyDelegate#register
- 1.x 为什么先注册心跳?
- 2.x 基于长连接?
#### 2.2.2 Nacos Server 处理注册流程
- V1:InstanceController
- Service#serviceMap
- Service#onDelete
- V2:InstanceRequestHandler
- serviceManager
- singletonRepository
- namespaceSingletonMaps
- clientManager
- publishers
- Client
- addServiceInstance
- removeServiceInstance
- 事件
- ClientEvent.ClientChangedEvent
- ClientOperationEvent.ClientRegisterServiceEvent
- MetadataEvent.InstanceMetadataEvent
### 2.3 Nacos 订阅流程源码分析
#### 2.3.1 Nacos Client 发起订阅流程
- 怎么理解订阅关键环节?
- 如何巧妙创造订阅异常?
- 第一步:启动 Nacos Server
- 第二步:启动 Nacos Provider
- 第三步:启动 Nacos Consumer
- 第四步:关闭 Nacos Server
- 第五步:向 Nacos Consumer 发起 HTTP 请求调用
- 订阅流程
- DynamicServerListLoadBalancer#updateListOfServers
- NacosServerList#getServers
- NamingClientProxyDelegate#subscribe
- scheduleUpdateIfAbsent
- 关注1.0与2.0分支分叉逻辑
#### 2.3.2 Nacos 心跳与剔除机制
- 心跳和剔除分别解决了什么问题?
- 心跳:Client 与 Server 之间的一个连接情况,维持Client与Server之间的联系,防止许久不联系的话,担心被Server删除干掉
- 剔除:解决的是,Server 这边很久都没感知到 Client 是否活跃的情况,因此需要将这些许久非活跃的数据删除掉,侧重于Server端的逻辑
- 强调这里讨论 1.0 版本非长连接的机制
- 心跳(客户端)
- 回顾注册环节提到的心跳代码片段
- NamingClientProxyDelegate#register
- BeatTask#run
- 剔除(服务端)
- 思考应该在哪里开启剔除监测?
- Service#init
- ClientBeatCheckTask#run
- deleteIp
- UDP 推送
- getPushService().serviceChanged(service)
## 三、Nacos 配置中心
### 3.1 Nacos Config 工程案例
#### 3.1.1 Nacos 动态更新端口案例搭建
- 官方文档:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
- 留个醒,重点关注,port 是不能随便修改的,一旦修改的话,虽然内存里面会直接变成最新的配置,但是端口提供的服务还是原来的旧端口
-
-
#### 3.1.2 Nacos 动态更新配置案例搭建
- 重点关注放在 bootstrap.yml
- ```yml
spring:
cloud:
nacos:
config:
extension-configs[0]:
dataId: nacos-dynamic-user.properties
refresh: true
extension-configs[1]:
dataId: nacos-dynamic-common.properties
refresh: true
extension-configs[2]:
dataId: nacos-dynamic-global.properties
refresh: true
```
-
### 3.2 Nacos 配置中心源码分析
#### 3.2.1 Nacos 客户端配置更新机制
- 如何模拟实时拉取效果?
- 根据配置返回内容反推源头
- 源码关键节点
- NacosConfigService
- ClientWorker 构造方法
- ConfigRpcTransportClient
- ClientWorker#executeConfigListen
- 堆栈流程
- 
#### 3.2.2 Nacos 服务端配置更新机制
- 找准核心操作源头 URL
- 精准定位到 ConfigController
- 源码关键节点
- ConfigController#publishConfig
- 发布 ConfigDataChangeEvent 事件
- 发布 LocalDataChangeEvent 事件
- LongPollingService 注册监听的回调
- DataChangeTask#run
### 3.3 Nacos 集群同步源码分析
#### 3.3.1 CP + AP 模式
- C(Consistency)A(Avaliability)P(Partition Tolerance)是什么?
-
- 如果想解决分区容忍的现象:意味着,需要将 dataA 进行复制给到其他节点
- 如果想解决复制的问题,但是又会引发复制过程带来的可用性问题
- 既然可用性也受损的话,那么是不是可以直接去掉分区概念
- 为什么一定得保证 P ?
- 既然去掉分区,那就意味着没有多节点的概念,从而意味着是单节点
- 单节点的好处就是,不用复制数据到其他节点,因为自己就是唯一,然后一致性得到了保证
- 但是如果,单节点发生的宕机,那是不是意味着可用性又受损了
- 如果想拥有可用性,那么又变相的反推需要多节点,那是不是又回到了分区的概念
- 因此呢,我们需要保证P,也就是需要对分区进行一定的容忍
- 而这个所谓的一定的容忍,可以是一段时间之内,也可以是一定数量之间,但是最终可以通过设计方案达到数据的最终一致
- CP 应用案例(价格)
- 一致性(强) + 分区容忍新
-
- AP 应用案例(点赞)
- 可用性 + 分区容忍性
-
- Nacos、Eureka、Zookeeper 典型模式
- Nacos:AP + CP
- Eureka:AP
- Zookeeper:CP
#### 3.3.2 Distro 一致性协议
- 新节点同步机制:com.alibaba.nacos.core.distributed.distro.DistroProtocol#startLoadTask
- 平等机制:com.alibaba.nacos.naming.web.DistroFilter
- 路由转发机制:com.alibaba.nacos.naming.web.DistroFilter
- 异步复制机制:com.alibaba.nacos.core.distributed.distro.DistroProtocol#sync(DistroKey, DataOperation, long)
- 健康检查机制:com.alibaba.nacos.core.distributed.distro.DistroProtocol#startVerifyTask
- 本地读机制:com.alibaba.nacos.naming.controllers.InstanceController#list
- 最最关键的环节:
- 每个 Nacos Server 都会处理一部分应该由自己负责的数据
- ```java
public boolean responsible(String responsibleTag) {
final List servers = healthyList;
if (!switchDomain.isDistroEnabled() || EnvUtil.getStandaloneMode()) {
return true;
}
if (CollectionUtils.isEmpty(servers)) {
// means distro config is not ready yet
return false;
}
// localAddress = 192.168.100.183:8848
String localAddress = EnvUtil.getLocalAddress();
// 获取起始索引值
int index = servers.indexOf(localAddress);
// 获取结束索引值
int lastIndex = servers.lastIndexOf(localAddress);
if (lastIndex < 0 || index < 0) {
return true;
}
// distroHash(responsibleTag) 计算出来的值与 servers.size() 求余后的结果,
// 若在 [index, lastIndex] 中则为当前节点负责的数据
int target = distroHash(responsibleTag) % servers.size();
return target >= index && target <= lastIndex;
}
```
- 每个 Nacos Server 都会存储所有 Nacos Client 端的所有数据
#### 3.3.3 Raft 一致性协议
-
- V1 版本的选举与同步
- 选举
- RaftCore
- 角色 LEADER、FOLLOWER、CANDIDATE
- MasterElection
- 同步
- HeartBeat
- V2 版本的选举与同步
- 选举
- com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl
- com.alibaba.nacos.naming.consistency.persistent.PersistentConsistencyServiceDelegateImpl#PersistentConsistencyServiceDelegateImpl
- com.alibaba.nacos.naming.consistency.persistent.impl.PersistentServiceProcessor#afterConstruct
- com.alibaba.nacos.core.distributed.raft.JRaftProtocol#addRequestProcessors
- com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup
- com.alipay.sofa.jraft.RaftGroupService#start(boolean)
- com.alipay.sofa.jraft.RaftServiceFactory#createAndInitRaftNode
- com.alipay.sofa.jraft.core.NodeImpl#init
- com.alipay.sofa.jraft.core.NodeImpl#electSelf
- com.alipay.sofa.jraft.core.NodeImpl#becomeLeader
- 同步
- com.alipay.sofa.jraft.ReplicatorGroup#addReplicator(com.alipay.sofa.jraft.entity.PeerId)
## 四、OpenFeign 负载均衡
### 4.1 OpenFeign 工程案例
#### 4.1.1 Ribbon 工程案例搭建
- Ribbon 是什么?
- Netflix 的开源项目,主要来提供关于客户端的负载均衡的能力。
- Ribbon
- Feign:Netflix,SpringCloud 的第一代 LB(负载均衡)客户端工具包
- OpenFeign:SpringCloud 自研,SpringCloud 的第二代LB(负载均衡)客户端工具包,扩展支持了 @RequestMapping、@GetMapping 等之类的注解的能力
-
- openfeign-provider 9040
- ribbon-consumer 9043
#### 4.1.2 OpenFeign 工程案例搭建
- 官网地址:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#netflix-feign-starter
- 引包
- @EnableFeignClients
- @FeignClient
- openfeign-consumer 9044
#### 4.1.3 负载均衡与降级案例搭建
- 负载均衡:openfeign-provider 9040 9041 9042
- 消费降级:
- https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker-fallback
-
- 方式一:直接实现接口
- 方式二:实现 FallbackFactory
- 工程搭建:degrade-consumer 9045
### 4.2 OpenFeign 源码分析
#### 4.2.1 @FeignClient 注解扫描机制
- 核心流程讲解
-
- 源码流程跟踪
- 扫描流程:org.springframework.cloud.openfeign.EnableFeignClients
- org.springframework.cloud.openfeign.FeignClientsRegistrar
- org.springframework.cloud.openfeign.FeignClientsRegistrar#registerBeanDefinitions
- org.springframework.cloud.openfeign.FeignClientsRegistrar#registerDefaultConfiguration
- org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
- org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents
- protected boolean isCandidateComponent(MetadataReader metadataReader)
- protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition)
- scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
- org.springframework.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration
- ```java
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
```
- org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClient
- org.springframework.cloud.openfeign.FeignClientFactoryBean + factoryBean.getObject()
- org.springframework.cloud.openfeign.FeignClientFactoryBean#getTarget
- org.springframework.cloud.openfeign.HystrixTargeter#target
- feign.ReflectiveFeign#newInstance
- ```java
public T newInstance(Target target) {
Map nameToHandler = targetToHandlersByName.apply(target);
Map methodToHandler = new LinkedHashMap();
List defaultMethodHandlers = new LinkedList();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
```
- 调用流程:feign.ReflectiveFeign.FeignInvocationHandler#invoke
- ```java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
```
- feign.SynchronousMethodHandler#invoke
- com.alibaba.nacos.client.naming.NacosNamingService#selectInstances
- 这就是 OpenFeign 首次发起远程调用时,最最最慢的核心原因
#### 4.2.2 Feign 的上下文隔离机制
- 入口:org.springframework.cloud.openfeign.FeignClientFactoryBean#feign
-
- getTarget() 所在对象的参数信息来源?
- 源码分析:
- org.springframework.cloud.openfeign.FeignClientFactoryBean#get
- org.springframework.cloud.context.named.NamedContextFactory#getInstance
- org.springframework.cloud.context.named.NamedContextFactory#getContext
- org.springframework.cloud.context.named.NamedContextFactory#createContext
- 含有 OpenFeign 的项目,就是比没有 OpenFeign 的项目会慢一点点的核心原因
- ```java
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
// 重点在这里,创建了一个迷你的上下文对象,并且刷新了迷你上下文对象进行实例化操作
context.refresh();
return context;
}
```
## 五、Gateway 网关路由
### 5.1 Gateway 路由案例搭建
- Zuul -> Gateway
- 官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
- 路径断言:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-path-route-predicate-factory
- 路径拦截:
- 请求前的地址:http://localhost:9050/gateway/web/consumer/helloworld/geek
- 请求前的地址:http://localhost:9050/gateway/web/provider/hello/geek
- 转换后的地址:http://localhost:9052/helloworld/geek
- uri: http://127.0.0.1:9052
- StripPrefix=3
- 怎么做到负载均衡?怎么探索源码找到负载均衡?
- org.springframework.cloud.gateway.filter.LoadBalancerClientFilter
- uri: lb://openfeign-provider
### 5.2 自定义拦截过滤
- 使用拦截
- 请求前的地址:http://localhost:9053/gateway/web/consumer/helloworld/geek
- 请求前的地址:http://localhost:9053/gateway/web/provider/hello/geek
- Header
- Query
- 查看对应的源码类:AbstractGatewayFilterFactory
- 自定义拦截
- 局部拦截
- 继承类:AbstractGatewayFilterFactory
- CostGatewayFilterFactory
- Cost=ON/OFF
- 借鉴:org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory
- 全局拦截
- 继承类:GlobalFilter
- LoggerGlobalFilter
- LOG=ON
- 借鉴:还是 org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory
### 5.3 转发重定向机制
- 思考寻找调试突破口
- **制造异常**
- 在必经之路的过滤器中进行断点
- 分析异常日志可以抓取哪些重要信息
-
- 在 ExceptionHandlingWebHandler 中添加断点
- 一边调试一边分析堆栈日志
- 找到 Gateway 框架体系处理的入口:org.springframework.http.server.reactive.ReactorHttpHandlerAdapter
- 构建网关上下文:org.springframework.web.server.adapter.HttpWebHandlerAdapter
- 遍历 Mapping 获取真实处理请求的 Handler:org.springframework.web.reactive.DispatcherHandler
- 构建过滤器链:org.springframework.cloud.gateway.handler.FilteringWebHandler
- 过滤器必经之路:org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain
- 负载均衡过滤器:org.springframework.cloud.gateway.filter.LoadBalancerClientFilter
- 利用 Netty 发送网络请求过滤器:org.springframework.cloud.gateway.filter.NettyRoutingFilter
- 官网调用流程抽象总结:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/images/spring_cloud_gateway_diagram.png
## 六、Sentinel 限流降级
### 6.1 Sentinel 工程案例
#### 6.1.1 Sentinel 手动设置限流/降级案例搭建
- 限流
- 官方文档:https://sentinelguard.io/zh-cn/docs/flow-control.html
- 并发数:触发阈值直接抛弃
- QPS
- 直接拒绝:触发阈值直接抛弃
- 冷启动:在一段时间内针对突发流量缓慢增长处理数量
- 匀速器:请求以均匀的速度通过;
- 降级
- 官方文档:https://sentinelguard.io/zh-cn/docs/circuit-breaking.html
- RT:统计时长内,请求总数大于预设请求数,且慢请求个数大于预设比例,则熔断拒绝一段时间
- 异常比例:统计时长内,请求总数大于预设请求数,且异常比例大于预比例,则熔断拒绝一段时间
- 异常数:统计时长内,请求总数大于预设请求数,且异常数大于预设数值,则熔断拒绝一段时间
#### 6.1.2 Sentinel 自动获取限流/降级案例搭建
- 整体架构
- Sentinel 控制台
- 官网文档:https://sentinelguard.io/zh-cn/docs/dashboard.html
- 控制台相关包下载地址:https://github.com/alibaba/Sentinel/releases/tag/2.0.0-alpha
- 可执行文件:https://github.com/alibaba/Sentinel/releases/download/2.0.0-alpha/sentinel-dashboard-2.0.0-alpha-preview.jar
- 源码包1:https://github.com/alibaba/Sentinel/archive/refs/tags/2.0.0-alpha.zip
- 源码包2:https://github.com/alibaba/Sentinel/archive/refs/tags/2.0.0-alpha.tar.gz
- 启动控制台
- java -Dserver.port=9999 -Dcsp.sentinel.dashboard.server=localhost:9999 -Dproject.name=sentinel-dashboar -jar sentinel-dashboard-2.0.0-alpha-preview.jar
- 打开控制台
- http://localhost:9999/
- 查看控制台
- 应用联动 Sentinel 限流
-
- 官网文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
- ```yaml
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:9999
```
- 联动 Sentinel 限流
-
- 联动 Sentinel 降级
-
#### 6.1.3 Sentinel 接入 Nacos 案例搭建
- 官方文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
-
- 动态数据源支持
- ```properties
spring.cloud.sentinel.datasource.flow.nacos.server-addr=localhost:8848
spring.cloud.sentinel.datasource.flow.nacos.data-id=sentinel-online-flowdeg-consumer-sentinel.properties
spring.cloud.sentinel.datasource.flow.nacos.group-id=DEFAULT_GROUP
spring.cloud.sentinel.datasource.flow.nacos.rule-type=flow
```
- 配置文件内容怎么写呢?
- Sentinel 控制台保存一下
- F12
- CV大法
- 利弊
- 利:简单方便,仅需配置中心即可完美动态更新限流降级配置
- 弊:不够直观,不知道怎么设置限流降级,对开发人员要求高
### 6.2 Sentinel 持久化操作
#### 6.2.1 探索 Sentinel 规则持久化机制
- 怎么寻找持久化突破口?
- ```yaml
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:9999
```
- 尝试断点,dashboard 都拿来在哪些地方用?
- com.alibaba.cloud.sentinel.SentinelProperties.Transport#getDashboard
- ```java
if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER))
&& StringUtils.hasText(properties.getTransport().getDashboard())) {
System.setProperty(TransportConfig.CONSOLE_SERVER,
properties.getTransport().getDashboard());
}
```
- com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration#init
- ```java
// earlier initialize
if (properties.isEager()) {
InitExecutor.doInit();
}
```
- 断点 + 代码分析
- com.alibaba.csp.sentinel.init.InitExecutor#doInit 添加断点后,发现没进来,为什么?
- 触发一次 Http 请求调用后,发现进入了 com.alibaba.csp.sentinel.init.InitExecutor#doInit 断点
- 循环逻辑代码块分析
- ```java
for (InitFunc initFunc : loader) {
RecordLog.info("[InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName());
insertSorted(initList, initFunc);
}
for (OrderWrapper w : initList) {
w.func.init();
RecordLog.info(String.format("[InitExecutor] Executing %s with order %d",
w.func.getClass().getCanonicalName(), w.order));
}
```
- 寻找到了 com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter#start 的相关收发数据逻辑
- 关键代码 1:开启服务端层面的线程
- ```java
socketReference = serverSocket;
executor.submit(new ServerThread(serverSocket));
success = true;
port = serverSocket.getLocalPort();
```
- 关键代码 2:从 ServerSocket 的 accept 方法收数据
- ```java
socket = this.serverSocket.accept();
setSocketSoTimeout(socket);
HttpEventTask eventTask = new HttpEventTask(socket);
bizExecutor.submit(eventTask);
```
- 数据源处理核心代码
- ```java
if (FLOW_RULE_TYPE.equalsIgnoreCase(type)) {
List flowRules = JSONArray.parseArray(data, FlowRule.class);
FlowRuleManager.loadRules(flowRules);
if (!writeToDataSource(getFlowDataSource(), flowRules)) {
result = WRITE_DS_FAILURE_MSG;
}
return CommandResponse.ofSuccess(result);
}
```
#### 6.2.2 联动 Sentinel 与 Nacos 的持久化实现
- 分析如何扩展数据源
- ```java
if (FLOW_RULE_TYPE.equalsIgnoreCase(type)) {
List flowRules = JSONArray.parseArray(data, FlowRule.class);
FlowRuleManager.loadRules(flowRules);
if (!writeToDataSource(getFlowDataSource(), flowRules)) {
result = WRITE_DS_FAILURE_MSG;
}
return CommandResponse.ofSuccess(result);
}
```
- 为什么 getFlowDataSource() 是 null 值?
- 如何给 getFlowDataSource() 赋值呢?
- 充分利用已有方法:com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry#registerFlowDataSource
- 充分利用 Spring 的特性
- org.springframework.beans.factory.InitializingBean
- @javax.annotation.PostConstruct
- 接着又如何得到 com.alibaba.csp.sentinel.datasource.WritableDataSource 实现类呢?
- 创建 com.alibaba.nacos.api.config.ConfigService 对象
- 调用 com.alibaba.nacos.api.config.ConfigService 进行发布数据
### 6.3 核心 SphU.entry 源码分析
#### 6.3.1 ProcessorSlotChain 过滤器链流程
- 如何找到限流降级的入口?
- 曾经无数次练习过的 Demo 入手:sca05-sentinel 内部的各个模块
- 找到 Sphu.entry 内的核心必经环节:CtSph#entryWithPriority
- 创建上下文:com.alibaba.csp.sentinel.context.ContextUtil#trueEnter
- 构建过滤器链:com.alibaba.csp.sentinel.CtSph#lookProcessChain
- com.alibaba.csp.sentinel.slotchain.SlotChainProvider#newSlotChain
- com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder#build
- ```properties
// NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot ->
// AuthoritySlot -> SystemSlot -> FlowSlot -> DefaultCircuitBreakerSlot -> DegradeSlot
```
- 从官网了解一些限流降级模块的作用:https://sentinelguard.io/zh-cn/docs/basic-implementation.html
- 过滤器链触发调用:
- ```java
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
```
#### 6.3.2 StatisticSlot 核心处理逻辑分水岭
- 分水岭的由来
- ```java
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
try {
// 先触发后续的过滤器
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 然后再做请求通过数累加、线程数累加等等
node.increaseThreadNum();
node.addPassRequest(count);
// 省略部分代码
} catch (PriorityWaitException ex) {
// 省略部分代码
} catch (BlockException e) {
// 省略部分代码
throw e;
} catch (Throwable e) {
// 省略部分代码
throw e;
}
}
```
- 核心校验逻辑
- com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
- ```java
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
Map> authorityRules = AuthorityRuleManager.getAuthorityRules();
if (authorityRules == null) {
return;
}
Set rules = authorityRules.get(resource.getName());
if (rules == null) {
return;
}
for (AuthorityRule rule : rules) {
if (!AuthorityRuleChecker.passCheck(rule, context)) {
// 校验不通过,抛出异常,并且将【来源 Origin】、【规则 rule】抛出去,便于识别哪条规则不通过了
throw new AuthorityException(context.getOrigin(), rule);
}
}
}
```
- com.alibaba.csp.sentinel.slots.system.SystemSlot
- ```java
public static void checkSystem(ResourceWrapper resourceWrapper, int count) throws BlockException {
// NPE 判断,如果为空,不做任何处理
if (resourceWrapper == null) {
return;
}
// Ensure the checking switch is on.
// 没有配置规则的话,那么就是处于 false 状态,并且直接返回,不做任何校验处理
if (!checkSystemStatus.get()) {
return;
}
// for inbound traffic only
// 并且该系统监控指标,只允许 IN 方向的控制,对于 OUT 方向的则不做任何校验处理
if (resourceWrapper.getEntryType() != EntryType.IN) {
return;
}
// 接下来就是一系列的指标判断,纯粹的比大小判断
// total qps
double currentQps = Constants.ENTRY_NODE.passQps();
if (currentQps + count > qps) {
throw new SystemBlockException(resourceWrapper.getName(), "qps");
}
// total thread
int currentThread = Constants.ENTRY_NODE.curThreadNum();
if (currentThread > maxThread) {
throw new SystemBlockException(resourceWrapper.getName(), "thread");
}
double rt = Constants.ENTRY_NODE.avgRt();
if (rt > maxRt) {
throw new SystemBlockException(resourceWrapper.getName(), "rt");
}
// load. BBR algorithm.
if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
if (!checkBbr(currentThread)) {
throw new SystemBlockException(resourceWrapper.getName(), "load");
}
}
// cpu usage
if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
throw new SystemBlockException(resourceWrapper.getName(), "cpu");
}
}
```
- com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
- ```java
public void checkFlow(Function> ruleProvider,
ResourceWrapper resource,
Context context,
DefaultNode node,
int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
Collection rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
// 具体的流控规则校验
if (!canPassCheck(rule, context, node, count, prioritized)) {
// 校验失败的话,则抛出异常
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
```
- com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
- ```java
void performChecking(Context context, ResourceWrapper r) throws BlockException {
// 如果没有降级规则的话,那么就直接返回,不需要做任何降级处理
List circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
return;
}
for (CircuitBreaker cb : circuitBreakers) {
if (!cb.tryPass(context)) {
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
}
}
}
```
## 七、Dubbo 服务调用框架
### 7.1 Dubbo 工程案例
- 官方文档:https://cn.dubbo.apache.org/zh-cn/overview/home/
- 如何从Alibaba工程代码一路找到Dubbo文档:https://github.com/alibaba/spring-cloud-alibaba
- 总体架构:https://cn.dubbo.apache.org/zh-cn/overview/core-features/service-discovery/
#### 7.1.1 Provider & Consumer 案例搭建
- 我们要搭建的工程架构是怎样的?
-
- 这里停顿一下,思考下,应该怎么正确的添加 pom 引用?
- ```xml
com.alibaba.cloud
spring-cloud-starter-dubbo
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
- 工程需要哪些模块?
- 需要怎么填写 yml 配置文件内容?
- 借鉴性参考:https://cn.dubbo.apache.org/zh-cn/overview/quickstart/java/spring-boot/
#### 7.1.2 依赖检查 & 负载均衡案例搭建
- 依赖检查
- check
- 验证 check = false 后,提供方启动成功了,那么消费方是否还能调用提供方成功?
- 第一步:启动消费方;
- 第二步:消费方其实会阻塞等待 20s;
- 第三步:当消费方在20s等待的过程中,立马启动提供方;
- 第四步:待提供方启动成功后,消费方这边就等待20s睡眠完成,然后再次发起远程调用。
- 负载均衡
- 搭建多套提供方
-
- loadbalance = roundrobin
- -Dserver.port=9070 -Ddubbo.protocol.port=29070
- -Dserver.port=9071 -Ddubbo.protocol.port=29071
- -Dserver.port=9072 -Ddubbo.protocol.port=29072
#### 7.1.3 广播调用 & 缓存操作案例搭建
- braodcast
-
- cache
-
#### 7.1.4 点对点调用案例搭建
- url = dubbo://IP地址:端口
- -Dcom.springcloudali.ms.facde.DemoService=dubbo://localhost:29070
#### 7.1.5 泛化调用案例搭建
- 模拟HTTP请求
- 协议:http vs dubbo
- 端口:443 vs 20880 --》 从注册中心拿
- 路径:相对地址 vs 类名/方法名/方法入参类型名
- 请求体:requestBody vs reqArgs
- 如何标识泛化形式调用
### 7.2 Dubbo 源码分析
#### 7.2.1 JDK SPI 与 Dubbo SPI 机制
- SPI:Service Provider Interface,“服务” 提供 “接口”
-
- JDK SPI:ServiceLoader.load
- 使用 load 方法频率高,容易影响 IO 吞吐和内存消耗。
- 使用 load 方法想要获取指定实现类,需要自己进行遍历并编写各种比较代码。
- Dubbo SPI:ApplicationModel.defaultModel().getExtensionLoader
- 增加缓存,来降低磁盘IO访问以及减少对象的生成。
- 使用Map的hash查找,来提升检索指定实现类的性能。
#### 7.2.2 Dubbo 服务发布流程
- 服务发布,到底发布了什么东西?
- 将自身信息注册到注册中心
- 开启端口服务
- 可以从哪些环节挖掘突破口?
- 在提供方这边,怎么写代码提供方服务?
- 
- **总结:**https://static001.geekbang.org/resource/image/2c/cd/2c4c58169731a6092afbdfa59aafcfcd.jpg

#### 7.2.3 Dubbo 服务订阅流程
- 巧妙利用对比思维摸出订阅流程
- 作用域
- @DubboService:作用提供方
- @DubboReference:作用在消费方
- 类的名称
- @DubboService:ServiceConfig
- @DubboReference:ReferenceConfig
- 辐射功能
- 提供方:注册自己、开启端口服务
- 消费方:注册自己、捞取订阅列表
- **总结:**https://static001.geekbang.org/resource/image/45/b5/45f6c0b365efc1f587f36336e95467b5.jpg

#### 7.2.4 Dubbo Wrapper 机制
- 泛化调用:接口类名、方法名、方法参数类名、业务请求参数
- 如何针对这些入参设计服务端接口?
- 简单方式
- 代理方式
- JDK 代理
- Cglib 代理
- 自定义代理
- Wrapper源码代理、测试代理效率
- **总结:**https://static001.geekbang.org/resource/image/99/c6/9939c9abb9879cd4c65c2yy7c11430c6.jpg

#### 7.2.5 Dubbo 协议编解码
- **抽象:**https://static001.geekbang.org/resource/image/4a/ef/4ab9241ef2acf486fdfd3b2a13e005ef.jpg
- Socket 发送数据 《----》 ServerSocket 接收数据
- 固定长度:发送 100 个字节
- 分隔符
- 定长+变长:特殊前缀(读取多少长度)+读取指定长度
- **总结:**https://static001.geekbang.org/resource/image/28/fb/28b10a1005ab622c8865e6693a17f8fb.jpg
