尚硅谷 周阳老师 SpringCloud 学习笔记

十八、SpringCloud Alibaba Sentinel实现熔断与限流

1. Sentinel

官网:https://github.com/alibaba/Sentinel
中文文档

是什么
分布式系统的流量防卫兵,Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Hystrix与Sentinel比较:

  • Hystrix
  1. 需要程序员自己手工搭建监控平台
  2. 没有一套web界面可以给我们进行更加细粒度化得配置流控、速率控制、服务熔断、服务降级
  • Sentinel
  1. 单独一个组件,可以独立出来
  2. 直接界面化的细粒度统一配置

去哪下
https://github.com/alibaba/Sentinel/releases
能干嘛
尚硅谷 周阳老师 SpringCloud 学习笔记_第1张图片
怎么玩
官方文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
服务使用中的各种问题:服务雪崩、服务降级、服务熔断、服务限流

2. 安装Sentinel控制台

Sentinel组件由两部分构成:

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

安装步骤:

  1. 下载Sentinel到本地
  2. 运行命令
    前提: Java8 环境OK,8080端口不能被占用,也可以指定端口启动
    命令:
java -jar sentinel-dashboard-1.7.1.jar
  1. 访问sentinel管理界面 http://localhost:8080
    尚硅谷 周阳老师 SpringCloud 学习笔记_第2张图片
    登录账号密码均为sentinel
    也可以修改端口启动:
java -jar sentinel-dashboard-1.7.1.jar --server.port=8888

尚硅谷 周阳老师 SpringCloud 学习笔记_第3张图片

3.初始化演示工程
3.1 启动Nacos8848
3.2 新建Module
3.2.1 新建cloudalibaba-sentinel-service8401
3.2.2 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020artifactId>
        <groupId>org.examplegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloudalibaba-sentinel-service8401artifactId>
    <dependencies>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.cspgroupId>
            <artifactId>sentinel-datasource-nacosartifactId>
        dependency>

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>4.6.3version>
        dependency>
    dependencies>

project>
3.2.3 application.yml
server:
  port: 8401
spring:
  application:
    name: cloudalibaba-sentinel-service

  cloud:
    nacos:
      discovery:
        #Nacos 服务注册中心地址
        server-addr: localhost:8848

    sentinel:
      transport:
        dashboard: localhost:8080  #配置sentinel dashboard地址
        port: 8719 #默认8719端口,假如被占用会自动从8719开始依次加1扫描,直到找到未被占用的端口

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    sentinel:
      enabled: true #加了此配置,不加此配置监控页面无显示
3.2.4 主启动类
package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class);
    }
}

3.2.5 业务类

controller

package com.atguigu.springcloud.alibaba.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FlowLimitController {
    @GetMapping("/testA")
    public String testA() {
        return "-----------A";
    }

    @GetMapping("/testB")
    public String testB() {
        return "-----------B";
    }
}
3.3 启动Sentinel8080
java -jar sentinel-dashboard-1.7.1.jar

尚硅谷 周阳老师 SpringCloud 学习笔记_第4张图片

3.4 启动微服务8401

nacos管理页面信息:
尚硅谷 周阳老师 SpringCloud 学习笔记_第5张图片

3.5 启动8401微服务后查看sentienl控制台

sentinel管理页面:
尚硅谷 周阳老师 SpringCloud 学习笔记_第6张图片
空空如也,啥都没有

Sentinel采用懒加载说明:
执行一次访问即可:
http://localhost:8401/testA
http://localhost:8401/testB

效果:
尚硅谷 周阳老师 SpringCloud 学习笔记_第7张图片
开始监控信息没有,yml加入配置 management.endpoint.sentinel.enabled= true,重启后效果如下:
尚硅谷 周阳老师 SpringCloud 学习笔记_第8张图片
结论: sentinel8080正在监控微服务8401

4. 流控规则
4.1 基本介绍

尚硅谷 周阳老师 SpringCloud 学习笔记_第9张图片
进一步解释说明:

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

直接 -> 快速失败(系统默认)
配置及说明:
尚硅谷 周阳老师 SpringCloud 学习笔记_第10张图片
表示1秒钟内查询1次是ok的,若超过次数1,就直接-快速失败,报默认错误

测试:
快速点击访问 http://localhost:8401/testA

结果:
尚硅谷 周阳老师 SpringCloud 学习笔记_第11张图片
思考:
直接调用默认报错信息,技术方面ok,但是是否应该有我们自己的后续处理? 类似有个fallback的兜底方法?

阈值类型为线程数时,指的是当处理api请求的线程超过阈值时,报错。

4.2.2 关联

**是什么:**当关联的资源达到阈值的时候,就限流自己。当与A关联的资源B达到阈值后,就限流A自己,B惹事,A挂了(举例:支付接口的请求量达到阈值后,就限制下单请求的接口)。

配置A:
当对testB的访问QPS大于1时,限流testA
尚硅谷 周阳老师 SpringCloud 学习笔记_第12张图片
postman模拟密集的访问testB
尚硅谷 周阳老师 SpringCloud 学习笔记_第13张图片
循环访问50次,间隔0.3s

测试结果:A被限流
尚硅谷 周阳老师 SpringCloud 学习笔记_第14张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第15张图片

4.2.3 链路

只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】

4.3 流控效果
4.3.1 直接 -> 快速失败(默认的流控处理)

直接失败,抛出异常 Blocked by Sentinel (flow limiting)
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController

4.3.2 预热

1.说明
公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值

