杨柳依 于2020年3月30日创建,最近更新于 2020年4月6日。 参考资料:尚硅谷周阳SpringCloud教程

Spring Cloud Alibaba

服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。

服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。

分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。

消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。

分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。。

阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。

分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。

阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

官方文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

Spring Cloud Alibaba Nacos 服务注册和配置中心

1. 简介与安装

一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos: Dynamic Naming and Configuration Service

注册中心 + 配置中心,Nacos = Eureka + Config + Bus

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

下载nacos-server-1.1.4.zip,解压,在进入到解压后的文件夹后,运行命令:

  1. bin\startup.cmd

访问http://localhost:8848/nacos,默认账号和密码是`nacos`

2. 作为服务注册中心

使用之前,需要在父POM中添加依赖:

  1. <dependencyManagement>
  2. <dependencies>
  3. <!-- 省略前面已经添加的 -->
  4. <!--spring cloud alibaba 2.1.0.RELEASE-->
  5. <dependency>
  6. <groupId>com.alibaba.cloud</groupId>
  7. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  8. <version>2.1.0.RELEASE</version>
  9. <type>pom</type>
  10. <scope>import</scope>
  11. </dependency>
  12. </dependencies>
  13. </dependencyManagement>

基于Nacos的服务提供者

新建模块cloudalibaba-provider-payment9001,添加依赖:

  1. <dependencies>
  2. <!--SpringCloud Alibaba Nacos -->
  3. <dependency>
  4. <groupId>com.alibaba.cloud</groupId>
  5. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  6. </dependency>
  7. <!-- SpringBoot整合Web组件 -->
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-starter-web</artifactId>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter-actuator</artifactId>
  15. </dependency>
  16. <!--日常通用jar包配置-->
  17. <dependency>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-devtools</artifactId>
  20. <scope>runtime</scope>
  21. <optional>true</optional>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.projectlombok</groupId>
  25. <artifactId>lombok</artifactId>
  26. <optional>true</optional>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-starter-test</artifactId>
  31. <scope>test</scope>
  32. </dependency>
  33. </dependencies>

新建配置文件application.yml

  1. server:
  2. port: 9001
  3. spring:
  4. application:
  5. name: nacos-payment-provider
  6. cloud:
  7. nacos:
  8. discovery:
  9. server-addr: localhost:8848 # Nacos地址
  10. # 暴露监控端点
  11. management:
  12. endpoints:
  13. web:
  14. exposure:
  15. include: '*'

新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类PaymentMain9001

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. public class PaymentMain9001 {
  4. public static void main(String[] args) {
  5. SpringApplication.run(PaymentMain9001.class, args);
  6. }
  7. }

在包alibaba下新建控制类controller.PaymentController

  1. @RestController
  2. public class PaymentController {
  3. @Value("${server.port}")
  4. private String serverPort;
  5. @GetMapping(value = "/payment/nacos/{id}")
  6. public String getPayment(@PathVariable("id") Integer id) {
  7. return "nacos registry, serverPort: " + serverPort + "\t id: " + id;
  8. }
  9. }

即可开始测试。首先保证Nacos是处于运行状态,启动本模块,访问http://localhost:9001/payment/nacos/1,正确返回结果。

访问在http://localhost:8848/nacos管理界面,可以看到服务成功注册。

为演示负载均衡功能,仿照9001新建模块cloudalibaba-provider-payment9002

基于Nacos的服务消费者

新建模块cloudalibaba-consumer-nacos-order83,POM依赖与9001一样。

新建配置文件application.yml

  1. server:
  2. port: 83
  3. spring:
  4. application:
  5. name: nacos-order-consumer
  6. cloud:
  7. nacos:
  8. discovery:
  9. server-addr: localhost:8848
  10. #消费者将要去访问的微服务名称(注册成功进Nacos的微服务提供者)
  11. service-url:
  12. nacos-user-service: http://nacos-payment-provider

新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类OrderNacosMain83

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

在包alibaba下新建设置类config.ApplicationContextConfig

@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced // 一定要加才能负载均衡
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

在包alibaba下新建控制类controller.OrderNacosController

@RestController
public class OrderNacosController {
    @Resource
    private RestTemplate restTemplate;

    @Value("${service-url.nacos-user-service}") // 之前写到配置文件里了
    private String serverURL;

    @GetMapping(value = "/consumer/payment/nacos/{id}")
    public String paymentInfo(@PathVariable("id") Integer id) {
        return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);
    }
}

在前面的基础上启动该模块,多次访问http://localhost:83/consumer/payment/nacos/1,可以看到负载均衡。

