Spring Could主要内容结构图:
简介
Spring Cloud Netflix将不再开发新的组件,进入维护模式。
我们都知道Spring Cloud版本迭代算是比较快的,因而出现了很多重大ISSUE都还来不及Fix就又推另一个Release了。进入维护模式意思就是目前一直以后一段时间Spring Cloud Netfli提供的服务和功能就这么多了,不在开发新的组件和功能了。以后将以维护和Merge分支Full Request为主。
Spring Cloud Alibaba的作用:
- 服务限流降级:默认支持Servlet、Feign、RestTemplate、Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics 监控。
- 服务注册与发现:适配 Spring Cloud服务注册与发现标准,默认集成了 Ribbon的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于Spring Cloud Stream为微服务应用构建消息驱动能力。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker (schedulerx-client)上执行。
Nacos服务注册和配置中心
名字的由来:前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。
Nacos是什么:
Nacos就是注册中心+配置中心的组合
相当于:Nacos = Eureka + Config + Bus
各注册中心的比较:
注意:Nacos支持AP和CP模式的切换
在父工程pom文件中引入alibaba 依赖,以下所有案例都基于该父工程
<!-- spring cloud alibaba 2.1.0.RELEASE -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Navos服务提供者注册
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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
yml文件:
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: "*"
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderMain9001 {
public static void main(String[] args) {
SpringApplication.run(NacosProviderMain9001.class,args);
}
}
controller:
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
public String serverPort;
@RequestMapping("/getPayment/{id}")
public String getPayment(@PathVariable("id") Integer id){
return "Alibaba Nacos server "+ serverPort+"-----"+id;
}
}
Nacos服务消费者的注册
pom文件:
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
yml文件:
server:
port: 83
spring:
application:
name: cloud-nacos-order
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称
server-url:
nacos-user-service: http://nacos-payment-provider
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class NacosOrderMain83 {
public static void main(String[] args) {
SpringApplication.run(NacosOrderMain83.class,args);
}
}
controller:
@RestController
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${server-url.nacos-user-service}")
private String url;
@GetMapping("/order/getPayment/{id}")
public String getPaymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(url+"/payment/getPayment/"+id,String.class);
}
}
配置类:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //负载均衡:轮询
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
注意:Nacos是默认整合了Ribbon的,能够自动实现负载均衡,所以需要添加@LoadBalanced 注解,否则会报错。
Nacos作为服务配置中心
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>
配置文件:
Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
bootstrap文件:
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: TEST_GROUP
namespace: 7a901d46-e75e-4e6a-b186-5980cca4249b
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
#nacos-config-client-dev.yaml
application文件:
spring:
profiles:
active: dev
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigNacosMain3377 {
public static void main(String[] args) {
SpringApplication.run(ConfigNacosMain3377.class,args);
}
}
controller
@RestController
@RefreshScope //支持nacos的动态刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
Nacos界面配置对应:
最后公式:
${spring.application.name}${spring.profiles.active}.$(spring.cloud.nacos.config.file-extension}
启动项目,成功获取配置文件
多环境多项目管理
问题1:
实际开发中,通常一个系统会准备
dev开发环境
test测试环境
prod生产环境。
如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:
一个大型分布式微服务系统会有很多微服务子项目,
每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境.
那怎么对这些微服务配置进行管理呢?
Namespace+Group+Data lD三者关系:
最外层的namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象。
默认情况:
Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
Nacos默认的命名空间是public,Namespace主要用来实现隔离。
比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
Service就是微服务;一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。比方说为了容灾,将Seryice微服务分别部署在了杭州机房和广州机房,
这时就可以给杭州机房的Service微服务起一个集群名称(HZ) ,
给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
最后是lnstance,就是微服务的实例。
## Nacos集群
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点(Nacos集群),数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
参考文章:
集群部署:link
集群配置:link
Sentinel实现熔断和限流
和Hystrix的对比
下载地址:
Sentinel分为两个部分:
- 核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对 Dubbo /Spring Cloud等框架也有较好的支持。
- 控制台(Dashboard)基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器。
启动本地Sentinel后
搭建服务,并将服务入驻Sentinel控制台(Dashboard)
pom文件:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</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>
yml文件:
server:
port: 8401
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: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
测试controler:
@RestController
public class FlowLimitController {
@RequestMapping("testA")
public String testA(){
return "testA-------";
}
@RequestMapping("testB")
public String testB(){
return "testB-------";
}
}
启动服务,观察Sentinel控制台
注意:Sentinel是懒加载模式,需要被监控的服务有请求后,才会显示监控信息。
监控规则说明
用于对每一个接口进行配置,规定接口的访问限制,如果不符合条件则调用失败。相当于将Hystrix在代码注解中的配置,在控制界面进行。
解释说明:
控流模式
直接模式流控
如下图的配置表示1秒钟内查询1次就是OK,若超过次数1,就直接-快速失败,报默认错误
关联模式流控
当关联的资源达到阈值时,就限流自己
比如当与A关联的资源B达到阀值后,就限流A自己。
上图的设置效果:
当关联资源/testB的qps阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名
控流效果
快速失败
默认的流控效果,直接返回失败
Warm Up
Warm Up ( RuleConstant.cONTROL_BEHAVIOR_NARA_up)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动"”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考流星控制- Warm Up文档,具体的例子可以参见
公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值
默认coldFactor为3,即请求QPS从(threshold / 3)开始,经多少预热时长才逐渐升至设定的QPS阈值。
案例:
阀值为10+预热时长设置5秒。系统初始化的阀值为10/3约等于3,即阀值刚开始为3;然后过了5秒后阀值才慢慢升高恢复到10
应用场景:
如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。
排队等待
匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。
设置含义:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒
应用场景:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一次直接拒绝多余的请求。
降级规则
和Hystrix相似,只是Sentinel是在监控界面进行配置,不需要在代码中使用注解进行配置。
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
注意:Sentinel的断路器是没有半开状态的,和Hystrix区别开,只要在窗口时间内没有触发新的熔断,窗口事件过后就会恢复。
半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,
有异常则继续打开断路器不可用。具体可以参考Hystrix
RT(平均响应时间,秒级)降级策略
平均响应时间超出阈值且在时间窗口内通过的请求>=5,两个条件同时满足后触发降级窗口期过后关闭断路器
RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)
案例:
假如一秒钟打进来10个线程(大于5个了)调用testD,我们希望200毫秒处理完本次任务,
如果超过20O0毫秒还没处理完,在未来1秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了
异常比列(秒级)降级策略
QPS >= 5且异常比例(秒级统计)超过阈值时,触发险级;时间窗口结束后,关闭降级
异常比例( DEGRADE_GRADE_EXCEPTION_RATIo):当资源的每秒请求量>=5,并且每秒异常总数占通过量的比值超过阈值( DegradeRule中的count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule中的timewindow,以s为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是[0.0,1.0],代表0%到100%。
案例:
当一秒钟内访问量超过5,且调用失败的比例超多百分之20,则触发降级一秒钟,一秒钟后关闭降级。
异常数(分钟级)降级策略
异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
时间窗口一定要大于等于60秒。
异常数(DEGRADE_GRADE_EXCEPTION_COUNT ):当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若timewindow小于60s,则结束熔断状态后可能再进入熔断状态。
案例:
http://localhost:8401/testE,第一次访问绝对报错,因为除数不能为零,我们看到error窗口,但是达到5次报错后,进入熔断后降级。
上述三种规则的测试代码:
@RequestMapping("testC")
public String testC(){
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "testC 测试RT-------";
}
@RequestMapping("testD")
public String testD(){
System.out.println("testD 异常比例测试");
int age = 10/0;
return "testD-------";
}
@RequestMapping("testE")
public String testE(){
System.out.println("testD 异常s数--测试");
int age = 10/0;
return "testD-------";
}
热点key限流(热点规则)
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的Top K数据,并对其访问进行限制。比如:
- 商品ID为参数,统计一段时间内最常购买的商品ID并进行限制
- 用户ID为参数,针对一段时间内频繁访问的用户ID进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
在1秒钟内,携带第一个参数的请求超过1一次,则进行服务降级,调用指定方法返回,没有指定则会返回错误页面
测试代码:
@RequestMapping("testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKet")
public String testHotKey(
@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2
){
System.out.println("testHotKey 热点Key--测试");
int age = 10/0;
return "testHotKey-------";
}
public String deal_testHotKet(String p1, String p2, BlockException e) {
return "----deal_testHotKet,------";
}
方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理
参数列外项:
参数列外项可以满足我们比较复杂的参数限流设置
有时候我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样特例
假如当p1的值等于5时,它的阈值可以达到200
注意:
@SentinelResource
处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
@RuntimeException
比如int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管
总结
@SentinelResource主管配置出错,运行出错该走异常走异常
系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 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达到阈值即触发系统保护。
@SentinelResource配置
注意:@SentinelResource不支持private方法
按资源名称限流
案例:
控制台配置:
接口代码:
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t服务不可用");
}
测试结果:
注意:如果服务关闭,对应的控制台配置规则也会消失
按照URL地址限流
案例:
控制台配置:
代码:
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200,"按URL限流测试OK",new Payment(2020L,"serial002"));
}
需要注意的是:这里没有指定blockHandler,则返回Sentinel自带的限流处理结果
客户自定义限流
上面兜底方案面临的问题:
- 系统默认的,没有体现我们自己的业务要求。
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
- 每个业务方法都添加一个兜底的,那代码膨胀加剧。
- 全局统—的处理方法没有体现。
创建CustomerBlockHandler类用于自定义限流处理逻辑
public class Customerhandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(444,"按客户自定义,global handlerException",new Payment(2020L,"serial003"));
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(444,"按客户自定义,global handlerException2",new Payment(2020L,"serial003"));
}
}
注意:没有注入,需要使用static修饰方法
在接口上进行注解配置:
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = Customerhandler.class, // 指定自定义类
blockHandler = "handlerException2") // 指定类中的方法
public CommonResult customerBlockHandler() {
return new CommonResult(200,"按客户自定义OK",new Payment(2020L,"serial002"));
}
服务熔断功能
配置fallback,处理业务异常
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常
public CommonResult<Payment> fallback(@PathVariable("id") 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;
}
public CommonResult handlerFallback(@PathVariable("id") Long id, Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult(444,"兜底异常blockHandler,exception内容"+e.getMessage(),payment);
}
同时配置fallback和blockHandler
public static final String SERVICE_URL="http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler")
public CommonResult<Payment> fallback(@PathVariable("id") 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;
}
public CommonResult handlerFallback(@PathVariable("id") Long id, Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult(444,"兜底异常handlerFallback,exception内容"+e.getMessage(),payment);
}
public CommonResult blockHandler(@PathVariable("id") Long id, BlockException exception) {
Payment payment = new Payment(id,"null");
return new CommonResult(445,"兜底异常blockHandler,exception内容"+exception.getMessage(),payment);
}
fallback只处理业务异常
blockHandler只处理控制台配置的限流异常
若blockHandler和fallback 都进行了配置,则被限流降级而抛出BlockException时只会进入blockHandler处理逻辑。也就是说在即发生了业务异常又流量又超过了控制的限流配置,则会进行blockHandler的处理。
对应一些特定的异常我们不想进行降级的可以使用exceptionsToIgnore 进行忽略,如下配置当发生IllegalArgumentException异常时会返回错误页面不会调用handlerFallback进行处理。
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler", exceptionsToIgnore = {IllegalAccessException.class}) //exceptionsToIgnore配置
public CommonResult<Payment> fallback(@PathVariable("id") 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;
}
public CommonResult handlerFallback(@PathVariable("id") Long id, Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult(444,"兜底异常handlerFallback,exception内容"+e.getMessage(),payment);
}
public CommonResult blockHandler(@PathVariable("id") Long id, BlockException exception) {
Payment payment = new Payment(id,"null");
return new CommonResult(445,"兜底异常blockHandler,exception内容"+exception.getMessage(),payment);
}
sentinel整合ribbon+openFeign+fallback
调用方配置:
yml文件:
server:
port: 84
spring:
application:
name: cloud-nacos-order
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
#消费者将要去访问的微服务名称
server-url:
nacos-user-service: http://nacos-payment-provider
feign:
sentinel:
enabled: true
config:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
Open 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<>(444,"服务降级返回---PaymentFallbackService",new Payment(id,"errorService"));
}
}
启动类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class NacosOrderMain84 {
public static void main(String[] args) {
SpringApplication.run(NacosOrderMain84.class,args);
}
}
服务方配置:
yml文件:
server:
port: 9004
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
application:
name: nacos-payment-provider
management:
endpoints:
web:
exposure:
include: '*'
controller:
@RestController
public class PaymentController {
@Value("${server.post}")
private String serverPost;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L,new Payment(1L,"000000000000000001"));
hashMap.put(2L,new Payment(2L,"000000000000000002"));
hashMap.put(3L,new Payment(3L,"000000000000000003"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult<>(200,"from mysql,serverPort:"+serverPost);
return result;
}
}
熔断框架对比:
Sentinel持久化规则
问题:
一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化
解决方案:
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台
的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效
详情参考:link
分布式事务Seata
分布式事务是什么
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,
业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据—致性问题没法保证。
案例:
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务∶对给定的商品扣除仓储数里。
- 订单服务:根据平购需求创建订单。
- 账户服:从用户帐户中扣除余额。
总结:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
Seata介绍
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
分布式事务处理过程 = 事务ID + 三组件模型
- TC-事务协调者心
维护全局和分支事务的状态,驱动全局事务提交或回滚。 - TM-事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。 - RM-资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
处理过程:
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
案例说明
这里创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
再通过远程调用账户服务来扣减用户账户里面的余额,
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
订单服务:
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>1.0.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>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
application.yml文件:
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://000.000.000.000:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath:mapper/*.xml
在application的同级目录下放入配置文件:
file.conf:
service {
#transaction service group mapping
vgroup_mapping.fsp_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#disable seata
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://000.000.000.000:3306/seata"
user = "root"
password = "123456"
}
}
registry.conf文件:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
service方法进行全局事务处理:
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
// name保证全局唯一即可
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
@Override
public void create(Order order) {
log.info("-------->开始创建新订单");
orderDao.create(order);
log.info("--------订单微服务开始调用库存,做扣减");
storageService.decrease(order.getProductId(),order.getCount());
log.info("-------订单微服务开始调用库存,做扣减end");
log.info("-------订单微服务开始调用账户,做扣减");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("-------订单微服务开始调用账户,做扣减end");
log.info("-------修改订单状态");
orderDao.update(order.getUserId(),0);
log.info("-------修改订单状态结束");
log.info("--------下订单结束了");
}
}
Seata原理:
三方关系:
Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造—站式的分布式解决方案。其中默认是AT模式。
AT模式
前提
- 基于支持本地ACID事务的关系型数据库。
- Java应用,通过JDBC访问数据库。
一阶段,Seata 会拦截“业务SQL”,
- 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
- 执行“业务SQL”更新业务数据,在业务数据更新之后,
- 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段
如是顺利提交的话,
因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,
Seata就需要回滚一阶段已经执行的“业务SQl,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比"数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。