2.官网
https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
Warm Up
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
尚硅谷 周阳老师 SpringCloud 学习笔记_第16张图片
默认codeFactor为3,即请求QPS从threshold/3开始,经过预热时长逐渐上升至设定的QPS阈值。
3.源码
com.alibaba.csp.sentinel.slots.block.flow.controller
在这里插入图片描述
4.WarmUp配置
默认coldFactor为3,即请求QPS从(threshold/3)开始,经过多少预热时长才逐渐升至设定的QPS阈值。
案例:阈值为10 + 预热时长设置5秒
系统初始化的阈值为10/3,约等于3;然后经过了5秒后阈值才开始慢慢升高恢复到10.
尚硅谷 周阳老师 SpringCloud 学习笔记_第17张图片
如:秒杀系统开启的瞬间,会有很多流量上来,有可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。

4.3.3 排队等待

匀速排队,让请求以均匀的速度通过,阈值类型必须设置为QPS,否则无效。
设置含义:/testB每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒
尚硅谷 周阳老师 SpringCloud 学习笔记_第18张图片

官网:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
尚硅谷 周阳老师 SpringCloud 学习笔记_第19张图片
源码:com.alibaba.csp.sentinel.slots.block.flow.controller
测试:
尚硅谷 周阳老师 SpringCloud 学习笔记_第20张图片

使用postman连续访问testB
后台打印日志:
尚硅谷 周阳老师 SpringCloud 学习笔记_第21张图片
每秒处理了一个请求

5. 降级规则

官网:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7

5.1 基本介绍

尚硅谷 周阳老师 SpringCloud 学习笔记_第22张图片

RT(平均响应时间,秒级) :
平均响应时间超出阈值且在时间窗口内通过的阈值大于等于5,两个条件同时满足后触发降级
尚硅谷 周阳老师 SpringCloud 学习笔记_第23张图片
异常比例:
QPS>=5,且异常比例(秒级比例)超过阈值时,触发降级;时间窗结束,关闭降级
窗口期过后关闭断路器
RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=xxx才能生效)
尚硅谷 周阳老师 SpringCloud 学习笔记_第24张图片

异常数(分钟级)
异常数(分钟统计)超过阈值时,触发降级;时间窗结束后,关闭降级。

尚硅谷 周阳老师 SpringCloud 学习笔记_第25张图片
进一步说明:
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。。
当资源被降级以后,在接下来的降级时间窗口内,对该资源的调用都自动熔断 (默认行为是抛出DegradeException)

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

5.2 降级策略实战

1.RT

是什么:

平均响应时间:当1s内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阈值(以毫秒为单位),那么在接下来的时间窗口(s为单位)内,对这个方法的调用都会自动地熔断(抛出DegradeException)。注意Sentinel默认的RT上限是4900ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项(-Dcsp.sentinel.statistic.max.rt=xxx)来配置。

尚硅谷 周阳老师 SpringCloud 学习笔记_第26张图片
测试:
代码

@GetMapping("/testD")
    public String testD() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.info("testD ---测试RT");
        return "---testD";
    }

配置
尚硅谷 周阳老师 SpringCloud 学习笔记_第27张图片
RT等于200表示要求平均在200ms内处理完成。

jmeter压测
尚硅谷 周阳老师 SpringCloud 学习笔记_第28张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第29张图片

按照上诉配置,永远1秒钟打进来10个线程(大于5个了),调用testD,Sentinel希望200ms处理完本次任务,如果超过200ms还没处理完,在未来的1秒钟的时间窗口内,断路器打开,微服务不可用。
后续停止jmeter,没有那么大访问量,断路器关闭,微服务恢复。

2.异常比例
是什么:

异常比例:当资源的每秒请求量大于等于5,并且每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下来的时间窗口之内,对这个方法的调用都会自动地返回。异常比例的阈值为0.0-1.0

尚硅谷 周阳老师 SpringCloud 学习笔记_第30张图片
测试:

    @GetMapping("/testC")
    public String testC() {
        log.info("testC 测试异常比例");
        int age = 10 / 0;
        return "-----testC";
    }

配置:
尚硅谷 周阳老师 SpringCloud 学习笔记_第31张图片
使用jmeter测试:
尚硅谷 周阳老师 SpringCloud 学习笔记_第32张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第33张图片

尚硅谷 周阳老师 SpringCloud 学习笔记_第34张图片
结论:
按照上述配置,单独访问一次,必然访问一次报错一次(int age = 10/0),调一次错一次。
开启jmeter后,直接高并发请求,多次调用达到我们的配置条件(每秒请求量大于等于5 + 异常比例超过阈值),断路器开启,微服务不可用,不再报错error而是服务降级了

3.异常数

异常数:当资源接近一分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若时间窗小于60s,则结束熔断状态后仍可能再进入熔断状态

时间窗口一定要大于等于60s
尚硅谷 周阳老师 SpringCloud 学习笔记_第35张图片

代码:

@GetMapping("/testE")
    public String testE() {
        log.info("testE 测试异常数");
        int age = 10 / 0;
        return "-----testE";
    }

配置:
尚硅谷 周阳老师 SpringCloud 学习笔记_第36张图片
访问 http://localhost:8401/testE ,第一次访问绝对出错,因为除数不能为0,我们看到的是error窗口,连续访问到第五次后,第六次开始进入熔断降级,看到的不再是error页面而是熔断降级的提示信息。

6. 热点key限流
6.1 基本介绍

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

尚硅谷 周阳老师 SpringCloud 学习笔记_第37张图片

官网:
官网文档

源码:com.alibaba.csp.sentinel.slots.block.BlockException

6.2 配置与配置
6.2.1 代码
    @GetMapping("/testHotKey")  //rest地址
    @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 exception) {
        return "------  deal_testHotKey";//sentinel 的默认提示都是Blocked by Sentinel (flow limiting)
    }

@SentinelResource(value = “testHotKey”, blockHandler =
“deal_testHotKey”)
表示调用的兜底方法为deal_testHotKey()

6.2.2 配置

尚硅谷 周阳老师 SpringCloud 学习笔记_第38张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第39张图片
配置 表示方法testHotKey里面的第一个参数只要QPS超过每秒1次,马上降级处理

6.2.3 结果