注册中心对比

Nacos服务发现实例模型

  • 临时实例(AP)
    • 客户端上报健康状态
    • 摘除不健康实例
    • 非持久化
  • 持久化实例(CP)
    • 服务端探测健康状态
    • 保留不健康实例
    • 持久化

Nacos与其它注册中心对比

功能 Nacos Eureka Consul CoreDNS Zookeeper
一致性协议 CP/AP AP CP / CP
健康检查 TCP/HTTP/MySQL/Client Beat Client Beat TCP/HTTP/gRPC/Cmd / Client Beat
负载均衡 权重/DSL/metadata/CMDB Ribbon Fabio RR /
雪崩保护 支持 × × ×
自动注销实例 × ×
访问协议 HTTP/DNS/UDP HTTP HTTP/DNS DNS TCP
监听支持 ×
多数据支持 × ×
跨注册中心 × × ×
SpringCloud集成 × ×
Dubbo集成 × × ×
K8s集成 × ×
  • Zookeeper不支持SpringCloud,是SpringCloud主动适配的Zookeeper。

Nacos AP/CP模式切换

C是所有节点在同一时间看到的数据是一致的(一致性),A的定义是所有的请求都会收到响应(高可用)。

何时选择使用何种模式?

  • 如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。
    当前主流的服务如Spring cloud和Dubbo服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例
  • 如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。
    CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。

运行以下命令进行模式切换:

curl -X PUT "$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP"

3. 作为服务配置中心

基础配置

新建模块cloudalibaba-config-nacos-client3377,添加依赖:

<dependencies>
    <!-- nacos-config -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!-- nacos-discovery -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- 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>
    <!-- 一般基础配置 -->
    <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>
</dependencies>

新建配置文件bootstrap.yml

# Nacos配置
server:
  port: 3377

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定配置文件的格式

新建配置文件application.yml

spring:
  profiles:
    active: dev   # 表示处于开发环境

新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类NacosConfigClientMain3377

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

在包alibaba下新建控制类controller.ConfigClientController

@RestController
@RefreshScope   // 支持Nacos的动态刷新功能
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() {
        return configInfo;
    }
}

接下来就是在Nacos中添加配置信息了【重点】

Nacos中的匹配规则:

