springCloud-Alibaba

Spring-Cloud-Alibaba

Nacos(服务注册和配置中心)

一、安装

官网:https://nacos.io/zh-cn/

刚下完是集群模式,所以直接启动会显示"nacos is starting with cluster"

可以在bin/startup.cmd文件中修改:

set MODE="standalone"

修改完成后即可启动成功,在浏览器输入:

http://localhost:8848/nacos

即可进入nacos管理界面:

在这里插入图片描述

二、作为服务注册中心

以下配置均可在官网文档找到

1.创建两个module(端口:4001,4002)作为provider集群

  • pom

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
  • yml

    # 基本配置
    server:
      port: 4001   # 集群时,注意端口号不一致
    
    spring:
      application:
        name: payment-alibaba-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  • 主启动类:添加注解:@EnableDiscoveryClient

  • controller

    @RestController
    public class PaymentController {
    
        @Value("${spring.application.name}")
        private String name;
    
        @Value("${server.port}")
        private String port;
    
        // 返回服务名称和当前实例的端口号,便于测试负载均衡
        @GetMapping("/get/nacos/{id}")
        public String getNacos(@PathVariable(value = "id")int id){
            return "get id from nacos: " + name + "----port:" + port;
        }
    }
    

    完成后即可在管理界面看到(注意需要先启动nacos,在启动微服务):

    在这里插入图片描述

