# sca-example
**Repository Path**: cjiangbo/sca-example
## Basic Information
- **Project Name**: sca-example
- **Description**: Spring Cloud Alibaba Example
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-05-10
- **Last Updated**: 2023-12-28
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
### 1、版本选择
参照:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
| spring-cloud | spring-cloud-alibaba | spring-boot | nacos |
| ------------ | -------------------- | -------------- | ----- |
| Hoxton.SR12 | 2.2.7.RELEASE | 2.3.12.RELEASE | 2.0.3 |
### 2、bootstrap & application
- bootstrap 是应用程序的父上下文,加载优先于 applicaton
- boostrap 里面的属性不能被覆盖
```xml
org.springframework.cloud
spring-cloud-starter-bootstrap
```
### 3、nacos
参照:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
#### 3.1、配置中心 - Data Id
```
dataId 格式
spring.profiles.active 存在时:
${prefix}-${spring.profiles.active}.${file-extension}
spring.profiles.active 不存在或为空时:
${prefix}.${file-extension}
```
- prefix:默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置
- spring.profiles.active:为当前环境对应的 profile,当 spring.profiles.active 为空时,dataId 的拼接格式变成 ${prefix}.${file-extension}
- file-exetension:配置内容的数据格式,通过配置项 spring.cloud.nacos.config.file-extension 来配置,如 properties、yaml 等
- 对应 IOC 组件需要添加 @RefreshScope,开启配置自动刷新
#### 3.2、Data Id & group & namespace
- nacos 默认 namespace 为 public,可以用于环境隔离如生产、测试、开发
- nacos 默认 group 为 DEFAULT_GROUP,可以用于服务分组
- 匹配顺序,先匹配 namespace 配置,再匹配 group,最后匹配 Data Id
#### 3.3、通讯协议
- 1.x 版本只支持 http
- 2.x 版本支持 grpc(长链接,减少链接频繁创建和销毁的过程)
### 4、ribbon
- 客户端负载均衡,超时、重试等
- 新版本 **SpringCloud(2021.x.x)** 已经用 **LoadBalancer** 替代了 **Ribbon**
#### 4.1、@LoadBalanced
通过 @LoadBalanced 注解赋予 RestTemplate 负载均衡
```java
@Configuration
public class RestConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
```
#### 4.2、负载均衡算法
- IRule:负载均衡策略父接口,核心方法为 choose,返回选择的实例,由具体子类实现
- AbstractLoadBalancerRule:辅助负责均衡策略选择合适的服务端实例
- RandomRule:随机选取,从提供服务的实例中随机选择,利用 Random 对象生成一个不大于服务实例总数的随机数,并将该数作为下标以获取一个服务实例
- RoundRobinRule:线性轮询,维护一个计数器,从提供服务的实例中按顺序选取,下标不断自增与服务总数取模作为服务实例的下标,连续10次没有取到服务,则会报一个警告No available alive servers after 10 tries from load balancer
- RetryRule:在 RoundRobinRule 的基础上添加重试机制,在指定的重试时间内,反复使用线性轮询策略来选择可用实例
- WeightedResponseTimeRule:对 RoundRobinRule 的扩展,维护实例的权重值,响应速度越快的实例选择权重越大,越容易被选择
- BestAvailableRule:继承自 ClientConfigEnabledRoundRobinRule,增加了根据 loadBalancerStats 中保存的服务实例的状态信息来过滤掉失效的服务实例,找出并发请求最小的服务实例,loadBalancerStats 可能为 null,此时采用父类的策略即线性轮询
- AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
- ZoneAvoidanceRule:**默认负载均衡算法**,区域敏感策略,根据服务实例所在的区域信息,将服务实例分组,在进行负载均衡时,尽量选择来自不同区域的服务实例,以降低某个区域出现故障的风险,如果所有的服务实例都在同一个区域内,则退化为 RoundRobinRule 算法
##### 4.2.1 WeightedResponseTimeRule
对 RoundRobinRule 的扩展,维护实例的权重值,**响应速度越快的实例选择权重越大**,越容易被选择
权重更新:
```java
// 1、WeightedResponseTimeRule 继承 RoundRobinRule
public class WeightedResponseTimeRule extends RoundRobinRule
// 2、WeightedResponseTimeRule 构造方法调用父类 RoundRobinRule 构造方法
public WeightedResponseTimeRule(ILoadBalancer lb) {
super(lb);
}
// 3、RoundRobinRule 构造方法,调用无参构造器,同时调用 setLoadBalancer 方法
public RoundRobinRule(ILoadBalancer lb) {
this();
this.setLoadBalancer(lb);
}
// 4、WeightedResponseTimeRule 重写父类 RoundRobinRule 的 setLoadBalancer 方法,调用父类 setLoadBalancer 方法做赋值,同时执行初始化方法 initialize
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
if (lb instanceof BaseLoadBalancer) {
name = ((BaseLoadBalancer) lb).getName();
}
initialize(lb);
}
// 5、initialize 开启一个定时任务,定时计算服务权重,计算任务具体查看 DynamicServerWeightTask,默认执行间隔 30s
void initialize(ILoadBalancer lb) {
if (serverWeightTimer != null) {
serverWeightTimer.cancel();
}
serverWeightTimer = new Timer("NFLoadBalancer-serverWeightTimer-" + name, true);
// serverWeightTaskTimerInterval 默认 30s
serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval);
// do a initial run
ServerWeight sw = new ServerWeight();
sw.maintainWeights();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
logger.info("Stopping NFLoadBalancer-serverWeightTimer-" + name);
serverWeightTimer.cancel();
}
}));
}
```
权重计算:
服务实例计算规则:
- **总响应时间 - 该实例实际响应时间 + 累加的权重**
- **累加的权重一开始为 0,每个实例计算时依次累加**
- **每个服务实例的实际区间宽度为:总响应时间 - 该实例实际响应时间**
- **累加权重只是为了划分区间**
- **服务实例的平均响应时间越短、权重区间的宽度就越大,服务实例被选中的概率就越高**
示例:
某个服务有四个实例,四个实例的平均响应实际分别为 A - 10、B - 50、C - 100、D - 200,则总响应时间为 10+50+100+200=360
对应每个实例的权重计算如下:
- **A:360 - 10 + 0 = 350 [0, 350]**
- **B:360 - 50 + 350 = 660 (350, 660]**
- **C:360 - 100 + 660 = 920 (660, 920]**
- **D:360 - 200 + 920 = 1080 (920, 1080]**
##### 4.2.2 AvailabilityFilteringRule
```java
@Override
public Server choose(Object key) {
int count = 0;
// 轮询获取服务实例
Server server = roundRobinRule.choose(key);
// 只判断 10 次
while (count++ <= 10) {
// 判断服务实例是否满足条件
if (predicate.apply(new PredicateKey(server))) {
return server;
}
server = roundRobinRule.choose(key);
}
// 如果未选择出实例,在轮询获取一次获取服务实例,备选策略
return super.choose(key);
}
```
- 先轮询 10 次获取服务实例,依次进行校验,满足则返回该实例,不满足则在轮询一次做备选策略
- 校验规则
- 排除故障节点
- 排除高负载节点,实例的并发请求数量 activeRequestCount 大于等于请求连接的阈值
##### 4.2.3 ZoneAvoidanceRule
**默认负载均衡算法**,根据服务实例的元数据信息(如 Zone、Region 等)将服务实例分组,然后根据各个分组中健康实例的数量和权重,计算每个分组的权重,在进行负载均衡时,优先选择权重高的分组,如果所有的服务实例都在同一个区域内,则退化为 RoundRobinRule 算法
权重计算因素:
- 健康实例的数量:ZoneAvoidanceRule 策略会统计每个可用区中健康实例的数量,并将健康实例数量作为该可用区权重的一部分,健康实例数量越多,该可用区的权重越高
- 可用区权重的配置:ZoneAvoidanceRule 策略也支持自定义的可用区权重配置,用户可以通过配置文件等方式,手动设置可用区的权重,从而影响负载均衡的结果
nacos 中配置服务实例区域信息:
```yaml
# metadata字段用于存储服务实例的元数据信息,zone字段配置区域信息
spring:
cloud:
nacos:
discovery:
metadata:
zone: "shanghai"
```
##### 4.2.4 NacosRule
###### 4.2.4.1 Ribbon 服务刷新机制
- 定期从服务注册中心拉取服务实例列表,并将其缓存在本地
- 默认情况下,Ribbon 每隔 30 秒会从服务注册中心刷新一次服务列表,这个时间间隔可以通过配置项来调整
- 当服务实例信息发生变化时,例如服务实例上线、下线或者状态变化等,服务注册中心会通知 Ribbon 刷新服务列表
- **注册中心通知机制是有一定的延迟的,尽管 Ribbon 支持基于事件驱动的服务列表更新机制,但是由于网络延迟、消息传递等原因,Ribbon 接收到通知的时间可能会有一定的延迟**
- Ribbon 提供了手动刷新服务列表的 API,可以通过调用该 API 来强制刷新服务列表
###### 4.2.4.2 NacosRule 服务刷新机制
- **NacosRule 利用了 Nacos 的长连接订阅机制来实现服务列表的实时更新**
- 当 NacosRule 启动时,它会向 Nacos 注册中心发起订阅请求,订阅指定服务的实例变化事件
- 当服务实例信息发生变化时,Nacos 会通过长连接向 NacosRule 发送通知,告知其服务实例信息发生变化
- NacosRule 接收到通知后,会自动更新本地缓存的服务列表
###### 4.2.4.3 同集群优先负载
- ribbon 默认的 ZoneAvoidanceRule 并不能实现根据同集群优先来实现负载均衡
- 负载策略需使用 com.alibaba.cloud.nacos.ribbon.NacosRule,可以实现优先从同集群中挑选实例
配置示例:
```yaml
# consumer-01 配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 集群配置
cluster-name: FZ
provider-02:
ribbon:
# provider-02 使用 NacosRule
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
# provider-02 第一个实例配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
cluster-name: FZ
# provider-02 第二个实例配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
cluster-name: FZ
# provider-02 第三个实例配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
cluster-name: XM
```
nacos 查看实例信息:
效果:
consumer-01 调用 provider-02 的接口优先调用集群 FZ 的节点,在集群 FZ 的节点都下线后在调用集群 XM 的节点
###### 4.2.4.4 权重配置
NacosRule 也支持权重配置,可在 nacos 控制台进行配置
###### 4.2.4.5 环境隔离
- nacos 通过 namespace 实现环境隔离
- 不同 namespace 下的实例不能互相调用
```yaml
# consumer-01 配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 集群配置
cluster-name: FZ
namespace: 6c6f78ee-8ce2-4ab5-95cf-4b44b4da033a
provider-02:
ribbon:
# provider-02 使用 NacosRule
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
# provider-02 第一个实例配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
cluster-name: FZ
namespace: 6c6f78ee-8ce2-4ab5-95cf-4b44b4da033a
# provider-02 第二个实例配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
cluster-name: FZ
namespace: 6c6f78ee-8ce2-4ab5-95cf-4b44b4da033a
# provider-02 第三个实例配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
cluster-name: XM
namespace: a55c750f-12aa-4b15-9d97-66bae3602e84
```
#### 4.3、自定义负载策略
##### 4.3.1 实现 IRule 接口,自定义负载策略
```java
/**
* 自定义 ribbon 负载策略,只使用第一个实例
*
* @author cjiangbo
* @date 2023/5/1
*/
public class MyRule implements IRule {
private ILoadBalancer loadBalancer;
@Override
public Server choose(Object key) {
return loadBalancer.getAllServers().get(0);
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.loadBalancer = lb;
}
@Override
public ILoadBalancer getLoadBalancer() {
return loadBalancer;
}
}
```
##### 4.3.2 注入自定义负载策略
注:该配置类的不能让 spring 扫描到,ribbon 有自己的上下文,**spring 自动扫描将作用于全局**
```java
/**
* 注入 MyRule 负载策略
*
* @author cjiangbo
* @date 2023/5/1
*/
@Configuration
public class RibbonExtConfig {
@Bean
public IRule getRule(){
return new MyRule();
}
}
```
##### 4.3.3 修改 ribbon 默认负载策略
- 定义任意配置类,通过 @RibbonClient 指定负载策略,此处使用启动类
- @RibbonClients(defaultConfiguration = MyRule.class) 作用于全局
- @RibbonClient(name = "provider-01", configuration = MyRule.class) 也可通过 name 指定特定服务
```java
/**
* 启动类
*
* @author cjiangbo
* @date 2023/4/30
*/
@EnableDiscoveryClient
@SpringBootApplication
// 作用于全局
// @RibbonClients(defaultConfiguration = MyRule.class)
// 作用于某个服务
@RibbonClient(name = "provider-01", configuration = MyRule.class)
public class Consumer01Application {
public static void main(String[] args) {
SpringApplication.run(Consumer01Application.class, args);
}
}
```
注:@RibbonClient(name = "provider-01", configuration = MyRule.class) 等价于如下 yml 配置
```yaml
provider-01:
ribbon:
NFLoadBalancerRuleClassName: MyRule
```
### 5、Feign & OpenFeign
- Feign 是一个声明式的客户端负载均衡器,采用的是基于接口的注解,整合 Ribbon 具有负载均衡的能力,整合 Hystrix,具有熔断的能力
- OpenFeign 是 SpringCloud 在 Feign 的基础上支持 SpringMVC 的注解,如 @RequestMapping 等
#### 5.1、使用
```java
// 1.通过 @FeignClient("provider-01") 定义远程调用,"provider-01" 为服务名
/**
* provider-01 远程调用
*
* @author cjiangbo
* @date 2023/5/2
*/
@FeignClient("provider-01")
@RequestMapping("/provider01")
public interface Provider01Service {
@GetMapping("/info")
public String getInfo();
}
// 2.通过 @EnableFeignClients 配置 @FeignClient 扫描路径,默认为启动类路径,若有差异需调整
/**
* 启动类
*
* @author cjiangbo
* @date 2023/4/30
*/
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.onde.sca.api")
@SpringBootApplication
public class Consumer01Application {
public static void main(String[] args) {
SpringApplication.run(Consumer01Application.class, args);
}
}
// 3.调用方直接注入使用即可
/**
* OpenFeign 测试类
*
* @author cjiangbo
* @date 2023/4/30
*/
@RestController
@RequestMapping("/feign")
public class OpenFeignController {
@Resource
private Provider01Service provider01Service;
@GetMapping("/provider01/info")
public String getProvider01Info() {
return provider01Service.getInfo();
}
}
```
#### 5.2、配置说明(日志配置为例)
##### 5.2.1 bean 配置 - 全局
@Configuration 标注且在 Spring 的扫描包下将作用于全局
```java
/**
* Feign 配置
*
* @author cjiangbo
* @date 2023/5/2
*/
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
```
注:
日志等级
- NONE:无日志
- BASIC:只记录请求方法和URL以及响应状态代码和执行时间
- HEADERS:记录基本信息以及请求和响应标头
- FULL:记录请求和响应的标头、正文和元数据
##### 5.2.2 bean 配置 - 局部
可指定具体服务,不使用 @Configuration 标注且不在 Spring 的扫描包下
```java
package com.onde.sca.api.config;
/**
* Feign 配置,Spring 扫描包为 com.onde.sca.consumer01,不会扫描到此 bean
*/
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.NONE;
}
}
/**
* provider-01 远程调用
* 通过 @FeignClient(value = "provider-01", configuration = FeignConfig.class) 指定配置,作用于某个服务上
*/
@FeignClient(value = "provider-01", configuration = FeignConfig.class)
@RequestMapping("/provider01")
public interface Provider01Service {
@GetMapping("/info")
String getInfo();
}
/**
* 启动类
* 也可通过 @EnableFeignClients(basePackages = "com.onde.sca.api", defaultConfiguration = FeignConfig.class)
* 指定默认配置,作用于全局
*/
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.onde.sca.api", defaultConfiguration = FeignConfig.class)
@SpringBootApplication
public class Consumer01Application {
public static void main(String[] args) {
SpringApplication.run(Consumer01Application.class, args);
}
}
```
##### 5.2.3 属性配置
```yaml
# 作用于全局
feign.client.config.default.loggerLevel: full
# 作用于指定服务
feign.client.config.<服务名>.loggerLevel: none
```
##### 5.2.4 配置优先级
全局 bean 配置 < 指定服务 bean 配置 < 全局属性配置 < 指定服务属性配置
重试配置、日志配置、OpenFeign 拦截器 RequestInterceptor 等配置方式均一致
#### 5.3、http 客户端选择
- 默认使用 java 原生的 URLConnection
- 可选择 httpclient 或 okhttp
httpclient :
```xml
io.github.openfeign
feign-httpclient
12.3
```
```yaml
# 开启 httpclient
feign:
httpclient:
enabled: true
```
okhttp:
```xml
io.github.openfeign
feign-okhttp
12.3
```
```yaml
# 开启 okhttp
feign:
okhttp:
enabled: true
```
#### 5.4、超时配置
- feign.client.config.default.xxx:全局配置,针对所有服务的配置
- feign.client.config.<服务名>.xxx:针对单个服务的配置
- 单个服务的配置优先于全局配置
```yaml
feign:
client:
config:
# 针对所有的服务
default:
# Feign的连接建立超时时间,默认为10秒
connectTimeout: 3000
# Feign的请求处理超时时间,默认为60秒
readTimeout: 3000
# 针对单个服务,此处针对服务"provider-02"
provider-02:
# Feign的连接建立超时时间,默认为10秒
connectTimeout: 3000
# Feign的请求处理超时时间,默认为60秒
readTimeout: 6000
```
#### 5.5、重试
- 默认不重试
- 在进行重试时,OpenFeign 会通过负载均衡器选择下一个可用的服务实例进行重试,如果使用的是 Ribbon 负载均衡器,那么在重试时,Ribbon 会根据预先配置的负载均衡策略选择下一个服务实例,因此,负载均衡策略在重试时仍然会生效
```java
// 默认配置在 org.springframework.cloud.openfeign.FeignClientsConfiguration
// Retryer.NEVER_RETRY 永不重试
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
```
```java
/**
* Feign 重试配置
*
* @author cjiangbo
* @date 2023/5/2
*/
@Configuration
public class FeignConfig {
@Bean
public Retryer feignRetry(){
/**
* period=100 请求重试时间间隔,单位毫秒
* maxPeriod=1000 请求重试最大时间间隔,单位毫秒
* maxAttempts=2 重试次数是 1,因为包括第一次,如果想要重试 2 次,就需要设置为 3
* 说明:
* period 表示第一次重试和第二次重试之间的时间间隔,
* 第二次重试和第三次重试之间的时间间隔是 period 的两倍,
* 第三次重试和第四次重试之间的时间间隔是 period 的四倍,
* 以此类推,直到时间间隔达到 maxPeriod 为止
* 示例:
* 如果 period 设置为 100 毫秒,
* maxPeriod 设置为 5000 毫秒,
* 并且最大重试次数为 3 次。
* 第一次重试和第二次重试之间的时间间隔为 100 毫秒,
* 第二次重试和第三次重试之间的时间间隔为 200 毫秒,
* 只重试两次
*/
Retryer retryer = new Retryer.Default(100, 1000, 2);
return retryer;
}
}
```
#### 5.6、数据压缩说明
- 在 OpenFeign 10.x 版本之前,OpenFeign 是支持数据压缩的,并且可以通过在请求头中设置 Accept-Encoding: gzip 来启用数据压缩,但是,在 OpenFeign 10.x 版本之后,OpenFeign 不再支持数据压缩,相关的配置也被去除了
- 数据压缩可能会导致一些问题
- 在负载均衡的情况下,由于负载均衡器无法获取压缩后的数据大小,可能会导致负载均衡器选择不合适的服务实例,从而影响系统的性能
- 在使用 HTTPS 协议时,由于 TLS 1.2 及以下版本的协议存在 CRIME 攻击漏洞,使用压缩数据可能导致数据泄露的风险
```yaml
feign:
# Feign请求响应压缩配置
compression:
request:
# 开启Feign请求压缩,默认不开启
enabled: true
# 配置支持压缩的MIME TYPE,默认为:text/xml,application/xml,application/json
mime-types: text/xml,application/xml,application/json
# 触发请求数据压缩的最小Size,默认2048KB
min-request-size: 2048
response:
# 开启Feign响应压缩,默认不开启
enabled: true
# 使用GZip解码器,默认不使用
useGzipDecoder: false
```
#### 5.7、OpenFeign 拦截器 RequestInterceptor
- RequestInterceptor:可以用来在发送请求之前或之后对请求进行拦截和处理
- 可以添加请求头,例如 Authorization、User-Agent 等,方便对请求进行身份验证或者标识
- 修改请求参数,例如对请求参数进行加密、签名等操作,或者根据不同的场景动态地修改请求参数
- 记录请求日志,例如请求的 URL、请求头、请求体等,方便后续的排查和调试
- 缓存请求结果,**中断请求可通过抛异常处理,注意异常可能会引发重试**
- 统计请求次数和耗时
- 高版本新增 ResponseInterceptor,可以对响应进行拦截和处理,此示例版本(feign-core 10.12)不支持 ResponseInterceptor
```java
/**
* 自定义 OpenFeign 拦截器,实现 RequestInterceptor 接口
*
* @author cjiangbo
* @date 2023/5/2
*/
public class MyOpenFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Enumeration headerNames = request.getHeaderNames();
if (Objects.nonNull(headerNames)) {
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
System.out.println("key - " + key);
if(StringUtils.isNotBlank(key)){
System.out.println("value - " + request.getHeader(key));
}
}
}
}
}
```
```yaml
# 添加配置,可参考5.2配置说明
feign:
client:
config:
# 针对所有的服务
default:
# 自定义拦截器
requestInterceptors:
- com.onde.sca.consumer01.config.MyOpenFeignRequestInterceptor
```
### 6、Seata
https://juejin.cn/post/7164254193362927624