${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

本模块的name为nacos-config-client,环境为dev,配置文件格式为yaml,即nacos-config-client-dev.yml

http://localhost:8848/nacos的配置列表中新增配置,Data ID为nacos-config-client-dev.yaml,配置格式选yaml,配置内容填:

config:
  info: "config info for dev, version=1"

注意文件名与后缀严格相等,前面写的是yaml,这里文件名应该是yaml。 建议不要使用Firefox,会有bug,使用Chrome访问。

启动本模块,访问http://localhost:3377/config/info,可以看到配置内容。修改配置,再次访问会发现内容会**自动刷新**。

分类配置

问题1:实际开发中,通常一个系统会准备 dev开发环境、test测试环境、prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢? 问题2:一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目仅都会有相应的开发环境、测试环境、预发环境、正式环境等。那怎么对这些微服务配置进行管理呢?

Namespace + Group + Data ID。Namespace用于区分部署环境、Group和Data ID用于逻辑上区分两个目标对象。

不同的Namespace是隔离的,Group可以把不同的微服务划分到同一个分组里面去。

默认:Namespace=public,Group=DEFAULT_GROUP,Cluster=DEFAULT。

Data ID方案

指定spring.profile.active和配置文件的Data ID来读取不同环境下的配置

在Nacos配置列表中新建test环境的配置nacos-config-client-test.yaml,修改3377的配置文件application.yml

spring:
  profiles:
    active: test   # 表示处于测试环境

重启模块,访问http://localhost:3377/config/info,可以看到修改后的测试环境的配置文件。

Group方案

在Nacos配置列表中新建dev环境的配置nacos-config-client-info.yaml,Group为DEV_GROUP

config:
  info: "DEV_GROUP, nacos-config-client-info.yaml, version=1"

新建test环境的配置nacos-config-client-info.yaml,Group为TEST_GROUP

config:
  info: "TEST_GROUP, nacos-config-client-info.yaml, version=1"

修改3377的配置文件bootstrap.yml,在spring.cloud.nacos.config下新增配置:

config:
  group: TEST_GROUP

修改3377的配置文件application.yml

spring:
  profiles:
    active: info

重启模块,访问http://localhost:3377/config/info,可以看到修改后的测试环境的配置文件。

Namespace方案

在Nacos管理界面新建2个命名空间devtest,在dev中新建配置nacos-config-client-info.yaml

config:
  info: "namespace: dev, nacos-config-client-info.yaml, version=1"

修改3377的配置文件bootstrap.yml,在spring.cloud.nacos.config下新增配置:

config:
  # group: TEST_GROUP    # 注释掉分组
  namespace: 6c10caf3-ba7c-4c1e-9f3f-10d029e25b57        # 为namespace dev对应的id

重启模块,访问http://localhost:3377/config/info,可以看到修改后的测试环境的配置文件。

4. 集群和持久化配置

持久化到MySQL

默认Nacos使用嵌入式数据库Derby实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储

Apache Derby是一个完全用java编写的数据库,非常小巧,核心部分derby.jar只有2M,所以既可以做为单独的数据库服务器使用,也可以内嵌在应用程序中使用。

nacos/conf/下有一个nacos-mysql.sql脚本,在mysql中新建数据库nacos_config,复制脚本内容并执行。

复制以下内容并粘到nacos/conf/application.properties中:

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

重启Nacos,可以发现前面配置的东西都没有了,说明切换数据库成功。在nacos添加一条配置,在数据库his_config_info表内可以看到数据。

Linux Nacos安装

预计需要 1个Nginx + 3个Nacos + 1个MySQL。

下载nacos-server-1.1.4.tar.gz到linux主机/opt/,在该路径下运行命令:

tar -xzvf nacos-server-1.1.4.tar.gz
mkdir /mynacos
cp -r nacos /mynacos
cd /mynacos/nacos/bin
cp startup.sh startup.sh.bk
cd /mynacos/nacos/conf
cp application.properties application.properties.bk
cp cluster.conf.example cluster.conf

进入MySQL,运行命令:

# 创建数据库
create database `nacos_config` character set 'utf8' collate 'utf8_general_ci';
# 选择数据库
use nacos_config;
# 执行sql文件脚本
source /mynacos/nacos/conf/nacos-mysql.sql

# 也可以不进入mysql在命令行执行脚本
mysql -uroot -p nacos_config < /mynacos/nacos/conf/nacos-mysql.sql

编辑conf/路径下的配置文件application.properties,添加内容:

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

修改集群配置文件cluster.conf

# 这里的ip必须是本地执行 hostname -i 显示出来的ip
127.0.0.1:3333
127.0.0.1:4444
127.0.0.1:5555

改启动脚本startup.sh,使其能够接受不同的启动端口:

# 57行
while getopts ":m:f:s:p:" opt
do
    case $opt in
        m)
            MODE=$OPTARG;;
        f)
            FUNCTION_MODE=$OPTARG;;
        s)
            SERVER=$OPTARG;;
        p)
            PORT=$OPTARG;;
        ?)
        echo "Unknown parameter"
        exit 1;;
    esac
done

# 134行
nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1 &

【TODO】

Spring Cloud Alibaba Sentinel 实现熔断与限流

1. 简介与安装

面向云原生微服务的流量控制、熔断降级组件。Hystrix与Sentinel对比:

Hystrix Sentinel
1. 需要手工搭建监控平台 1. 单独一个组件,可以独立出来
2. 没有web界面可以进行细粒度化的配置实现流量控制、速率控制、服务熔断、服务降级 2. 界面化的细粒度统一配置

Hystrix

Github上下载sentinel-dashboard-1.7.1.jar,在下载路径运行java -jar sentinel-dashboard-1.7.1.jar,默认端口为8080,访问http://localhost:8080,默认用户名和密码是`sentinel`。

2. 使用

新建一个模块cloudalibaba-sentinel-service8401,添加依赖:

<dependencies>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>  
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>4.6.3</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>

添加配置application.yml

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        # 配置dashboard地址
        dashboard: localhost:8080
        # 默认8719端口,端口被占用会依次往后+1扫描到能用的端口
        port: 8719

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

新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类MainApp8401

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

在包alibaba下新建控制类controller.FlowLimitController

@RestController
@Slf4j
public class FlowLimitController {
    @GetMapping("/testA")
    public String testA() {
        log.info(Thread.currentThread().getName()+"\t "+ "...testA");
        return "-----------testA----------";
    }

    @GetMapping("/testB")
    public String testB() {
        return "-----------testB----------";
    }
}

先启动nacos、sentinel,再启动本模块,打开sentinel管理界面,发现空空如也,原因是sentinel采用懒加载机制,访问一次服务就会把服务加载上去。

3. 流控规则

基本介绍

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

流控模式

直接(默认)