尚硅谷 周阳老师 SpringCloud 学习笔记_第40张图片
当请求中含有指定的参数且QPS超过阈值时,就会降级调用兜底方法,上图就是返回的兜底方法的执行结果。当请求的参数中不含有sentinel指定的热点key时,QPS超过阈值也不会降级处理。

注意:
当不配置blockHandler,达到降级条件的返回结果页面如下:

    @GetMapping("/testHotKey")  //rest地址
    @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 exception) {
        return "------  deal_testHotKey";//sentinel 的默认提示都是Blocked by Sentinel (flow limiting)
    }

尚硅谷 周阳老师 SpringCloud 学习笔记_第41张图片
异常打到了前台用户界面,不友好。所以尽量使用blockHandler配置兜底的方法。

6.3 参数例外项

6.2.1-6.2.3的案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流。这是普通的情况,超过阈值且满足条件时马上被限流。

特例情况:
当期望p1参数是某个特殊值时,它的限流值和平时不一样。比如:当p1的值为5时,它的阈值可以达到200

尚硅谷 周阳老师 SpringCloud 学习笔记_第42张图片

6.3.1 配置

尚硅谷 周阳老师 SpringCloud 学习笔记_第43张图片
添加按钮不能忘

6.3.2 测试

尚硅谷 周阳老师 SpringCloud 学习笔记_第44张图片

尚硅谷 周阳老师 SpringCloud 学习笔记_第45张图片
当参数p1的值不为5时,阈值超过1后就会降级。参数为5时,阈值为200,此时手动很难超过此阈值,没超过此阈值也就不会降级。

前提条件:热点key的注意点,参数必须是基本类型或者String

6.4 其他

手贱添加异常看看:
在这里插入图片描述

配置
尚硅谷 周阳老师 SpringCloud 学习笔记_第46张图片
结果
若含有配置的热点key,未达到所配置的降级条件,返回异常页面。
尚硅谷 周阳老师 SpringCloud 学习笔记_第47张图片

若含有配置的热点key,且达到了降级条件,则降级调用兜底方法返回:
尚硅谷 周阳老师 SpringCloud 学习笔记_第48张图片
若请求钟不包含配置的热点key参数,则每次请求都是超出异常,不会发生降级:
尚硅谷 周阳老师 SpringCloud 学习笔记_第49张图片

@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置兜底处理

RuntimeException
int age = 10/0,这个是java运行时报出的运行时异常, @SentinelResource不会管

总结: @SentinelResource主管配置出错,运行出错该走异常走异常

7. 系统规则(系统自适应限流)
7.1 是什么

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

官网文档

系统保护的目的:1.保证系统不被拖垮 2.在系统稳定的前提下,保持系统的吞吐量

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量,比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
尚硅谷 周阳老师 SpringCloud 学习笔记_第50张图片

7.2 各项参数配置说明:

Load 自适应:系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 max QPS * min Rt 估算得出。设定参考值一般是 CPU cores * 2.5。
CPU使用率(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

8. @SentinelResource
8.1 按资源名称限流 + 后续处理
8.1.1 启动Nacos+Sentinel
8.1.2 Module

1.创建module cloudalibaba-sentinel-service8401或者使用上面创建的8401

2.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-sentinel-service8401</artifactId>
    <dependencies>
        <!--SpringCloud alibaba nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--引入自定义的api包,可以使用Payment支付Entity-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--springBoot整合web组件+actuator-->
        <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>

        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </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>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.3</version>
        </dependency>
    </dependencies>

</project>

3.application.yml

server:
  port: 8401
spring:
  application:
    name: cloudalibaba-sentinel-service

  cloud:
    nacos:
      discovery:
        #Nacos 服务注册中心地址
        server-addr: localhost:8848

    sentinel:
      transport:
        dashboard: localhost:8080  #配置sentinel dashboard地址
        port: 8719 #默认8719端口,假如被占用会自动从8719开始依次加1扫描,直到找到未被占用的端口

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    sentinel:
      enabled: true #加了此配置,不加此配置监控页面无显示

4.业务类RateLimitController

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {

    @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 服务不可用");
    }
}

5.主启动

package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class);
    }
}

8.1.3 配置流控规则

尚硅谷 周阳老师 SpringCloud 学习笔记_第51张图片
表示1秒钟内查询次数大于1,就进行流控限流。

8.1.4 测试

正常情况:

尚硅谷 周阳老师 SpringCloud 学习笔记_第52张图片
达到流控条件时:
返回了自定义的限流处理信息,限流发生
尚硅谷 周阳老师 SpringCloud 学习笔记_第53张图片

8.1.5 额外问题

关闭服务8401看看
Sentinel控制台,流控规则消失了??
临时/持久?

8.2 按照URL地址限流 + 后续处理

通过访问的URL来限流,会返回Sentinel自带的默认的限流处理信息

8.2.1 业务类RateLimitController
package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {

    @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 服务不可用");
    }

    @GetMapping("/rateLimit/byUrl")
    @SentinelResource(value = "byUrl")
    public CommonResult byUrl() {
        return new CommonResult(200, "按url限流测试ok", new Payment(2020L, "serial002"));
    }
}

byUrl() 方法的@SentinelResource没有配blockHandler,所以没有兜底方法,当达到限流条件时,会使用系统默认的兜底方法。

8.2.2 Sentinel控制台配置

尚硅谷 周阳老师 SpringCloud 学习笔记_第54张图片

8.2.3 测试

正常访问:
尚硅谷 周阳老师 SpringCloud 学习笔记_第55张图片
达到流控条件时:
返回Sentinel自带的限流处理结果
尚硅谷 周阳老师 SpringCloud 学习笔记_第56张图片

8.3 上面兜底方案面临的问题
  1. 系统默认的,没有体现我们自己的业务需求
  2. 依照现有条件,我们自定义的处理方法又和业务代码耦合在一起,不直观
  3. 每个业务方法都添加一个兜底的方法,那代码膨胀加剧
  4. 全局统一的处理方法没有体现
