SpringCloud Alibaba
中文文档
开源代码
服务限流降级:默认支持Servlet、Feign、RestTemplate、Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics监控
服务注册与发现:适配Spring Cloud服务注册与发现标准,默认集成Ribbon的支持
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新
消息驱动能力:基于SpringCloudStream为微服务应用构建新消息驱动能力
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行
1 服务注册和配置中心Nacos
1.1 Nacos简介
Nacos:Dynamic Naming and Configuration Service
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos就是注册中心+配置中心的组合(Eureka+Config+Bus)
快速入门
安装运行成功后访问http://localhost:8848/nacos(默认账号密码为nacos)
1.2 Nacos作为服务注册中心
服务提供者注册
1.新建模块cloudalibaba-provider-payment-9001
2.pom
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.yml
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
4.主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}
5.业务
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "Nacos registry, server port: " + serverPort + "\t id" + id;
}
}
6.测试
- 启动nacos和9001
- 访问http://localhost:9001/payment/nacos/1
![在这里插入图片描述](https://img-blog.csdnimg.cn/d30e362537b24e5789349adfd6b4badb.png#pic_center)
服务消费者注册和负载
1.新建cloudalibaba-consumer-nacos-order-83
2.pom
3.yml
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
4.主启动
5.配置
由于nacos自带负载均衡,其包引入ribbon,故配置resttemplate
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
5.业务
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id)
{
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
6.测试
- 启动nacos、9001、9002、83
- 访问http://localhost:83/consumer/payment/nacos/1
服务注册中心对比
![在这里插入图片描述](https://img-blog.csdnimg.cn/d0ae5b72c6d241d899fac9e4a01653ac.png#pic_center)
Nacos支持AP和CP模式的切换
C是所有节点在同一时间看到的数据是一致的,而A的定义是所有的请求都会收到响应
一般来说,
如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么可以选择AP模式。当前主流的服务如SpringCloud和Dubbo服务,都适用于AP模式。AP模式为服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例
如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。CP模式下则支持注册持久化实例,此时则以Raft协议为集群运行模式,该模式下注册实例之前必须先注册微服务,如果服务不存在,则返回错误
# 模式切换
curl -X POST '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
服务注册与发现框架 |
CAP模型 |
控制台管理 |
社区活跃度 |
Eureka |
AP |
支持 |
低(2.X版本闭源) |
Zookeeper |
CP |
不支持 |
中 |
Consul |
CP |
支持 |
高 |
Nacos |
AP |
支持 |
高 |
1.3 Nacos作为服务配置中心
基础配置
1.新建cloudalibaba-config-nacos-client-3377
2.pom
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.yml
Nacos与SpringCloud Config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置后,才能保证项目的正常启动,SpringBoot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
bootstrap.yml
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
application.yml
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
4.主启动
5.业务
@RefreshScope:通过SpringCloud原生注解@RefreshScope实现配置的自动更新
@RestController
@RefreshScope //支持Nacos的动态刷新功能。
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
Nacos中匹配规则
Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则
在 Nacos Spring Cloud 中,dataId
的完整格式如下:
${prefix}-${spring.profiles.active}.${file-extension}
-
prefix
默认为 spring.application.name
的值,也可以通过配置项 spring.cloud.nacos.config.prefix
来配置。
-
spring.profiles.active
即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当 spring.profiles.active
为空时,对应的连接符 -
也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
-
file-exetension
为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension
来配置。目前只支持 properties
和 yaml
类型。
公式
# nacos-config-client-dev.yaml
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
![在这里插入图片描述](https://img-blog.csdnimg.cn/a5f6bd29a6934b8f82ff17cf0b7d8fa0.png#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/c549543d58da4a4eb37738b7ae7ac823.png#pic_center)
测试
- 启动需要在nacos客户端-配置管理-配置管理模块下有对应的yaml文件
- 运行cloud-config-nacos-client-3377
- 调用接口查看配置信息 http://localhost:3377/config/info
自带动态刷新
修改Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新
分类配置
多环境多项目管理的问题
问题1
实际开发中,一个系统会准备
- dev开发环境
- test测试环境
- prod生产环境
如何保证指定环境启动时服务能正确读取到Nacos相应环境的配置文件?
问题2
一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又会有相应的开发环境、测试环境、预发环境、正式环境等,
如何对这些微服务配置进行管理?
配置管理
![在这里插入图片描述](https://img-blog.csdnimg.cn/1ca268b2ed9f4b63b49d6f055b7944f6.png#pic_center)
命名空间
![在这里插入图片描述](https://img-blog.csdnimg.cn/99e73a0663e64ad0b29adc1bfe719e3e.png#pic_center)
Namespace+Group+DataID
1.是什么
类似Java里面的包名和类名,最外层的namespace可以用于区分部署环境,Group和DataID逻辑上区分两个目标对象
2.三种情况
![在这里插入图片描述](https://img-blog.csdnimg.cn/11bd383e8e1d458db2e95614497dbecf.png#pic_center)
默认情况
Namespace=public, Group=DEFAULT_GROUP,默认Cluster是DEFAULT
-
Nacos默认的命名空间是public,Namespace主要用于实现隔离
-
Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
-
Service就是微服务,一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分
-
Instance就是微服务实例
DataID方案配置
指定spring.profiile.active和配置文件的DataID来使不同环境下读取不同的配置
默认空间+默认分组+新建dev和test两个DataID
通过spring.profiile.active属性进行多环境下配置文件的读取
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
Group方案配置
通过Group实现环境区分,新建Group,在Nacos图形界面控制台新建配置文件
![在这里插入图片描述](https://img-blog.csdnimg.cn/d3574159d34441eaaadbd63b85efeccc.png#pic_center)
bootstrap+application
在config下增加一条group的配置,可以配置为DEV_GROUP或者TEST_GROUP
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP
Namespace方案配置
新建dev/test的Namespace
![在这里插入图片描述](https://img-blog.csdnimg.cn/661fd546c2f042b78845233db17f98df.png#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/d058108b73504a2890d507837a2c8f95.png#pic_center)
bootstrap.yml
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP
namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4
application.yml
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
1.4 Nacos集群和持久化配置
集群部署说明
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决该问题,Nacos采用集中式存储的方式来支持集群化部署,目前仅支持MYSQL的存储
[待补充]
2 服务熔断和限流Sentinel
参考文档
Sentinel特性
![在这里插入图片描述](https://img-blog.csdnimg.cn/dcf2d8dd920445f6819039a33230496c.png#pic_center)
Sentinel分两个部分
- 核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对Dubbo/SpringCloud等框架也有较好的支持
- 控制台(Dashboard)基于SpringBoot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器
Sentinel控制台安装步骤
-
下载地址
-
运行命令(前提Java8环境;8080端口)
java -jar sentinel-dashboard-1.8.6.jar
-
访问sentinel管理界面(账号密码均为sentinel)
2.1 初始化监控
启动Nacos http://localhost:8848/nacos
1.新建cloudalibaba-sentinel-service-8401
2.pom
3.yml
4.主启动
5.业务类
6.测试
启动8401,由于Sentinel采用懒加载
执行一次访问http://localhost:8401/testA和http://localhost:8401/testB
2.2 流控规则
![在这里插入图片描述](https://img-blog.csdnimg.cn/218351db811947bd9e6af3e8e3a15262.png#pic_center)
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型/单机阈值:
- QPS(每秒钟的请求数量):当调用该api的QPS达到阈值时,进行限流
- 线程数:当调用该api的线程数达到阈值时,进行限流
- 是否集群,不需要集群
- 流控模式:
- 直接:api到达限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来时的流量,如果达到阈值,就进行限流)【api级别的针对来源】
- 流控效果:
- 快速失败::直接失败,抛异常
- Warm Up:根据coldFactor(冷加载因子,默认3)的值,从阈值/coldFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
2.3 降级规则
![在这里插入图片描述](https://img-blog.csdnimg.cn/e521dcde3a3047b7ac416ab491e11d97.png#pic_center)
基本介绍
RT(平均响应时间,秒级)
平均响应时间 超出阈值且在时间窗内通过的请求>=5,两个条件同时满足后触发降级,窗口期过后关闭断路器,RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)
异常比例(秒级)
QPS>=5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
Sentinel熔断器降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)
Sentinel断路器是没有半开状态的
半开的状态系统自动检测是否有请求异常,没有异常就去关闭断路器恢复使用,有异常则继续打开断路器不可用
RT
![在这里插入图片描述](https://img-blog.csdnimg.cn/6cbb7dd5e5224343a3371d1e4d3508c1.png#pic_center)
异常比例 (ERROR_RATIO
)
当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
![在这里插入图片描述](https://img-blog.csdnimg.cn/63368a76653c44868d534dc3271080fa.png#pic_center)
异常数 (ERROR_COUNT
)
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
![在这里插入图片描述](https://img-blog.csdnimg.cn/46cf780f4095412db96b01a393e154fa.png#pic_center)
2.4 热点规则
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
![在这里插入图片描述](https://img-blog.csdnimg.cn/ba21d032f9214fb5be39e39813edf047.png#pic_center)
代码
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName()+"\t"+"...testB");
return "------testB";
}
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
return "------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException e) {
return "-------deal_testhotkey";
}
}
配置
![在这里插入图片描述](https://img-blog.csdnimg.cn/e8e96510138f4f7f8eb7d2426506df51.png#pic_center)
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
方法testHotKey里面的第一个参数只要QPS超过每秒一次,马上降级处理
参数例外项
特例情况
- 普通: 超过1s后阈值马上被限流
- 特殊:期望p1参数为某特殊值时,它的限流值和平时不一样
配置
![在这里插入图片描述](https://img-blog.csdnimg.cn/5e54b178f4a14b3f995ecc986ed78c34.png#pic_center)
@SentinelResource处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理,主管配置出错,运行时异常不管的
2.5 系统规则
参考文档
Sentinel 系统自适应过载保护从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
-
Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是 CPU cores * 2.5
。
-
CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
-
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
-
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
-
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
2.6 SentinelResource配置
参考文档
按资源名称限流+后续处理
1.启动Nacos
2.启动Sentinel
3.修改cloudalibaba-sentinel-service-8401
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名限流测试成功", new Payment(2023L, "serial001"));
}
public CommonResult handleException(BlockException ex) {
return new CommonResult(444, ex.getClass().getCanonicalName() + "\t服务不可用");
}
}
4.配置流控规则
![在这里插入图片描述](https://img-blog.csdnimg.cn/f23618e4344e47a5a9564ca0b3f003e9.png#pic_center)
测试流控后,将8401关闭,发现流控规则消失,无法持久化
按URL地址限流+后续处理
@GetMapping("/byUrl")
@SentinelResource(value = "byUrl", blockHandler = "handleException")
public CommonResult byUrl() {
return new CommonResult(200, "按url限流测试成功", new Payment(2023L, "serial001"));
}
上述两种兜底方案存在问题
- 系统默认的,没有体现自己的业务要求
- 依照现有条件,自定义的处理方法和业务代码耦合在一起,不直观
- 每个业务方法都添加一个兜底的,造成代码膨胀
- 全局统一的异常处理方法没有体现
客户自定义限流处理逻辑
1.创建CustomerBlockHandler类用于自定义限流处理逻辑
public class CustomerBlockHandler {
public static CommonResult handlerException1(BlockException ex) {
return new CommonResult(444, "按客户自定义,全局异常处理器----1");
}
public static CommonResult handlerException2(BlockException ex) {
return new CommonResult(444, "按客户自定义,全局异常处理器----2");
}
}
2.controller
@GetMapping("/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException1")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客户自定义", new Payment(2023L, "serial003"));
}
@SentinelResource 注解
注意:注解方式埋点不支持 private 方法。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource
注解包含以下属性:
-
value
:资源名称,必需项(不能为空)
-
entryType
:entry 类型,可选项(默认为 EntryType.OUT
)
-
blockHandler
/ blockHandlerClass
: blockHandler
对应处理 BlockException
的函数名称,可选项。blockHandler 函数访问范围需要是 public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
-
fallback
/fallbackClass
: blockHandler
对应处理 BlockException
的函数名称,可选项。blockHandler 函数访问范围需要是 public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
-
defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore
里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。
- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
-
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
1.8.0 版本开始,defaultFallback
支持在类级别进行配置。
注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException
)进行处理,不能针对业务异常进行处理。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。
sentinel主要有三个核心API
- SphU定义资源
- Tracer定义统计
- ContextUtil定义上下文
2.7 服务熔断
Sentinel整合Ribbon+OpenFeign+fallback
Ribbon系列
提供者9003/9004
1.新建cloudalibaba-provider-payment-9003/9004
2.pom
3.yml
4.主启动
5.业务
消费者84
1.新建cloudalibaba-consumer-nacos-order-84
2.pom
3.yml
4.主启动
5.业务s
目的
fallback负责运行时异常
blockHandler负责配置违规
若blockHandler和fallback都进配置,则被限流降级而抛出BlockException时只会进入blockHandler处理逻辑
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
//@SentinelResource(value = "fallback") //没有配置
//@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常
//@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
}
Feign系列
Feign组件一般是消费侧
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
}
}
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
测试84调用9003,此时9003微服务宕机,消费者84会被降级
熔断框架比较
|
Sentinel |
Hystrix |
resilience4j |
隔离策略 |
信号量隔离(并发线程数限流) |
线程池隔离/信号量隔离 |
信号量隔离 |
熔断降级策略 |
基于响应时间、异常比率、异常数 |
基于异常比率 |
基于异常比率、响应时间 |
实时统计实现 |
滑动窗口(LeapArray) |
滑动窗口(基于RxJava) |
Ring Bit Buffer |
动态规则配置 |
支持多种数据源 |
支持多种数据源 |
有限支持 |
扩展性 |
多个扩展点 |
插件形式 |
接口形式 |
基于注解的支持 |
支持 |
支持 |
支持 |
限流 |
基于QPS、支持基于调用关系的限流 |
有限的支持 |
Rate Limiter |
2.8 规则持久化
一旦重启应用,Sentinel规则将消失,生产环境需要将配置进行持久化
将限流规则持久化进Nacos进行保存,只要刷新8401某个rest地址,Sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上的Sentinel上的流控规则持续有效
步骤
修改cloudalibaba-sentinel-service-8401
pom
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
yml(添加Nacos数据源配置)
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
添加Nacos业务规则配置
![在这里插入图片描述](https://img-blog.csdnimg.cn/2ce3d442e88d44849730590dc342a1e0.png#pic_center)
{
“resource”:"/rateLimit/byUrl",
"limitApp":"default",
"grade":1,
"count":1,
"strategy":0,
"controllerBehavior":0,
"clusterMode":false
}
属性 |
说明 |
resource |
资源名称 |
limitApp |
来源应用 |
grade |
阈值类型,0表示线程数,1表示QPS |
count |
单机阈值 |
strategy |
流控模式,0表示直接,1表示关联,2表示链路 |
controllerBehavior |
流控效果,0表示快速失败,1表示Warm Up,2表示排队等待 |
clusterMode |
是否集群 |
启动8401,刷新Sentinel
3 分布式事务处理 Seata
分布式事务存在问题
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务保证,但是全局的数据一致性无法保证
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
![在这里插入图片描述](https://img-blog.csdnimg.cn/92356e5ba8f640c5b73fc2080a91a28d.png#pic_center)
3.1 Seata简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
官网文档
发布说明
典型的分布式事务过程
分布式事务处理过程的ID+三组件模型
ID |
说明 |
Transaction ID XID |
全局唯一的事务ID |
术语 |
说明 |
TC (Transaction Coordinator) - 事务协调者 |
维护全局和分支事务的状态,驱动全局事务提交或回滚 |
TM (Transaction Manager) - 事务管理器 |
定义全局事务的范围:开始全局事务、提交或回滚全局事务。 |
RM (Resource Manager) - 资源管理器 |
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 |
处理过程
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚
- TC调度XID下管辖的全部分支事务完成提交或回滚请求
![在这里插入图片描述](https://img-blog.csdnimg.cn/d14bb58689554d85865bf1d6acc482ab.png#pic_center)
3.2 快速入门
下载地址
![在这里插入图片描述](https://img-blog.csdnimg.cn/b2f9df77a17443f4b6bdad4524076824.png#pic_center)
seata-server-0.9.0解压到指定目录并修改conf目录下的file.com配置文件
-
先备份原始file.conf文件
-
主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息
-
file.conf
# service模块
service {
#vgroup->rgroup
vgroup_mapping.my_test_tx_group = "fsp-tx_group"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
# store模块
store {
## store mode: file、db
mode = "db"
....
新建数据量seata(表在文件里提供)
修改…\seata\conf里面的registry.conf(指定注册中心为nacos)
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
....
先启动Nacos(8848),再启动seata-server
3.3 订单/库存/账户业务数据库准备
使用教程
创建三个微服务(订单、库存、账户)
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成
(该操作跨越三个数据库,两次远程调用,明显存在分布式事务问题)
创建业务数据库
-
seata_order:存储订单的数据库
CREATE DATABASE seata_order;
USE seata_order;
CREATE TABLE t_order(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
count INT(11) DEFAULT NULL COMMENT '数量',
money DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
status INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
-
seata_storage:存储库存的数据库
CREATE DATABASE seata_storage;
USE seata_storage;
CREATE TABLE t_storage(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
total INT(11) DEFAULT NULL COMMENT '总库存',
used INT(11) DEFAULT NULL COMMENT '已用库存',
residue INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
-
seata_account:存储账户信息的数据库
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
total DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
used DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
residue DECIMAL(10,0) DEFAULT 0 COMMENT '剩余可用额度'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
对上述数据库表创建对应的回滚日志表
- 订单-库存-账户三个库各建各自的回滚日志表
-
…\environment\seata\conf目录下db_undo_log.sql
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3.4 订单/库存/账户业务微服务准备
业务需求:
下订单----减库存----扣余额----改(订单)状态
新建订单Order-Module
1.新建seata-order-service-2001
2.pom
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: *****
password: *****
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
4.file.conf
5.registry.conf
6.domain
CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
Order
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
7.dao
@Mapper
public interface OrderDao {
// 1. 新建订单
void create(Order order);
// 修改订单状态
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cyan.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.cyan.springcloud.alibaba.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create" parameterType="com.cyan.springcloud.alibaba.domain.Order">
INSERT INTO t_order(user_id, product_id, "count", money, status)
VALUES (null, #{userId}, #{productId}, #{count}, #{money}, 0);
</insert>
<update id="update" parameterType="com.cyan.springcloud.alibaba.domain.Order">
UPDATE t_order SET status = 1 WHERE user_id = #{userId} AND status = #{status};
</update>
</mapper>
8.service
public interface OrderService {
void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping("/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping("/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
@Override
public void create(Order order) {
// 1. 新建订单
log.info("*****************开始新建订单");
orderDao.create(order);
// 2. 扣减库存
log.info("*****************订单微服务开始调用库存扣减");
storageService.decrease(order.getProductId(), order.getCount());
log.info("*****************订单微服务调用库存扣减结束");
// 3.扣减账户
log.info("*****************订单微服务开始调用账户扣减");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("*****************订单微服务开始调用账户结束");
// 4. 修改状态
log.info("*****************开始修改订单状态");
orderDao.update(order.getUserId(), 0);
log.info("*****************修改订单状态结束");
log.info("*****************成功下单!!!");
}
}
9.controller
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "订单创建成功");
}
}
10.MyBatis和数据源代理配置
@Configuration
@MapperScan({"com.cyan.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
新建库存Storage-Module
新建账户Account-Module
1.新建seata-storage-service-2002
…一模一样
3.5 验证
正常下单
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
手动制造超时异常,当库存和账户金额扣减后,订单状态没有设置为已完成(没有从零变为一),由于feign的重试机制,账户余额还有可能被多次扣减
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
// 1. 新建订单
log.info("*****************开始新建订单");
orderDao.create(order);
// 2. 扣减库存
log.info("*****************订单微服务开始调用库存扣减");
storageService.decrease(order.getProductId(), order.getCount());
log.info("*****************订单微服务调用库存扣减结束");
// 3.扣减账户
log.info("*****************订单微服务开始调用账户扣减");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("*****************订单微服务开始调用账户结束");
// 4. 修改状态
log.info("*****************开始修改订单状态");
orderDao.update(order.getUserId(), 0);
log.info("*****************修改订单状态结束");
log.info("*****************成功下单!!!");
}
}