QPS:对/testA新增流控规则,QPS为1,再访问http://localhost:8401/testA,可以发现每秒一次访问可以成功,狂点则返回默认错误`Blocked by Sentinel (flow limiting)`。后续会改进fallback方法,返回自定义内容。

线程数:修改testA方法,增加sleep。对/testA修改流控规则,线程数为1,再访问http://localhost:8401/testA,可以发现慢速点可以成功,狂点则返回默认错误,原因是前面的线程还没完成访问,会开新的线程,被限流。

@GetMapping("/testA")
public String testA() {
    // 暂停
    try {
        TimeUnit.MILLISECONDS.sleep(800);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "-----------testA----------";
}

关联

比如当与A关联的资源B达到阈值后,就限流A自己。恢复testA方法。对/testA修改流控规则,关联资源为/testB,QPS为1。

用postman模拟并发访问testB,新建get请求http://localhost:8401/testB,保存到Collection中,然后点Run,指定Iterations为20,Delay为300,意思是20个线程每次间隔0.3s访问1次,然后运行。浏览器访问testA,发现A返回默认错误。

链路

TODO

流控效果

快速失败

直接失败,抛异常。源码为com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController,在sentinel-core-1.6.3.jar中。

Warm Up(预热)

公式:阈值除以coldFactor(默认3),经过预热时长后才会达到阈值。对/testA修改流控规则,流控模式为直接,QPS为5,流控效果为Warm Up,预热时长为3,意思是刚开始的QPS阈值为5/3=1,3s后阈值达到5。访问testA,发现刚开始狂点会失败,后面狂点会正常返回结果。

排队等待

阈值类型只能是QPS,对/testA修改流控规则,流控模式为直接,QPS为1,流控效果为排队等待,超时时间为20000

用postman模拟并发访问testA,保存到Collection中,然后点Run,指定Iterations为10,Delay为100,然后运行。观察打印日志,可以发现都能访问成功,但是会变成1秒1个请求(前面设置的QPS)。

4. 降级规则

基本介绍

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

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

Sentinel的断路器没有半开状态。取而代之的是时间窗口。

降级策略:

  • RT:平均响应时间,秒级
    平均响应时间超出阈值 在时间窗口内通过的请求≥5,两个条件同时满足后触发降级。窗口期过后关闭断路器,RT最大值为4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXX才能生效)
  • 异常比例:秒级
    QPS≥5异常比例(秒级统计)超过阈值时,触发降级。时间窗口期结束后关闭降级。
  • 异常数:分钟级
    异常数(分钟统计)超过阈值时,触发降级。时间窗口期结束后关闭降级。

RT

FlowLimitController类新增方法testD:

@GetMapping("/testD")
public String testD() {
    // 暂停
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info(Thread.currentThread().getName()+"\t "+ "...testD 测试RT");
    return "-----------testA----------";
}

在sentinel界面新增降级规则/testD,降级策略为RT,阈值为200,时间窗口为1。意思是每个请求必须要在200ms内完成,时间窗口为1s。使用Postman进行压测,100个请求,delay为100ms。再浏览器访问testD,可以发现服务降级了。

异常比例

阈值范围为[0.0, 1.0]。对FlowLimitController类新增方法testE:

@GetMapping("/testE")
public String testE() {
    int a = 10 / 0;
    log.info(Thread.currentThread().getName() + "\t " + "...testE 测试异常比例");
    return "-----------testE 测试异常比例----------";
}

在sentinel界面新增降级规则/testE,降级策略为异常比例,阈值为0.2,时间窗口为1。单独访问发现每次都报错,使用Jmeter进行压测,每秒10个请求,再浏览器访问testD,可以发现服务降级了。

异常数

此时时间窗口一定要≥60s。对FlowLimitController类新增方法testF:

@GetMapping("/testF")
public String testF() {
    int a = 10 / 0;
    log.info(Thread.currentThread().getName() + "\t " + "...testF 测试异常数");
    return "-----------testE 测试异常数----------";
}

在sentinel界面新增降级规则/testF,降级策略为异常数,阈值为5,时间窗口为70。浏览器多次访问testF, 发现从第6次开始触发服务降级。

5. 热点key限流

基本介绍

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

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

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

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

热点参数限流支持集群模式。

配置

使用@SentinelResource可以实现自己的兜底方法。

FlowLimitController类新增方法:

@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
                         @RequestParam(value = "p1", required = false) String p2) {
    return "---------testHotKey";
}

// 兜底方法
public String deal_testHotKey(String p1, String p2, BlockException exception) {
    return "---------deal_testHotKey,兜底方法";
}