8.4 客户自定义限流处理逻辑
8.4.1 创建CustomerBlockHandler类用于自定义限流处理逻辑

自定义限流处理类 CustomerBlockHandler

package com.atguigu.springcloud.alibaba.myhandler;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;

public class CustomerBlockHandler {

    public static CommonResult handlerException(BlockException exception) {
        return new CommonResult(444, "按客户自定义, global handlerException----1");
    }

    public static CommonResult handlerException2(BlockException exception) {
        return new CommonResult(444, "按客户自定义, global handlerException-----2");
    }
}

8.4.2 业务类RateLimitController
package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.alibaba.myhandler.CustomerBlockHandler;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {

    @GetMapping("/rateLimit/byUrl")
    @SentinelResource(value = "byUrl")
    public CommonResult byUrl() {
        return new CommonResult(200, "按url限流测试ok", new Payment(2020L, "serial002"));
    }

    @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 服务不可用");
    }

    //  使用自定义限流处理类 CustomerBlockHandler
    @GetMapping("/rateLimit/CustomerBlockHandler")
    @SentinelResource(value = "CustomerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
    public CommonResult CustomerBlockHandler() {
        return new CommonResult(200, "按自定义限流处理类限流测试ok", new Payment(2020L, "serial003"));
    }
}

8.4.3 Sentinel控制台配置

尚硅谷 周阳老师 SpringCloud 学习笔记_第57张图片

8.4.4 测试

正常访问
尚硅谷 周阳老师 SpringCloud 学习笔记_第58张图片
达到限流条件:
尚硅谷 周阳老师 SpringCloud 学习笔记_第59张图片

8.4.5 进一步说明

尚硅谷 周阳老师 SpringCloud 学习笔记_第60张图片

8.4.6 更多注解属性说明

@SentinelResource注解
尚硅谷 周阳老师 SpringCloud 学习笔记_第61张图片

Sentinel主要有3个核心的API:
SphU定义资源
Tracer定义统计
ContextUtil定义了上下文

9. 服务熔断功能

Sentinel整合ribbon + openFeign + fallback

9.1 Ribbon系列
9.1.1 启动nacos和sentinel
9.1.2 提供者9003/9004

新建cloudalibaba-provider-payment9003/9004 (两个做法一样)

1.pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020artifactId>
        <groupId>org.examplegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloudalibaba-provider-payment9004artifactId>
    <dependencies>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        <dependency>
            <groupId>org.examplegroupId>
            <artifactId>cloud-api-commonsartifactId>
            <version>${project.version}version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>
project>

2.application.yml

server:
  port: 9003

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址

management:
  endpoints:
    web:
      exposure:
        include: '*'

3.主启动类

package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9003.class);
    }
}

4.业务类

package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9003.class);
    }
}
9.1.3 消费者84

新建module cloudalibaba-consumer-nacos-order84

1.pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020artifactId>
        <groupId>org.examplegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloudalibaba-consumer-nacos-order84artifactId>
    <dependencies>
        
        
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
        dependency>
        
        <dependency>
            <groupId>org.examplegroupId>
            <artifactId>cloud-api-commonsartifactId>
            <version>${project.version}version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

project>

2.application.yml

server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

# 激活Sentinel对Feign的支持
#feign:
#  sentinel:
#    enabled: false

3.主启动类

package com.atguigu.springcloud.alibaba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class);
    }
}

4.业务类
ApplicationContextConfig

package com.atguigu.springcloud.alibaba.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {
    
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

CircleBreakController

修改后请重启微服务:热部署对Java代码级生效,对@SentinelResource注解内属性,有时效果不太好
目的:fallback管运行时异常,blockHandler管配置违规

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class CircleBreakController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")//没有配置
    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;
    }

}

测试地址:http://localhost:84/consumer/fallback/1

1.没有任何配置,如下: 给客户error页面,不友好

尚硅谷 周阳老师 SpringCloud 学习笔记_第62张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第63张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第64张图片
2.只配置fallback:

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class CircleBreakController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//第一种情况,什么都没有配置
    @SentinelResource(value = "fallback", fallback = "handlerFallback")//第二种情况,fallback只负责业务异常
    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;
    }
    
    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
        Payment payment = new Payment(id, "null");
        return new CommonResult(444,"兜底异常handlerFallback, exception内容: " +e.getMessage(), payment);
    }

}

测试结果:
在这里插入图片描述
在这里插入图片描述
3.只配置blockHandler:

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class CircleBreakController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//第一种情况,什么都没有配置
    //@SentinelResource(value = "fallback", fallback = "handlerFallback")//第二种情况,只配fallback,fallback只负责业务异常
    @SentinelResource(value = "fallback", blockHandler = "blockHandler")//第三种情况,只配blockHandler,负责sentinel控制台配置违规
    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);
    }

}

配置sentinel:
尚硅谷 周阳老师 SpringCloud 学习笔记_第65张图片
测试结果:
运行时异常没有兜底方法:
尚硅谷 周阳老师 SpringCloud 学习笔记_第66张图片
尚硅谷 周阳老师 SpringCloud 学习笔记_第67张图片
当异常数达到sentinel控制台配置的降级规则:
尚硅谷 周阳老师 SpringCloud 学习笔记_第68张图片

4.fallback和blockHandler都配置

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class CircleBreakController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//第一种情况,什么都没有配置
    //@SentinelResource(value = "fallback", fallback = "handlerFallback")//第二种情况,只配fallback,fallback只负责业务异常
    //@SentinelResource(value = "fallback", blockHandler = "blockHandler")//第三种情况,只配blockHandler,负责sentinel控制台配置违规
    @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")//第四种情况,fallback和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);
    }

}

