[阶段4 企业开发进阶] 7. 微服务--SpringCloud Alibaba

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.测试

  1. 启动nacos和9001
  2. 访问http://localhost:9001/payment/nacos/1

在这里插入图片描述

服务消费者注册和负载

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.测试

  1. 启动nacos、9001、9002、83
  2. 访问http://localhost:83/consumer/payment/nacos/1

服务注册中心对比

在这里插入图片描述

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模型控制台管理社区活跃度
EurekaAP支持低(2.X版本闭源)
ZookeeperCP不支持
ConsulCP支持
NacosAP支持

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 来配置。目前只支持 propertiesyaml 类型。

公式

# nacos-config-client-dev.yaml
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

在这里插入图片描述

在这里插入图片描述

测试

  1. 启动需要在nacos客户端-配置管理-配置管理模块下有对应的yaml文件
  2. 运行cloud-config-nacos-client-3377
  3. 调用接口查看配置信息 http://localhost:3377/config/info

自带动态刷新

修改Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新

分类配置

多环境多项目管理的问题

问题1

实际开发中,一个系统会准备

  • dev开发环境
  • test测试环境
  • prod生产环境

如何保证指定环境启动时服务能正确读取到Nacos相应环境的配置文件?

问题2

一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又会有相应的开发环境、测试环境、预发环境、正式环境等,

如何对这些微服务配置进行管理?

配置管理

在这里插入图片描述

命名空间

在这里插入图片描述

Namespace+Group+DataID

1.是什么

类似Java里面的包名和类名,最外层的namespace可以用于区分部署环境,Group和DataID逻辑上区分两个目标对象

2.三种情况

在这里插入图片描述

默认情况

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图形界面控制台新建配置文件

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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特性

在这里插入图片描述

Sentinel分两个部分

  • 核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对Dubbo/SpringCloud等框架也有较好的支持
  • 控制台(Dashboard)基于SpringBoot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器

Sentinel控制台安装步骤

  1. 下载地址

  2. 运行命令(前提Java8环境;8080端口)

    java -jar sentinel-dashboard-1.8.6.jar
    
  3. 访问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 流控规则

在这里插入图片描述

  • 资源名:唯一名称,默认请求路径
  • 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
  • 阈值类型/单机阈值:
    • QPS(每秒钟的请求数量):当调用该api的QPS达到阈值时,进行限流
    • 线程数:当调用该api的线程数达到阈值时,进行限流
  • 是否集群,不需要集群
  • 流控模式:
    • 直接:api到达限流条件时,直接限流
    • 关联:当关联的资源达到阈值时,就限流自己
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来时的流量,如果达到阈值,就进行限流)【api级别的针对来源】
  • 流控效果:
    • 快速失败::直接失败,抛异常
    • Warm Up:根据coldFactor(冷加载因子,默认3)的值,从阈值/coldFactor,经过预热时长,才达到设置的QPS阈值
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效

2.3 降级规则

在这里插入图片描述

基本介绍

RT(平均响应时间,秒级)

平均响应时间 超出阈值且在时间窗内通过的请求>=5,两个条件同时满足后触发降级,窗口期过后关闭断路器,RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)

异常比例(秒级)

QPS>=5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

Sentinel熔断器降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)

Sentinel断路器是没有半开状态

半开的状态系统自动检测是否有请求异常,没有异常就去关闭断路器恢复使用,有异常则继续打开断路器不可用

RT

在这里插入图片描述

异常比例 (ERROR_RATIO)

当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

在这里插入图片描述

异常数 (ERROR_COUNT)

当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

在这里插入图片描述

2.4 热点规则

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

在这里插入图片描述

代码

@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";
    }
}

配置

在这里插入图片描述

@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")

方法testHotKey里面的第一个参数只要QPS超过每秒一次,马上降级处理

参数例外项

特例情况

  • 普通: 超过1s后阈值马上被限流
  • 特殊:期望p1参数为某特殊值时,它的限流值和平时不一样

配置

在这里插入图片描述


@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.配置流控规则

在这里插入图片描述

测试流控后,将8401关闭,发现流控规则消失,无法持久化

按URL地址限流+后续处理

@GetMapping("/byUrl")
@SentinelResource(value = "byUrl", blockHandler = "handleException")
public CommonResult byUrl() {
    return new CommonResult(200, "按url限流测试成功", new Payment(2023L, "serial001"));
}

上述两种兜底方案存在问题

  1. 系统默认的,没有体现自己的业务要求
  2. 依照现有条件,自定义的处理方法和业务代码耦合在一起,不直观
  3. 每个业务方法都添加一个兜底的,造成代码膨胀
  4. 全局统一的异常处理方法没有体现

客户自定义限流处理逻辑

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/fallbackClassblockHandler 对应处理 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 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 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会被降级

熔断框架比较

SentinelHystrixresilience4j
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口(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业务规则配置

在这里插入图片描述

{
    “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个微服务提供支持:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

在这里插入图片描述

3.1 Seata简介

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

官网文档

发布说明

典型的分布式事务过程

分布式事务处理过程的ID+三组件模型

ID说明
Transaction ID XID全局唯一的事务ID
术语说明
TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚
TM (Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

处理过程

  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
  2. XID在微服务调用链路的上下文中传播
  3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
  4. TM向TC发起针对XID的全局提交或回滚
  5. TC调度XID下管辖的全部分支事务完成提交或回滚请求

在这里插入图片描述

3.2 快速入门

下载地址

在这里插入图片描述

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("*****************成功下单!!!");

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值