2.创建一个module(端口:81)作为consumer

  • pom,同上

  • yml,同上(注意微服务名称和端口)

  • 主启动类,同上

  • controller

    • 支持Restful,所以需要注入RestTemplate(代码略);因为nacos包含了ribbon,所以支持负载均衡,使用时与ribbon一样添加``@LoadBalanced`注解即可
    @RestController
    public class ConsumerController {
    // 此对象为官网使用方法,下面注释的方式配合该对象,可以不用添加@LoadBalanced注解也能开启负载均衡
    //    @Autowired
    //    private LoadBalancerClient loadBalancerClient;
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Value("${provider.service-url}")
        private String providerName;
    
    
    //    @GetMapping("consumer/get/nacos/{id}")
    //    public String getNacos(@PathVariable(value = "id")int id){
    //        ServiceInstance serviceInstance = loadBalancerClient.choose(providerName);
    //        return restTemplate.getForObject("http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() +
    //               " /get/nacos/" + id , String.class);
    //    }
    
        @GetMapping("consumer/get/nacos/{id}")
        public String getNacos(@PathVariable(value = "id")int id){
            return restTemplate.getForObject("http://"+providerName+"/get/nacos/"+id,String.class);
        }
    
    
    }
    

    在这里插入图片描述

Nacos支持AP和CP的切换,Eureka只支持AP

三、作为配置中心

1.快速入门

  1. 新建module(端口2001)

  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(application、bootstrap), springCloud也是这样

    bootstrap:

    server:
      port: 2001
    
    spring:
      application:
        name: config-center-alibaba
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848  # 服务注册
          config:
            server-addr: localhost:8848  # nacos作为配置中地址
            file-extension: yaml         # 指定配置文件类型
    
    

    application:

    spring:
      profiles:
        active: dev # 环境,与bootstrap中的配置可以结合起来
    
  4. 主启动类,注意添加EableDiscoverClient注解

  5. nacos配置中心创建配置文件:

    在这里插入图片描述

    在这里插入图片描述

    Data ID需要注意规范:
    ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

    即:微服务名称-环境.配置文件类型====》对应案例:config-center-alibaba-dev.yaml

  6. 测试:

    @RestController
    @RefreshScope
    public class ConfigController {
    
        @Value("${config.info}")
        private String info;
    
        @GetMapping("/get/config")
        public String getInfo(){
            return info;
        }
    }
    

    访问:http://localhost:2001/get/info 即可得到配置信息

    Nacos具有自动的动态更新配置的功能,在controller上添加@RefreshScope注解即可,不需要和springcloud一样通过bus发送广播更新

tips:docker 安装nacos

  1. 安装docker(不作解释)

  2. docker search nacos
    
  3. docker pull nacos/nacos-server  # 默认最新版
    
  4. docker images  # 查看是否安装成功
    
  5. docker run -d -p 8848:8848 --env MODE=standalone  --name nacos  nacos/nacos-server
    
  6. http://IP+8848/nacos  登录
    
  7. 注意修改微服务中的配置文件的ip

2.分类配置

NameSpace、Group、Data Id

相当于java中包和类的关系

在这里插入图片描述

新建命名空间和配置文件:

在这里插入图片描述

微服务服务配置:

server:
  port: 2001

spring:
  application:
    name: config-center-alibaba
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848  # 服务注册
      config:
        server-addr: localhost:8848  # nacos作为配置中地址
        file-extension: yaml         # 指定配置文件类型
        group: DEFAULT_GROUP         # 指定分组
        namespace: ca6f1c91-fb7e-4a46-bf91-7f210bcef489   # 指定命名空间

四、Nacos集群和持久化

1.准备:

Nacos集群需要使用mysql:因为Nacos自带有小型的嵌入式数据库,所以在在集群的时候每个nacos都会有自己的数据库,这样就会导致数据的不一致,所以要切换到mysql将其统一持久到一个mysql数据库中。(目前只支持mysql)

在windows中配置mysql为数据源:

在ncaos中的conf文件夹下有:

在这里插入图片描述

nacos-mysql.sql文件:里面是数据库建表的语句,直接cv创建即可(数据库名称自定义)

application.properties文件修改如下(只需要将注解打开即可,注意修改数据库名称和密码,docker环境下就不需要修改这个文件了):

spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root

配置完成后,启动naco并登录管理界面,就会看到原来的配置文件不见了,再新建的配置文件信息可以再MySQL的自建的数据库的config_info表中看到,即配置成功。

在mysql版本过高可能会出现建表失败(时间的错误)

2.naocs集群配置(这里使用docker)

在上面已经下载nacos的docker版,只需在linux中配置mysql即可(如上,这里也是使用docker的mysql)。

创建容器命令(需要三份分别为:8846、8847、8848):

docker run -d \
-e PREFER_HOST_MODE=hostname \
-e MODE=cluster \
-e NACOS_APPLICATION_PORT=8847 \
-e NACOS_SERVERS="192.168.1.118:8846 192.168.1.118:8847 192.168.1.118:8848" \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=182.168.1.118 \
-e MYSQL_SERVICE_PORT=3307 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=123456 \
-e MYSQL_SERVICE_DB_NAME=nacos_config \
-e NACOS_SERVER_IP=192.168.1.118 \
-p 8847:8847 \
--name my-nacos8847 \
nacos/nacos-server:1.4.2

简单解释:

  • 配置模式为:集群
  • nacos服务端口:8846
  • 集群的IP:3个
  • 使用数据库为mysql,在这里配置后,不需要修改application.properties文件了,将会自动绑定数据库
  • 数据库信息
  • 宿主机端口映射

**此时可以作个小测试:**

浏览器分别访问这三个nacos,在其中一个里面添加配置文件,刷新其他两个nacos管理界面,会出现同样的配置文件,即完成集群,且配置文件已经持久到mysql数据库中,可在数据库中查看。

注意:在登录管理界面时,可能出现默认账户密码错误,可以通过查看数据库中user表中是否有数据,大概率时没有导致的,只需要添加即可(密码是加密后的密码)

3.nginx配置(使用docker)

这里没有开启nginx的高可用

创建nginx容器:

docker run \
--name=nginx-MASTER \
-d -p 8000:80 \
-v $PWD/nginx.conf:/etc/nginx/nginx.conf:ro \
-v $PWD/conf.d:/etc/nginx/conf.d \
nginx

修改配置文件:

upstream cluster{         # nacos集群列表
        server 192.168.1.118:8846;
        server 192.168.1.118:8847;
        server 192.168.1.118:8848;
}
# server下
listen       80;     # nginx访问端口,docker映射为81:80
location / {
        proxy_pass http://cluster;
}

如上配置启动nginx,nacos;

访问:

http://192.168.1.118:81/nacos/#/lodin/

即可到nacos登录界面,且已经集群;在实际开发中,只能暴露nginx的虚拟ip,而nacos的IP都是不公开的。

遇到的坑:完成上述集群后,最后一步,需要注册微服务。但是始终无法注册进入nacos。调用服务注册api:

/nacos/v1/ns/instance

总会出现:server is DOWN now, please try again later!等字样,在java控制台中报错500或503.

查阅资料、官方文档、github上的issues,尝试过以下方法:

  • 修改docker中的nacos版本:latest、2.0.0、1.4.1
  • 设置linux的hostname即IP
  • 删除nacos里面的data中的protocol文件夹
  • 修改alibaba的依赖版本等

最终使用1.4.1版本使用/nacos/v1/ns/instance访问成功,并注册成功。具体原因还是没找到。个人认为是版本问题。在github上,官方说是由于(1)前置Filter拦截。(2)旧的jRaft的data数据会干扰本次jRaft的选举。总之还是使用RELEASE版本比较好

官网问题描述:https://github.com/alibaba/nacos/issues/5346

Sentinel(熔断与限流)

sentinal官网:

https://sentinelguard.io/zh-cn/

https://github.com/alibaba/Sentinel

功能:

在这里插入图片描述

下载:https://github.com/alibaba/Sentinel/releases/tag/1.8.1

下载的是一个jar包,所以世直接使用java -jar xxxx 直接运行即可,在浏览器访问http://localhost:8080可以看到dashboard(用户名密码默认为sentinel)

一、入门:对服务的监控

  1. 新建module

  2. 添加pom

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
    
  3. 配置yml

    server:
      port: 4003
    
    spring:
      application:
        name: alibaba-sentinel-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        # sentinel监控该服务
        sentinel:
          transport:
            dashboard: localhost:8080
            port: 8719
    
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
    
  4. 创建主启动类

  5. 创建controller(随意创建mapping)

  6. 测试:在nacos中注册成功;sentinel中没有:因为sentinel使用的是懒加载,当进行一次访问该服务时就会加载

在这里插入图片描述

二、流控规则

流控规则添加:

在这里插入图片描述

解释:

  • 资源名:微服务中的Rest地址

  • 针对来源:

  • 阈值类型:QPS:访问的数量;线程数:服务端的线程数量

    在这里插入图片描述

  • 单机阈值:QPS的请求阈值或线程数的数量

  • 是否集群:使用集群

  • 流控模式:

    1. 直接:达到限流条件时,直接限流自己

    2. 关联:A关联B,B达到设定阈值后,限流A自己

      如下:test1关联test2,当test2的请求QPS大于2时,test1就会被限流

      在这里插入图片描述

    3. 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)

  • 流控效果

    • 快速关闭:超过阈值直接返回错误
    • Warm up:预热,一个系统平时访问量很低,突然之间访问量暴增,超过阈值会使系统崩溃;使用预热留空会在设定时间内,将阈值提到上限;正常情况的阈值为:阈值上限/3 (3为预热因子,默认为3)
    • 排队等待:匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。

三、降级规则

在这里插入图片描述

1.慢调用比例策略

慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

在这里插入图片描述

如图:对于/get/test3这个请求资源,满足如下规则触发熔断:

  • 在1000ms的时间内,最少需要有5次请求
  • 在1000ms内,超出RT的请求(即慢调用请求)的比例为1(100%)

就会触发2s的熔断。

**测试:**使用JMETER测试,给定每秒钟10个请求,controller中的处理时间为1s(超过RT,所以阈值为100%),此时使用浏览器访问该资源,会出现错误 Blocked by Sentinel (flow limiting)

    @GetMapping("/get/test3")
    public String test3(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "-----test3----";
    }

2.异常比例

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

在这里插入图片描述

如图,对于test4资源:

设置的比例阈值为0.5(50%),即:

  • 当1s内,最少有5次请求
  • 且,异常的比例为50%以上

则触发熔断。这里需要注意:这里的异常指的是业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效

    @GetMapping("/get/test4")
    public String test4(){
        int a=1/0;    // 错误率100%
        return "-----test4----";
    }

3.异常数

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

在这里插入图片描述

异常数是按分钟为单位计数

如图:当窗口其内请求异常数超过10个就会触发熔断。

四、热点规则

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

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

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

在这里插入图片描述

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

1.初步使用

业务代码:

这里使用到了新的标签@SentinelResource,该注解只适用于sentinel配置的异常,如服务限流、熔断、热点限流等;后台的程序异常使用,如:RuntimeException等

    @GetMapping("/hotKeyTest")
    @SentinelResource(value = "hotKeyTest", blockHandler = "deal_hotKeyTest")
    public String hotKeyTest(
            @RequestParam(value = "p1", required = false)String p1,
            @RequestParam(value = "p2", required = false)String p2){

        return "-----hotKeyTest----";
    }
    
	// blockHandler异常处理的方法
    public String deal_hotKeyTest(String p1, String p2, BlockException blockException){
        return "deal_hotKeyTest,  request error";
    }

普通Sentinel配置:

在这里插入图片描述

解释:资源名为hotKeyTest,而不是"/hotKeyTest";仅支持QPS的限流方式;参数索引为0,即第一个参数为热点参数;阈值:即统计时长内的请求。

所以,对于上面的热点规则,当在1s内超过一次请求带有参数p1则会触发异常,调用blockHandler方法;如果不带参数p1则不会触发。

参数例外项配置:

当某个热点参数的值为某个特定的值的时候,给与使用特定的规则,如下:

在这里插入图片描述

热点参数为p1,当p1的值为java.String类型的"zhangsan"的时候,允许其阈值为5,即可以在1s内访问5次,而其他带有p1参数的请求只能1s访问1次。

五、系统规则

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

  • 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 达到阈值即触发系统保护。

如图:

在这里插入图片描述

当设置系统规则为入口QPS后,系统下的任何一个资源的访问大于1,都会被限流。

六、@SentinelResource

注解包含以下属性:

  • value:资源名称,必需项(不能为空)
  • entryType:entry 类型,可选项(默认为 EntryType.OUT)
  • blockHandler / blockHandlerClass: blockHandler 对应处理BlockException的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class`对象,注意对应的函数必需为 static 函数,否则无法解析。
  • fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable`类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass为对应的类的 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 逻辑中,而是会原样抛出。

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。

1.blockHandler / blockHandlerClass的使用

该属性用于BlockException异常的处理,即服务限流、降级相关的异常。

当出现这些异常的时候会使用指定的异常处理方法进行处理。配置如下(为了防止代码膨胀,将处理方法置于其他专门的类中):

controller:

    @GetMapping("/byResource/globalHandler")
    @SentinelResource(
            value = "globalHandler" ,  // 资源名称
            blockHandlerClass = CustomerBlockHandler.class,   // 指定异常处理所在类的class
            blockHandler = "globalHandler1")      // 异常处理方法名称
    public CommonResult globalHandler(){
        return new CommonResult(200,"request /byResource/globalHandler success");
    }

异常处理类:

// 异常处理类
public class CustomerBlockHandler {

    // 异常处理方法1
    public static CommonResult globalHandler1(BlockException e){
        return new CommonResult(444, "error by globalHandler1");
    }
    // 异常处理方法2
    public static CommonResult globalHandler2(BlockException e){
        return new CommonResult(444, "error by globalHandler2");
    }
    // 异常处理方法3
    public static CommonResult globalHandler3(BlockException e){
        return new CommonResult(444, "error by globalHandler3");
    }
}

注意:异常处理方法必须是public static修饰的,否则无法解析

因此,当对资源"globalHandler"限流的时候。超过阈值时就会进入"globalHandler1"方法并返回。

2.fallback / fallbackClass的使用

fallback用于处理程序运行时抛出的异常,测试如下:

@RestController
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @Value("${provider.service-url}")
    private String providerName;


    // 调用微服务查询结果
    @GetMapping("/consumer/get/payment/{id}")
    // fallback可以处理运行(业务)的异常
    // @SentinelResource(value = "fallback",fallback = "fallback_method")
    // blockHandler不能处理运行异常,只能处理sentinel的配置异常
    // @SentinelResource(value = "fallback",blockHandler = "blockHandle_method")
    // 同时可以处理sentinel配置异常,也可以处理业务异常
    @SentinelResource(value = "fallback",fallback = "fallback_method",blockHandler = "blockHandle_method")
    public CommonResult getPayment(@PathVariable(value = "id")Long id){
        CommonResult res = restTemplate.getForObject("http://" + providerName + "/get/payment/" + id, CommonResult.class);
        Object data = res.getData();
        if(data != null){
            return res;
        }else{
            throw new NullPointerException("用户不存在");
        }
    }

    // 再次强调:fallback函数和blockHandle函数的格式必须严格遵守官网要求
    public static CommonResult fallback_method(@PathVariable(value = "id")Long id,Throwable e){
        return new CommonResult(444,"请求失败:id为:" + id + " 的用户不存在 ");

    }

    // 再次强调:fallback函数和blockHandle函数的格式必须严格遵守官网要求
    public static CommonResult blockHandle_method(@PathVariable(value = "id")Long id, BlockException e){
        return new CommonResult(444,"请求失败:被限流限流了");
    }

}

上述代码对单独使用"fallback",单独使用"blockHandler",同时使用二者进行了测试,结果为:

  • 单独使用fallback,只能处理查询运行时抛出的异常
  • 单独使用blockHandler,只能处理sentinel配置后违规的异常
  • 同时使用,则所有异常都能处理,且当同时又两种异常发生时,会优先使用blockHandler的异常处理。

对于fallback,也可以将异常处理方法置于一个单独的类中,操作与1.一样。

3.defaultFallback

即:使用了@SentinelResource注解后,不添加fallback,则使用默认的方法;操作同上。

4.exceptionsToIgnore

忽略异常,即:当抛出的异常为指定的以场数时,不再做处理,将按照原样抛出;指定的参数为.class

七、服务熔断

Sentinel+OpenFeign实现服务熔断

  • POM添加依赖

                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-openfeign</artifactId>
                </dependency>
    
  • 修改yml配置

    # 开启sentinel与feign的结合
    feign:
      sentinel:
        enabled: true
    
  • 主启动类添加注解@EnableFeignClients

  • 添加OpenFeign接口和实现

    接口:

    @Component
    @FeignClient(value = "payment-alibaba-provider", fallback = FeignServiceImpl.class)
    public interface FeignService {
    
        @GetMapping("/get/payment/{id}")
        public CommonResult getNacos(@PathVariable(value = "id")int id);
    }
    

    实现:

    @Component
    public class FeignServiceImpl implements FeignService {
        @Override
        public CommonResult getNacos(int id) {
            return new CommonResult(444,"请求失败:id为:" + id + " 的用户不存在 ");
        }
    }
    
  • Controller

        @Autowired
        private FeignService feignService;
    
        @GetMapping("/consumer/get/byFeign/{id}")
        public CommonResult getByFeign(@PathVariable(value = "id")int id){
            return feignService.getNacos(id);
        }
    
  • 测试:在断开服务后,consumer访问服务将会被openFeign的实现类中的方法处理。

八、Sentinel规则持久化

Seata(处理分布式事务)

分布式事务问题:

在这里插入图片描述

微服务的开发需要用到多个数据库保存不同的数据信息,也有许多不同的微服务调用,所以就需要事务来控制数据的一致性,所有需要使用到分布式事务。

一、Seata的基本概念

官网:https://seata.io/zh-cn/

Seata有3个基本组成部分:

  • **事务协调器(TC):**维护全局事务和分支事务的状态,驱动全局提交或回滚。
  • **事务管理器TM:**定义全局事务的范围:开始全局事务,提交或回滚全局事务。
  • **资源管理器(RM):**管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。

seata由一个XID+三个管理器组成

Seata管理的分布式事务的典型生命周期:

  1. TM要求TC开始一项新的全局事务。TC生成代表全局事务的XID。
  2. XID通过微服务的调用链传播。
  3. RM将本地事务注册为XID到TC的相应全局事务的分支。
  4. TM要求TC提交或回退相应的XID全局事务。
  5. TC驱动XID对应的全局事务下的所有分支事务,以完成分支的提交或回滚。

在这里插入图片描述

二、seata的配置与启动

下载地址:https://github.com/seata/seata/releases/tag/v1.4.2

1.配置文件修改

  • seata\seata-server-1.4.2\conf\file.conf

    1.  修改数据库
    mode = "db"
    
    2.  设置数据库连接
    driverClassName = "com.mysql.cj.jdbc.Driver"
        url = "jdbc:mysql://localhost:3306/seata?rewriteBatchedStatements=true&serverTimezone=UTC&useSSL=false"
        user = "root"
        password = "root"
        
    3.   添加设置,以下代码原来的文件中没有,需添加,指定事务组;重点关注"admin-tx_group"的值
    service {
      #transaction service group mapping
      vgroupMapping.my_test_tx_group = "admin-tx_group"
      #only support when registry.type=file, please don't set multiple addresses
      default.grouplist = "127.0.0.1:8091"
      #degrade, current not support
      enableDegrade = false
      #disable seata
      disableGlobalTransaction = false
    }
    

    这里需要注意驱动的使用:本次使用的版本是1.4.2,在lib目录下由jdbc目录,将里面的8.0的驱动拖到lib目录下,并按照上述配置修改即可(不这样操作会导致无法连接数据库而启动失败)

  • seata\seata-server-1.4.2\conf\registry.conf

    1.registry.type=nacos
    2.registry.nacos.serverAddr="localhost:8848" #此处为nacos地址端口
    
  • 创建数据库

    创建数据库,名称为上述配置中的名称(任意)。

    创建表:sql脚本地址:https://github.com/seata/seata/tree/develop/script/server/db

    创建完成后:

    在这里插入图片描述

  • 测试:

    1.启动nacos

    2.启动seata

    3.注册成功

三、案例准备

在这里插入图片描述

在这里插入图片描述

  • 如图:当用户下单时,会在订单服务(order)创建一个订单,然后通过远程调用库存服务器来扣除库存;

  • 再通过远程调用账户服务来扣除用户账户余额;

  • 最后再订单服务中修改订单的状态为已完成。

1.创建数据库

  • order

    create table t_order(
    	id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    	user_id BIGINT(11) DEFAULT NULL,
    	product_id BIGINT(11) DEFAULT NULL,
    	count INT(11) DEFAULT NULL,
    	money DECIMAL(11,0) DEFAULT NULL,
    	status INT(1) DEFAULT NULL COMMENT '0:订单创建中 1:已完结' 
    )ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    
    -- 日志表
    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,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    
  • storage

    create table t_storage(
    	id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    	product_id BIGINT(11) DEFAULT NULL,
    	total INT(11) DEFAULT NULL,
    	used INT(11) DEFAULT NULL COMMENT '已用库存' ,
    	residue INT(11) DEFAULT NULL COMMENT '剩余'
    )ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    
    
    insert into seata_storage.t_storage(id,product_id,total,used,residue) values (1,1,100,0,100);
    
    select * from seata_storage.t_storage
    
      
    -- 日志表
    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,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    
  • account

    create table t_account(
    	id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    	user_id BIGINT(11) DEFAULT NULL,
    	total DECIMAL(11) DEFAULT NULL COMMENT '总额度度',
    	used DECIMAL(11) DEFAULT NULL COMMENT '已用额度' ,
    	residue DECIMAL(11) DEFAULT NULL COMMENT '剩余可用额度'
    )ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    
    
    insert into t_account(id,user_id,total,used,residue) values (1,1,10000,0,10000);
    
    select * from t_account
    
    -- 日志表
    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,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    
    

    日志表创建sql脚本:https://github.com/seata/seata-samples/blob/master/ha/src/main/resources/sql/undo_log.sql

2.order-Module创建

1.添加pom

    <dependencies>
        <!--feign组件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--nacos组件-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata组件-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <!--需要排除该依赖,再另外添加-->
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.4.2</version>
        </dependency>
        <!--spring boot 组件-->
        <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>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </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>

2.创建yml

server:
  port: 8011

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        tx-service-group: admin-tx_group   # 需要与seata的服务端设置的事务组名称一致
        service:
          vgroup-mapping:
            admin-tx_group: default  #key与上面的tx-service-group的值对应
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    password: root
    username: root

feign:
  hystrix:
    enabled: true

mybatis:            # 整合mybatis
  type-aliases-package: com.zjj.cloud01.domian    # 别名所在包
  mapperLocations: classpath:mapper/*.xml

3.添加seata配置文件的conf

  • 直接将seata的file.conf和registry.conf放入resources目录下即可

  • cong.file

    # 添加如下配置信息,
    service {
      #transaction service group mapping
      vgroupMapping.admin-tx_group = "default"
      #only support when registry.type=file, please don't set multiple addresses
      default.grouplist = "127.0.0.1:8091"
      #degrade, current not support
      enableDegrade = false
      #disable seata
      disableGlobalTransaction = false
    }
    

    tips:这里需要注意,当前使用的1.4.2本版seata。在下载下来后,所提供的file.config文件中不存在”service“这个配置项,这个配置项主要用于定义事务组的名称。在使用时,服务端的seata定义了事务组的名称(详见本章"二"配置的详细内容),而客户端需要配置与其一致的事务组名称(如上添加的配置项)。如不配置,可能会出现提示:can not get cluster name in registry config ‘…’ please make sure registry config correct 的错误

  • registry.conf

    主要修改注册类型为nacos,修改地址和ip

4.创建domain包

  • 创建order的实体类:Order
  • 创建结果返回类:CommonResult

5.创建mapper

@Component
@Mapper
public interface OrderMapper {
    // 添加订单
    void createOrder(Order order);

    // 修改订单状态
    void updateOrderStatus(@Param(value = "userId")Long userId, @Param(value = "status")Integer status);

}
<mapper namespace="com.zjj.cloud01.mapper.OrderMapper">

    <insert id="createOrder" parameterType="com.zjj.cloud01.domain.Order">
        insert into t_order values (null,#{userId},#{productId},#{count},#{money},0)
    </insert>

    <update id="updateOrderStatus">
        update  t_order set status = 1 where user_id = #{userId} and status = #{status}
    </update>

</mapper>

6.创建service

  • OrderServiceImpl

    public class OrderServiceImpl implements OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
    
        @Autowired
        private AccountService accountService;
    
        @Autowired
        private StorageService storageService;
    
        @Override
        public void createOrder(Order order) {
            log.info("开始添加订单================");
            orderMapper.createOrder(order);
    
            log.info("开始扣减库存=================");
            storageService.decrease(order.getProductId(),order.getCount());
            log.info("扣减库存结束================");
    
            log.info("开始扣减余额==================");
            accountService.decrease(order.getUserId(),order.getMoney());
            log.info("扣减余额结束================");
    
            log.info("开始修改订单的状态=============");
            orderMapper.updateOrderStatus(order.getUserId(), 0);
            log.info("修改订单的状态结束================");
    
            log.info("添加订单结束================");
    
        }
    }
    
  • AccountService

    @FeignClient(value = "seata-account-service")
    public interface AccountService {
    
        @PostMapping("/account/decrease")
        CommonResult decrease(@RequestParam(value = "userId")Long userId, @RequestParam(value = "money")BigDecimal money);
    }
    
  • StorageService

    @FeignClient(value = "seata-storage-service")
    public interface StorageService {
    
        @PostMapping("/storage/decrease")
        CommonResult decrease(@RequestParam(value = "productId")Long productId, @RequestParam(value = "count")Integer count);
    }
    

7.创建controller

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("order/create")
    public CommonResult createOrder(Order order){
        orderService.createOrder(order);
        return new CommonResult(200,"添加订单成功~");
    }
}

8.创建主启动类

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMain8011 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMain8011.class,args);
    }
}

9.创建config

  • MybatisConfig

    @Configuration
    @MapperScan({"com.zjj.cloud01.mapper.OrderMapper"})
    public class MyBatisConfig {
    }
    
    
  • DateSourceProxyConfig

    // 使用Seata对数据源管理
    @Configuration
    public class DateSourceProxyConfig {
    
        @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 sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSourceProxy);
            sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
            sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
            return sqlSessionFactoryBean.getObject();
        }
    }
    

3.storage-Module创建

步骤同上,只需要修改业务逻辑即可(修改库存操作)

4.account-创建

步骤同上,只需要修改业务逻辑即可(修改用户余额操作)

以上步骤全部完成后,测试:访问order,传入订单信息,将会相继调用微服务并修改数据库。

在这里插入图片描述

四、案例实践

当正常访问时,一切安好。

1.异常:

情况:当订单创建后(order表新建记录,状态为0),后面的任何一个环节出现故障,都会导致数据的不一致。

实例:修改AccountServiceImpl,为其设置超时(睡眠20s)

    @Override
    public void updateAccount(Long userId, BigDecimal money) {
        log.info("扣款开始===========");
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        accountMapper.updateAccount(userId,money);
        log.info("扣款完成===========");
    }

结果:当执行到account时,由于时间超时,所以在order端就会报错而不会执行下面的修改状态操作,最终导致用户余额减少,而订单状态处于未处理状态(数据不一致)。

在这里插入图片描述

2.解决

在OrderServiceImpl方法上添加@GlobalTransactional注解即可(在入口添加)

name属性值任取,rollbackFor表示何种情况回滚

	@Override
    @GlobalTransactional(name = "order-transaction", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        log.info("开始添加订单================");
        orderMapper.createOrder(order);

        log.info("开始扣减库存=================");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("扣减库存结束================");

        log.info("开始扣减余额==================");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("扣减余额结束================");

        log.info("开始修改订单的状态=============");
        orderMapper.updateOrderStatus(order.getUserId(), 0);
        log.info("修改订单的状态结束================");

        log.info("添加订单结束================");

    }

五、seata原理

在这里插入图片描述

ccountServiceImpl,为其设置超时(睡眠20s)

    @Override
    public void updateAccount(Long userId, BigDecimal money) {
        log.info("扣款开始===========");
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        accountMapper.updateAccount(userId,money);
        log.info("扣款完成===========");
    }

结果:当执行到account时,由于时间超时,所以在order端就会报错而不会执行下面的修改状态操作,最终导致用户余额减少,而订单状态处于未处理状态(数据不一致)。

[外链图片转存中…(img-FZGm2hHG-1620911972626)]

2.解决

在OrderServiceImpl方法上添加@GlobalTransactional注解即可(在入口添加)

name属性值任取,rollbackFor表示何种情况回滚

	@Override
    @GlobalTransactional(name = "order-transaction", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        log.info("开始添加订单================");
        orderMapper.createOrder(order);

        log.info("开始扣减库存=================");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("扣减库存结束================");

        log.info("开始扣减余额==================");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("扣减余额结束================");

        log.info("开始修改订单的状态=============");
        orderMapper.updateOrderStatus(order.getUserId(), 0);
        log.info("修改订单的状态结束================");

        log.info("添加订单结束================");

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值