Sentinel配置:
尚硅谷 周阳老师 SpringCloud 学习笔记_第69张图片

测试

当没有达到设置的QPS阈值时:
尚硅谷 周阳老师 SpringCloud 学习笔记_第70张图片

在这里插入图片描述
达到阈值时:
在这里插入图片描述
在这里插入图片描述
违反Sentinel控制台配置的规则时,直接服务熔断,就不会再进入正常的业务代码,就不会抛出异常(blockHandler控制入口,fallback控制内部逻辑)。

忽略异常属性

尚硅谷 周阳老师 SpringCloud 学习笔记_第71张图片

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class CircleBreakController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//第一种情况,什么都没有配置
    //@SentinelResource(value = "fallback", fallback = "handlerFallback")//第二种情况,只配fallback,fallback只负责业务异常
    //@SentinelResource(value = "fallback", blockHandler = "blockHandler")//第三种情况,只配blockHandler,负责sentinel控制台配置违规
    @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
            exceptionsToIgnore = {IllegalArgumentException.class})//第四种情况,fallback和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);
    }

}

测试:
尚硅谷 周阳老师 SpringCloud 学习笔记_第72张图片
在这里插入图片描述

9.2 Feign系列

修改84模块,84消费者调用提供者9003,Feign组件一般是消费侧

9.2.1 pom.xml

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
9.2.2 application.yml
server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true

9.2.3 主启动类

添加 @EnableFeignClients 注解

package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class);
    }
}

9.2.4 业务类

1.带 @FeignClient 注解的业务接口:

package com.atguigu.springcloud.alibaba.service;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
    // order调用paymentService
    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}


2.fallback = PaymentFallbackService.class

package com.atguigu.springcloud.alibaba.service;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackService implements PaymentService {
    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
        return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"error serial"));
    }
}

3.controller

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.alibaba.service.PaymentService;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback")//第一种情况,什么都没有配置
    //@SentinelResource(value = "fallback", fallback = "handlerFallback")//第二种情况,只配fallback,fallback只负责业务异常
    //@SentinelResource(value = "fallback", blockHandler = "blockHandler")//第三种情况,只配blockHandler,负责sentinel控制台配置违规
    @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
            exceptionsToIgnore = {IllegalArgumentException.class})//第四种情况,fallback和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);
    }
    
    
    //=============OpenFeign
    @Resource
    private PaymentService paymentService;
    @GetMapping(value = "/consumer/paymentSQL/{id}")
    public CommonResult paymentSQL(@PathVariable("id") Long id) {
        return paymentService.paymentSQL(id);
    }
}

9.2.5 测试

启动84、9003两个服务
地址:http://localhost:84/consumer/paymentSQL/1
在这里插入图片描述
尚硅谷 周阳老师 SpringCloud 学习笔记_第73张图片

测试84调用9003,此时故意关闭9003微服务提供者,看84消费侧自动降级,不会被耗死
尚硅谷 周阳老师 SpringCloud 学习笔记_第74张图片
这里的fallback是远程调用失败Feign指定的兜底方法,跟sentinel无关。
当用上 @SentinelResource 时sentinel的限流是也有效果的。

9.3 熔断框架比较
Sentinel Hystrix resilicence4j
隔离策略 信号量隔离(并发线程数限流) 线程池隔离/信号量隔离 信号量隔离
熔断降级策略 基于响应时间、异常比例、异常数 基于异常比例 基于异常比例、响应时间
实时统计实现 滑动窗口(leapArray) 滑动窗口(基于RxJava) Ring Bit Buffer
动态规则配置 支持多种数据源 支持多种数据源 有限支持
扩展性 多个扩展点 插件的形式 接口的形式
基于注解的支持 支持 支持 支持
限流 基于QPS,支持基于调用关系的限流 有限的支持 Rate Limiter
10.规则持久化
10.1 是什么

一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。

10.2 怎么玩

将限流配置规则持久化进nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401sentinel上的流控规则持续有效。

10.3 步骤

修改cloudalibab-sentinel-service8401

10.3.1 pom.xml
		<dependency>
            <groupId>com.alibaba.cspgroupId>
            <artifactId>sentinel-datasource-nacosartifactId>
        dependency>
10.3.2 application.yml

添加Nacos数据源配置:
尚硅谷 周阳老师 SpringCloud 学习笔记_第75张图片

9.3.3 添加Nacos业务规则配置

尚硅谷 周阳老师 SpringCloud 学习笔记_第76张图片
配置内容:

[{
    "resource": "/rateLimit/byUrl",
    "IimitApp": "default",
    "grade": 1,
    "count": 1, 
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
}]

内容解析:
resource: 资源名称
limitApp: 来源应用
grade: 阈值类型,0表示线程数,1表示QPS
count: 单机阈值
strategy: 流控模式,0表示直接,1表示关联,2表示链路
controlBehavior: 流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
clusterMode: 是否集群

通过上面配置的这种形式,将限流规则写进了Nacos中,发布保存后:
尚硅谷 周阳老师 SpringCloud 学习笔记_第77张图片

9.3.4 启动8401后,刷新Sentinel业务规则

需要刷新设置流控规则资源的rest地址才能在Sentinel规则列表查看到

访问 http://localhost:8401/rateLimit/byUrl
默认流控效果:
尚硅谷 周阳老师 SpringCloud 学习笔记_第78张图片

尚硅谷 周阳老师 SpringCloud 学习笔记_第79张图片

停止8401,再看Sentinel:
尚硅谷 周阳老师 SpringCloud 学习笔记_第80张图片
停机后发现流控规则没有了

重启8401,刷新rest地址,再看Sentinel:
尚硅谷 周阳老师 SpringCloud 学习笔记_第81张图片

尚硅谷 周阳老师 SpringCloud 学习笔记_第82张图片
规则持久化验证通过。
尚硅谷 周阳老师 SpringCloud 学习笔记_第83张图片