接着新增热点规则。资源名为testHotKey,参数索引为0(顺序以Java代码中的索引为准),单击阈值为1,窗口期为1。

此外,用了热点规则限流,请一定要实现自定义兜底方法,否则会返回Error Page。

参数例外项

期望当p1的值为某个特殊值时,采用另外的限流规则。

修改testHotKey热点规则,添加参数例外项,参数类型为String,参数值为5,阈值为200。

注意,Sentinel主管管理界面设置的配置出错,运行出错照样走异常。

6. 系统规则

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

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如Web服务或Dubbo服务端接收的请求,都属于入口流量。

系统规则支持以下的模式:

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

7. @SentinelResource

cloudalibaba-sentinel-service8401模块添加依赖:

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

增加业务类RateLimitController

@RestController
public class RateLimitController {
    @GetMapping("/byResource")
    @SentinelResource(value = "byResource", blockHandler = "handleException")
    public CommonResult byResource() {
        return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
    }

    @GetMapping("/rateLimit/byURL")
    @SentinelResource(value = "byURL", blockHandler = "handleException")
    public CommonResult beyURL() {
        return new CommonResult(200, "按URL限流测试OK", new Payment(2020L, "serial002"));
    }

    public CommonResult handleException(BlockException e) {
        return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");
    } 
}

启动模块,新建流控规则byResource,阈值QPS=1。快速点击/byResource,返回自定义兜底方法。

新建流控规则/rateLimit/byURL,阈值QPS=1。快速点击/rateLimit/byURL,发现还是返回默认流控信息。

按url限流只会返回默认流控信息。关闭模块,发现限流规则消失,说明限流规则是临时的。

自定义限流处理逻辑

RateLimitController类添加方法:

@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
                  blockHandlerClass = CustomerBlockHandler.class,
                  blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
    return new CommonResult(200, "按客户自定义OK", new Payment(2020L, "serial002"));
}

创建myhandler.CustomerBlockHandler类用于自定义限流处理逻辑:

public class CustomerBlockHandler {
    public static CommonResult handlerException(BlockException exception) {
        return new CommonResult(444, "按客户自定义,全局兜底方法---1");
    }

    public static CommonResult handlerException2(BlockException exception) {
        return new CommonResult(444, "按客户自定义,全局兜底方法---2");
    }
}

狂点http://localhost:8401/rateLimit/customerBlockHandler,发现返回自定义兜底方法的内容。

8. 服务熔断—Ribbon

Ribbon

新建模块cloudalibaba-provider-payment9003,添加依赖:

<dependencies>
    <!--SpringCloud Alibaba Nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <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>
</dependencies>

添加配置application.yml

server:
  port: 9003

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

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类PaymentMain9003

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

新建业务类controller.PaymentController

@RestController
public class PaymentController {
    public static HashMap<Long, Payment> hashMap = new HashMap<>();

    // 模拟数据库数据
    static {
        hashMap.put(1L, new Payment(1L, "111111111111111111111111111"));
        hashMap.put(2L, new Payment(2L, "222222222222222222222222222"));
        hashMap.put(3L, new Payment(3L, "333333333333333333333333333"));
    }

    @Value("${server.port}")
    private String serverPort;

    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
        Payment payment = hashMap.get(id);
        return new CommonResult<>(200, "from mysql, serverPort:" + serverPort, payment);
    }
}

启动9003,访问http://localhost:9003/paymentSQL/{id},分别尝试参数为1/2/3/4,看看是否正常。

仿照9003新建模块cloudalibaba-provider-payment9004,除了配置文件端口和主启动类需要修改,其它不动。

新建消费端模块cloudalibaba-consumer-nacos-order84,添加依赖:

<dependencies>
    <!--SpringCloud openfeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <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>
</dependencies>

添加配置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

新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类OrderNacosMain84

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

新建配置类config.ApplicationContextConfig

@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced // 一定要加才能负载均衡
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

【核心】新建业务类controller.CircleBreakerController

@RestController
@Slf4j
public class CircleBreakerController {
    @Resource
    private RestTemplate restTemplate;

    @Value("${service-url.nacos-user-service}") // 之前写到配置文件里了
    private String serverURL;

    @GetMapping(value = "/consumer/fallback/{id}")
    @SentinelResource(value = "fallback") // 没有配置
    public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class);
        if (id == 4) {
            throw new IllegalArgumentException("参数非法异常");
        } else if (result.getData() == null) {
            throw new NullPointerException("ID没有对应记录,空指针异常");
        }
        return result;
    }
}

