# 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