十九、SpringCloud Alibaba Seata 处理分布式事务

1. 分布式事务的问题

分布式之前,没有分布式事务的问题。
分布式之后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证
尚硅谷 周阳老师 SpringCloud 学习笔记_第84张图片
**一句话:**一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务。

2. Seata简介
2.1 是什么

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
官网地址:https://seata.io/zh-cn/

2.2 能干嘛

一个典型的分布式事务过程

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

Transaction ID XID ,全局唯一的事务ID

2.2.2 三组件概念

Transaction Coordinator (TC) : 事务协调者,维护全局和分支事务的状态,负责协调并驱动全局事务提交或者回滚
Transaction Manager ™ : 事务管理器,定义全局事务的范围,负责开启一个全局事务,并最终发起全局事务提交或者回滚的决议
Resource Manager (资源管理器) : 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或者回滚

2.2.3 处理过程
  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
  2. XID在微服务调用链路的上下文中传播
  3. RM向TC注册分支事务,将其纳入XID对全局事务的管辖
  4. TM向TC发起针对XID的全局提交或回滚决议
  5. TC调度XID下管辖的全部分支事务完成提交或者回滚请求
    尚硅谷 周阳老师 SpringCloud 学习笔记_第85张图片
2.3 去哪下

发布说明:https://github.com/seata/seata/releases

2.4 怎么玩

本地 @Transactional
全局 @GlobalTransactional
SEATA的分布式交易解决方案:
尚硅谷 周阳老师 SpringCloud 学习笔记_第86张图片
我们只需要使用一个@GlobalTransactional

3. Seata-server 下载安装

1.官网地址
https://seata.io/zh-cn/

2.下载版本 0.9.0

3.seata-server-0.9.0.zip解压到指定目录并修改conf目录下的file.conf配置文件
先备份原始file.conf文件,主要修改:自定义事务组名称 + 事务日志存储模式为db + 数据库连接信息
file.conf :
service模块:

vgroup_mapping.my_test_tx_group = “fsp_tx_group” 、、。;;#fsp_tx_group是随便取的

store模块:

mode = “db”

db 部分:
因为用的mysql8,需要将seata的lib目录下5.x的mysql驱动换为8.x的驱动,且配置如下:

driver-class-name =“com.mysql.cj.jdbc.Driver”
url = “jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true”
user = “root”
password = “自己的密码”

4.在数据库新建库seata
尚硅谷 周阳老师 SpringCloud 学习笔记_第87张图片

5.在seata库中新建表
建表db_store.sql在\seata-server-0.9.0\seata\conf目录下
尚硅谷 周阳老师 SpringCloud 学习笔记_第88张图片
6.修改\conf目录下的registry.conf配置文件
registry:

type = “nacos”

nacos:

serverAddr = “localhost:8848”

目的是指明注册中心为nacos,及修改nacos连接信息

7.先启动Nacos (8848)

8.再启动seata-server
双击 seata-server.bat 启动seata
尚硅谷 周阳老师 SpringCloud 学习笔记_第89张图片
启动成功!

4. 订单/库存/账户业务数据库准备

以下的演示都需要先启动Nacos后再启动Seata,保证两个都OK

4.1 分布式事务业务说明
4.1.1 业务说明

这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

下订单 —> 口库存 —> 减账户

4.2 创建业务数据库

seata_order: 存储订单的数据库
seata_storage: 存储库存的数据库
seata_account: 存储账户信息的数据库

建库SQL:

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
4.3 按照上述3库分别建对应的业务表
4.3.1 seata_order库中创建t_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=1 DEFAULT CHARSET=utf8;

SELECT * FROM t_order;

4.3.2 seata_storage库中创建t_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=1 DEFAULT CHARSET=utf8;

INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');