启动9003、9004、84,访问http://localhost:84/consumer/fallback/1,为轮询。

没有配置

访问http://localhost:84/consumer/fallback/4,会报Error Page,体验不友好。

配置fallback方法

修改fallback的注解,在类中添加方法:

// @SentinelResource(value = "fallback") // 没有配置
@SentinelResource(value = "fallback", fallback = "handleFallback") // fallback只管业务异常
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
    // ...
}

// 本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
    Payment payment = new Payment(id, "null");
    return new CommonResult<>(444, "兜底异常handlerFallback,exception内容  " + e.getMessage(), payment);
}

重启模块,访问http://localhost:84/consumer/fallback/4,会返回友好出错信息。

配置blockHandler方法

修改fallback的注解,在类中添加方法:

@GetMapping(value = "/consumer/fallback/{id}")
// @SentinelResource(value = "fallback") // 没有配置
// @SentinelResource(value = "fallback", fallback = "handlerFallback") // fallback只管业务异常
@SentinelResource(value = "fallback", blockHandler = "blockHandler") // blockHandler只管sentinel控制台违规
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
    // ...
}

// 本例是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);
}

重启模块,访问http://localhost:84/consumer/fallback/4,会报Error Page。在sentinel控制台对资源fallback添加降级规则,异常数为2,时间窗口为1s,狂点/consumer/fallback/4,会有blockHandler方法进行兜底。

2种都配置

修改fallback的注解:

@GetMapping(value = "/consumer/fallback/{id}")
// @SentinelResource(value = "fallback") // 没有配置
// @SentinelResource(value = "fallback",) // fallback只管业务异常
// @SentinelResource(value = "fallback", blockHandler="blockHandler") // blockHandler只管sentinel控制台违规
@SentinelResource(value="fallback", fallback="handlerFallback", blockHandler="blockHandler") // 2种都配
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
    // ...
}

重启模块,访问http://localhost:84/consumer/fallback/1,在sentinel控制台对资源`fallback`添加流控规则,QPS为1。

快速点击http://localhost:84/consumer/fallback/4,发现blockHandler是优先于fallback的。

即2个都配的话,出现异常是进入blockHandler处理逻辑的。

忽略异常

@SentinelResource有属性exceptionsToIgnore 用于忽略某些异常,如:

@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
            exceptionsToIgnore = {IllegalArgumentException.class, NullPointerException.class})

传入需要忽略的异常类的数组,可以忽略处理这些异常类型。

9. 服务熔断—Feign

修改模块cloudalibaba-consumer-nacos-order84,引入OpenFeign(上面已提前引入)。在配置文件中激活Sentinel对Feign的支持(上面已激活)。在主启动类OrderNacosMain84上添加注解@EnableFeignClients激活OpenFeign。

新建接口service.PaymentService

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

新建兜底类service.PaymentFallbackService

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

CircleBreakerController中添加成员变量和方法:

// OpenFeign
@Resource
private PaymentService paymentService;

@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
    return paymentService.paymentSQL(id);
}

重启模块,访问http://localhost:84/consumer/paymentSQL/1,轮询返回结果。

然后关掉9003和9004,发生服务降级返回。

10. 规则持久化

重启模块之后,规则会消失。

将配置规则持久化进Nacos保存,只要刷新84某个REST地址就能获取到。

cloudalibaba-consumer-nacos-order84添加依赖:

<!-- Sentinel规则持久化进Nacos-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

修改配置文件application.yml,在sentinel节点下新增:

# sentinel规则持久化进行nacos
datasource:
  ds1:
    nacos:
      server-addr: localhost:8848
      dataId: ${spring.application.name}
      groupId: DEFAULT_GROUP
      data-type: json
      rule-type: flow

然后在nacos配置列表界面新增一条配置,Data ID:nacos-order-consumer,Group:DEFAULT_GROUP,配置格式:json,配置内容:

[
    {
        "resource": "GET:http://nacos-payment-provider/paymentSQL/{id}",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "stategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

注意,不支持配置规则直接写入到nacos中(除非用Java代码)。

Spring Cloud Alibaba Seata 处理分布式事务

1. 基本介绍

分布式之后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证

基本概念

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

三组件模型:

  • Transaction Coordinator(TC):事务协调者
    事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
  • Transaction Manager(TM):事务管理器
    控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
  • Resource Manager(RM):资源管理器
    控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

2. Seata Server安装

github上下载seata-server-1.0.0.zip,解压。

进入conf/路径下修改配置文件file.conf(拿file.conf.example的内容覆盖拷贝到file.conf中):

  • 修改service模块,将vgroupMapping.my_test_tx_group的值改为"my_tx_group"
  • 修改store模块,将mode的值改为db,修改具体的db连接信息。

修改配置文件registry.conf,将type改为nacos,将nacos的serverAddr改为"localhost:8848"

新建数据库seata,复制https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql上的建表语句运行。

先启动nacos,再运行bin\seata-server.bat

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

业务说明:用户下订单,系统远程调用库存服务减库存,远程调用账户服务扣减用户余额,最后修改订单状态为已完成。

创建数据库和业务表:

  • seata_order:存储订单的数据库```mysql

    t_order表

    CREATE TABLE t_order ( id bigint(11) NOT NULL AUTO_INCREMENT, user_id bigint(11) NULL DEFAULT NULL COMMENT ‘用户id’, product_id bigint(11) NULL DEFAULT NULL COMMENT ‘产品id’, count int(11) NULL DEFAULT NULL COMMENT ‘数量’, money decimal(11, 0) NULL DEFAULT NULL COMMENT ‘金额’, status int(1) NULL DEFAULT NULL COMMENT ‘订单状态:0-创建中,1-已完成’, PRIMARY KEY (id) ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8; ```

  • seata_storage:存储库存的数据库```mysql

    t_storage表

    CREATE TABLE t_storage ( id bigint(11) NOT NULL AUTO_INCREMENT, product_id bigint(11) NULL DEFAULT NULL COMMENT ‘产品id’, total int(11) NULL DEFAULT NULL COMMENT ‘总库存’, used int(11) NULL DEFAULT NULL COMMENT ‘已用库存’, residue int(11) NULL DEFAULT NULL COMMENT ‘剩余库存’, PRIMARY KEY (id) ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;

插入数据

INSERT INTO t_storage VALUES (1, 1, 100, 0, 100);


- seata_account:存储账户信息的数据库```mysql
# t_account表
CREATE TABLE `t_account`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id',
  `total` decimal(10, 0) NULL DEFAULT NULL COMMENT '总额度',
  `used` decimal(10, 0) NULL DEFAULT NULL COMMENT '已用额度',
  `residue` decimal(10, 0) NULL DEFAULT NULL COMMENT '剩余额度',
  PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;

# 插入数据
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);

3个库都要建各自的回滚日志表undo_log

CREATE TABLE IF NOT EXISTS `undo_log` (
    `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT 'increment id',
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME     NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

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

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

订单微服务

新建订单模块seata-order-service2001,添加依赖:

<dependencies>
    <!--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>
        <!--要排除掉,与seata server的版本相匹配-->
        <exclusions>
            <exclusion>
                <artifactId>seata-all</artifactId>
                <groupId>io.seata</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.1.0</version>
    </dependency>
    <!--open feign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--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>
    <!--mysql-druid-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.37</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

添加配置文件application.yml

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: my_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

将seata/conf下的配置文件file.conf拷贝到本项目resources中,修改service节点的事务组名称:

#修改自定义事务组名称
vgroupMapping.my_tx_group = "default"

将seata/conf下的配置文件registry.conf拷贝到本项目resources中。

新建包com.atguigu.springcloud.alibaba,在包下新建domain用于存放实体。在domain下新建类CommonResult

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
    private Integer code;
    private String  message;
    private T       data;

    public CommonResult(Integer code, String message)
    {
        this(code,message,null);
    }
}

domain下新建类Order

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status; //订单状态:0:创建中;1:已完结
}

alibaba包下新建dao包用于存放接口,在dao下新建接口OrderDao

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

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

resources目录下新建mapper文件夹用于存放mapper文件,新建OrderMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.atguigu.springcloud.alibaba.dao.OrderDao">
    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>

    <insert id="create">
        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>

alibaba包下新建service包,新建3个接口OrderServiceStorageServiceAccountService

public interface OrderService {
    void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService {
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService {
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

新建接口实现类impl.OrderServiceImpl

@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) {
        log.info("----> 新建订单");
        orderDao.create(order);

        log.info("----> 订单微服务开始调用[库存微服务],扣减count [start]");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----> 订单微服务开始调用[库存微服务],扣减count [end]");

        log.info("----> 订单微服务开始调用[账户微服务],扣减money [start]");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----> 订单微服务开始调用[账户微服务],扣减money [end]");

        log.info("----> 修改订单状态 [start]");
        orderDao.update(order.getUserId(), 0);
        log.info("----> 修改订单状态 [end]");
    }
}

alibaba包下新建controller包,新建OrderController类:

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

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

alibaba包下新建config包用于存放配置,新建MyBatisConfig类:

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

新建DataSourceProxyConfig类:

/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceProxyConfig {

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

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

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().
                                                 getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
}

alibaba包下新建主启动类SeataOrderMainApp2001

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

库存微服务

新建订单模块seata-storage-service2002,添加依赖同2001。

添加配置文件application.yml同2001,修改server.port为2002,spring.application.name为seata-storage-service,spring.datasource.url为jdbc:mysql://localhost:3306/seata_storage。添加file.confregistry.conf同2001。

新建包com.atguigu.springcloud.alibaba,新建domain包用于存放实体。在domain下新建类CommonResult同2001,新建实体类Storage

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

alibaba包下新建dao包用于存放接口,在dao下新建接口StorageDao

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

resources目录下新建mapper文件夹用于存放mapper文件,新建StorageMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.atguigu.springcloud.alibaba.dao.StorageDao">
    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Storage">
        <id 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>

alibaba包下新建service包,新建接口StorageService

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

新建接口实现类impl.StorageServiceImpl

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
    @Resource
    private StorageDao storageDao;

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

alibaba包下新建controller包,新建StorageController类:

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

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

alibaba包下新建config包用于存放配置,新建MyBatisConfigDataSourceProxyConfig同2001。

新建主启动类SeataStorageMainApp2002

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

账户微服务

新建订单模块seata-account-service2003,添加依赖同2001。

添加配置文件application.yml同2001,修改server.port为2003,spring.application.name为seata-account-service,spring.datasource.url为jdbc:mysql://localhost:3306/seata_account。添加file.confregistry.conf同2001。

新建包com.atguigu.springcloud.alibaba,新建domain包用于存放实体。在domain下新建类CommonResult同2001,新建实体类Account

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Long id;
    private Long userId; // 用户id
    private BigDecimal total; // 总额度
    private BigDecimal used; // 已用额度
    private BigDecimal residue; // 剩余额度
}

alibaba包下新建dao包用于存放接口,在dao下新建接口AccountDao

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

resources目录下新建mapper文件夹用于存放mapper文件,新建AccountMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.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>

alibaba包下新建service包,新建接口AccountService

public interface AccountService {
    // 扣减账户余额
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

新建接口实现类impl.AccountServiceImpl

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    @Resource
    private AccountDao accountDao;

    // 扣减账户余额
    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("------->account-service中扣减账户余额开始");
        // 模拟超时异常,全局事务回滚
        // 暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        accountDao.decrease(userId,money);
        log.info("------->account-service中扣减账户余额结束");
    }
}

alibaba包下新建controller包,新建AccountController类:

@RestController
public class AccountController {
    @Resource
    private AccountService accountService;

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

alibaba包下新建config包用于存放配置,新建MyBatisConfigDataSourceProxyConfig同2001。

新建主启动类SeataAccountMainApp2003

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

先启动nacos,seata,2001,2002,再启动本模块。

5.使用@GlobalTransactional

访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100,可以发现前台报错,但是数据库订单记录显示为未完成,但是库存和账户余额都扣了。

在2001的OrderServiceImpl类的create()方法上加上注解@GlobalTransactional,重启2001,再访问刚才的链接。发现订单不会创建,库存和余额也不会有变化。

注意,Nacos默认是AP的(临时注册),一旦调用微服务失败就会移除。

6. Seata原理

概念

  • TC——事务协调者,管理整个事务的生命周期,类似于班主任管理整个班级
  • TM——事务管理器,事务发起方,类似于老师,上课之前先让班主任通知到位
  • RM——资源管理器,事务参与者,类似于每个学生

基本步骤

  • TM开启分布式事务(TM向TC注册全局事务记录);
  • 按业务场景,编排数据库、服务等事务内资源(RM 向TC汇报资源准备状态);
  • TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务);
  • TC汇总事务信息,决定分布式事务是提交还是回滚;
  • TC通知所有RM提交/回滚资源,事务二阶段结束。

一阶段加载

在一阶段,Seata会拦截“业务SQL”

  1. 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成”before image”
  2. 执行“业务SQL”更新业务数据,在业务数据更新之后,
  3. 其保存成”after image”,最后生成行锁。

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

二阶段回滚

二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务SQL”,还原业务数据。

回滚方式便是用”before image”还原业务数据;但在还原前要首先要校验脏写,对比”数据库当前业务数据”和”after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不-致就说明有脏写,出现脏写就需要转人工处理。

二阶段提交

二阶段如是顺利提交的话,因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可