SELECT * FROM t_storage;
4.3.3 seata_account库中创建t_account表
CREATE TABLE t_account(
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
	`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=1 DEFAULT CHARSET=utf8;

INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');

SELECT * FROM t_account;
4.4 按照上述3库分别创建对应的日志回滚表

订单——库存——账户 3个库下都需要建各自的回滚日志表
\seata-server-0.9.0\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;
4.5 最终效果

尚硅谷 周阳老师 SpringCloud 学习笔记_第90张图片

5. 订单/库存/账户业务微服务准备

业务需求:下订单 —> 减库存 —> 扣余额 —> 修改订单状态

5.1 新建订单 Order-Module
5.1.1 seata-order-service2001
5.1.2 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020artifactId>
        <groupId>org.examplegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>seata-order-service2001artifactId>
    <dependencies>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-allartifactId>
                    <groupId>io.seatagroupId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>io.seatagroupId>
            <artifactId>seata-allartifactId>
            <version>0.9.0version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>

project>
5.1.3 application.yml
server:
  port: 2001
spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group  # 自定义事务组名称需要与seata-server中的对应

    nacos:
      discovery:
        server-addr: localhost:8848

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true&useInformationSchema=false
    username: root
    password: xxxxxxx

feign:
  hystrix:
    enabled: false

logging:
  level:
    io: 
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

5.1.4 file.conf
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

service {

  vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称

  default.grouplist = "127.0.0.1:8091"
  enableDegrade = false
  disable = false
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
  disableGlobalTransaction = false
}


client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  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.cj.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true"
    user = "root"
    password = "123456"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}
5.1.5 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"
  }
}

5.1.6 domain

CommonResult 类

package com.atguiggu.springcloud.alibaba.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@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类

package com.atguiggu.springcloud.alibaba.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@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: 已完成
}

5.1.7 dao接口及实现

OrderDao

package com.atguiggu.springcloud.alibaba.dao;

import com.atguiggu.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderDao {
    // 新建订单
    void create(Order order);

    // 修改订单状态 从0改为1
    void update(@Param("userId") Long userId, @Param("status") Integer status);
}

resource文件夹下新建mapper文件夹后添加OrderMapper.xml


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.atguiggu.springcloud.alibaba.dao.OrderDao">

    <resultMap id="BaseResultMap" type="com.atguiggu.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="BIGINT"/>
    resultMap>

    <insert id="create">
        insert into t_order(id, user_id, product_id, count, money, status)
        VALUES (null, #{userId}, #{productId}, #{count}, #{money}, 0)
    insert>

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

5.1.8 service接口及实现

OrderService

package com.atguiggu.springcloud.alibaba.service;

import com.atguiggu.springcloud.alibaba.domain.Order;

public interface OrderService {
    void create(Order order);
}

AccountService

package com.atguiggu.springcloud.alibaba.service;
import com.atguiggu.springcloud.alibaba.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

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

StorageService

package com.atguiggu.springcloud.alibaba.service;
import com.atguiggu.springcloud.alibaba.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage-service")//调用微服务的名字
public interface StorageService {

    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

OrderServiceImpl

package com.atguiggu.springcloud.alibaba.service.impl;

import com.atguiggu.springcloud.alibaba.dao.OrderDao;
import com.atguiggu.springcloud.alibaba.domain.Order;
import com.atguiggu.springcloud.alibaba.service.AccountService;
import com.atguiggu.springcloud.alibaba.service.OrderService;
import com.atguiggu.springcloud.alibaba.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
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);
        log.info("-----> 开始新建订单end");
        // 2.扣减库存
        log.info("-----> 订单微服务开始调用库存,做扣减count操作");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("-----> 订单微服务开始调用库存,做扣减count操作end");
        // 3.扣减账户余额
        log.info("-----> 账户余额做money扣减");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("-----> 账户余额做money扣减end");
        // .修改订单状态
        log.info("-----> 修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("-----> 修改订单状态结束");

        log.info("订单已完成");
    }
}

5.1.9 Controller
package com.atguiggu.springcloud.alibaba.controller;

import com.atguiggu.springcloud.alibaba.domain.CommonResult;
import com.atguiggu.springcloud.alibaba.domain.Order;
import com.atguiggu.springcloud.alibaba.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class OrderController {
    @Resource
    private OrderService orderService;

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

5.1.10 Config配置

DataSourceProxyConfig

package com.atguiggu.springcloud.alibaba.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.io.IOException;

@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Bean
    public DataSource 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();

    }
}

MybatisConfig

package com.atguiggu.springcloud.alibaba.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MybatisConfig {
}

5.1.11 主启动类
package com.atguiggu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

5.2 库存Storage-Module
5.2.1 seata-storage-service2002
5.2.2 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020artifactId>
        <groupId>org.examplegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>Storage-ModuleartifactId>
    <dependencies>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-allartifactId>
                    <groupId>io.seatagroupId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>io.seatagroupId>
            <artifactId>seata-allartifactId>
            <version>0.9.0version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>

project>
5.2.3 application.yml
server:
  port: 2002
spring:
  application:
    name: seata-storage-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group  # 自定义事务组名称需要与seata-server中的对应

    nacos:
      discovery:
        server-addr: localhost:8848

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true&useInformationSchema=false
    username: root
    password: xxxxxx

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
5.2.4 file.conf(与2001一模一样)
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

service {

  vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称

  default.grouplist = "127.0.0.1:8091"
  enableDegrade = false
  disable = false
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
  disableGlobalTransaction = false
}


client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  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.cj.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true"
    user = "root"
    password = "xxxxxxxx"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}
5.2.5 registry.conf(与2001一模一样)
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"
  }
}

5.2.6 domain

CommonResult.java

package com.atguigu.springcloud.alibaba.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

Storage.java

package com.atguigu.springcloud.alibaba.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
    private Long id;
    /**
     * 产品id
     */
    private Long productId;
    /**
     * 总库存
     */
    private Integer total;
    /**
     * 已用库存
     */
    private Integer used;
    /**
     * 剩余库存
     */
    private Integer residue;
}

5.2.7 dao接口及实现

StorageDao.java

package com.atguigu.springcloud.alibaba.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface StorageDao {
    //扣减库存
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

resource文件夹下新建mapper文件夹后添加OrderMapper.xml


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.atguigu.springcloud.alibaba.dao.StorageDao">
    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Storage">
        <result column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="INTEGER"/>
        <result column="used" property="used" jdbcType="INTEGER"/>
        <result column="residue" property="residue" jdbcType="INTEGER"/>
    resultMap>

    <update id="decrease">
        update t_storage set used = used + #{count}, residue = residue - #{count}
            where product_id = #{productId}
    update>

mapper>
5.2.8 Service接口及实现

StorageService.java

package com.atguigu.springcloud.alibaba.service;

public interface StorageService {
    /**
     * 扣减库存
     */
    void decrease(Long productId, Integer count);
}

StorageServiceImpl .java

package com.atguigu.springcloud.alibaba.service.impl;

import com.atguigu.springcloud.alibaba.dao.StorageDao;
import com.atguigu.springcloud.alibaba.service.StorageService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class StorageServiceImpl implements StorageService {

    private static final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Resource
    private StorageDao storageDao;

    @Override
    public void decrease(Long productId, Integer count) {
        logger.info("------>storage-service开始扣减库存");
        storageDao.decrease(productId,count);
        logger.info("------>storage-service扣减库存结束");
    }
}

5.2.9 Controller
package com.atguigu.springcloud.alibaba.controller;

import com.atguigu.springcloud.alibaba.domain.CommonResult;
import com.atguigu.springcloud.alibaba.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class StorageController {
    @Autowired
    private StorageService storageService;

    /**
     * 扣减库存
     */
    @RequestMapping("/storage/decrease")
    public CommonResult decrease(Long productId, Integer count) {
        storageService.decrease(productId,count);
        return new CommonResult(200,"扣减库存成功");
    }
}

5.2.10 Config配置 (省略,与2001一样)
5.2.11 主启动类
package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
@EnableFeignClients
@EnableDiscoveryClient
public class SeataStorageServiceApplication2002 {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageServiceApplication2002.class);
    }
}
5.3 新建账户Account-Module
5.3.1 新建module seata-account-service2003
5.3.2 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020artifactId>
        <groupId>org.examplegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>seata-account-service2003artifactId>
    <dependencies>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-allartifactId>
                    <groupId>io.seatagroupId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>io.seatagroupId>
            <artifactId>seata-allartifactId>
            <version>0.9.0version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>

project>
5.3.3 application.yml
server:
  port: 2003
spring:
  application:
    name: seata-account-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group  # 自定义事务组名称需要与seata-server中的对应

    nacos:
      discovery:
        server-addr: localhost:8848

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true&useInformationSchema=false
    username: root
    password: xxxxxxx

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
5.3.4 file.conf(省略,与2001、2002相同)
5.3.5 registry.conf(省略,与2001、2002相同)
5.3.6 domain

Account .java

package com.atguigu.springcloud.alibaba.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Long id;

    /**
     * 用户id
     */
    private Long userId;
    /**
     * 总额度
     */
    private BigDecimal total;
    /**
     * 已用额度
     */
    private BigDecimal used;
    /**
     * 剩余额度
     */
    private BigDecimal residue;
    
}

CommonResult.java 与2001、2002一致,省略

5.3.7 Dao

AccountDao.java

package com.atguigu.springcloud.alibaba.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface AccountDao {
    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

AccountMapper.xml


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.AccountDao">
    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Account">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="DECIMAL"/>
        <result column="used" property="used" jdbcType="DECIMAL"/>
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
    resultMap>

    <update id="decrease">
        update t_account
            set
                residue = residue - #{money}, used = used + #{money}
            where
                user_id = #{userId}
    update>
mapper>
5.3.8 Service接口及实现

AccountService.java

package com.atguigu.springcloud.alibaba.service;

import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

public interface AccountService {
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money);
}

AccountServiceImpl.java

package com.atguigu.springcloud.alibaba.service.impl;

import com.atguigu.springcloud.alibaba.dao.AccountDao;
import com.atguigu.springcloud.alibaba.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.math.BigDecimal;
@Service
public class AccountServiceImpl implements AccountService {
    private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);
    @Resource
    AccountDao accountDao;
    @Override
    public void decrease(Long userId, BigDecimal money) {
        logger.info("------> account-service 中开始扣减账户余额");
        accountDao.decrease(userId,money);
        logger.info("------> account-service 中扣减账户余额结束");
    }
}

5.3.9 Controller
package com.atguigu.springcloud.alibaba.controller;

import com.atguigu.springcloud.alibaba.domain.Account;
import com.atguigu.springcloud.alibaba.domain.CommonResult;
import com.atguigu.springcloud.alibaba.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
public class AccountController {
    @Autowired
    AccountService accountService;

    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
        accountService.decrease(userId,money);
        return new CommonResult(200,"账户扣减成功");
    }
}

5.3.10 Config配置 (省略,与2001一样)
5.3.11 主启动类
package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

5.4 测试

业务:下订单 --> 减库存 --> 扣余额 --> 改订单状态

5.4.1 数据库初始情况

seata_order.t_order
尚硅谷 周阳老师 SpringCloud 学习笔记_第91张图片
seata_storage.t_storage
尚硅谷 周阳老师 SpringCloud 学习笔记_第92张图片
seata_account.t_account
尚硅谷 周阳老师 SpringCloud 学习笔记_第93张图片

5.4.2 正常下单

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
在这里插入图片描述
库中数据:
t_order
尚硅谷 周阳老师 SpringCloud 学习笔记_第94张图片
t_storage
尚硅谷 周阳老师 SpringCloud 学习笔记_第95张图片
t_account
尚硅谷 周阳老师 SpringCloud 学习笔记_第96张图片
seata-order-service 日志:
尚硅谷 周阳老师 SpringCloud 学习笔记_第97张图片

5.4.3 超时异常,没加@GlobalTransactional

AccountServiceImpl添加超时
尚硅谷 周阳老师 SpringCloud 学习笔记_第98张图片
Feign默认调用超时1s
测试:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
尚硅谷 周阳老师 SpringCloud 学习笔记_第99张图片
seata-order-service 日志:
尚硅谷 周阳老师 SpringCloud 学习笔记_第100张图片
库中数据:
t_order
尚硅谷 周阳老师 SpringCloud 学习笔记_第101张图片
t_storage
尚硅谷 周阳老师 SpringCloud 学习笔记_第102张图片
t_account
尚硅谷 周阳老师 SpringCloud 学习笔记_第103张图片
可见t_order表中订单状态status为0,但库存被扣减(经过20s后扣减)了。

故障情况:
当库存和账户金额扣减后,订单状态并没有设置为已完成,没有从0改为1;而且由于feign的重试机制,账户余额还可能被多次扣减。

5.4.4 超时异常,添加@GlobalTransactional

AccountServiceImpl添加超时
尚硅谷 周阳老师 SpringCloud 学习笔记_第104张图片

OrderServiceImpl添加@GlobalTransactional
尚硅谷 周阳老师 SpringCloud 学习笔记_第105张图片
测试:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
在这里插入图片描述
seata-order-service 日志:
尚硅谷 周阳老师 SpringCloud 学习笔记_第106张图片

库中数据:
t_order
尚硅谷 周阳老师 SpringCloud 学习笔记_第107张图片
t_storage
尚硅谷 周阳老师 SpringCloud 学习笔记_第108张图片
t_account
尚硅谷 周阳老师 SpringCloud 学习笔记_第109张图片
结果:下单后数据库数据并没有任何改变,记录都没有添加进来。

你可能感兴趣的:(springcloud)