Spring Cloud Alibaba

Spring Cloud Alibaba

视频参考尚硅谷 周阳 老师的微服务:尚硅谷-周阳微服务
有一些我自己的心得体会,和老师提供的 源码笔记。

该篇为下篇,上篇为:SpringCloud

GitHub 源码已上传: github

GitHub : spring-cloud-alibaba/README-zh.md at 2.2.x · alibaba/spring-cloud-alibaba (github.com)

官网: Spring Cloud Alibaba

版本对应关系:

由于 Spring Boot 1 和 Spring Boot 2 在 Actuator 模块的接口和注解有很大的变更,且 spring-cloud-commons 从 1.x.x 版本升级到 2.0.0 版本也有较大的变更,因此我们采取跟 SpringBoot 版本号一致的版本:

1.5.x 版本适用于 Spring Boot 1.5.x 
2.0.x 版本适用于 Spring Boot 2.0.x
2.1.x 版本适用于 Spring Boot 2.1.x
2.2.x 版本适用于 Spring Boot 2.2.x
2021.x 版本适用于 Spring Boot 2.4.x

为什么会出现 SpringCloud Alibaba?

Spring Cloud Netflix 项目进入维护模式

主要功能:

  • 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

引入依赖:

dependencyManagement 中添加如下配置。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.7.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

一、Nacos

Nacos 服务注册中心

官方文档: Spring Cloud Alibaba Reference Documentation (spring-cloud-alibaba-group.github.io)

Nacos: Dynamic Naming and Configuration Service

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

Nacos = Eureka+Config +Bus

下载地址:https://github.com/alibaba/Nacos

1655541218434

启动:

进入软件的 bin 目录,打开 cmd 命令行执行:

startup.cmd -m standalone
单启动、非集群启动

web管理界面:http://localhost:8848/nacos

默认账号密码:nacos

注意注意: 安装目录不能有中文,否则启动不起来!!!!

案例演示:新建模块 9001 、9002,东西都一样

  • 建 module ---- cloudalibaba-provider-payment9001
  • 改 Pom
  • 写 YML
  • 主启动
  • 业务类

父模块POM:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

本模块 POM:

    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </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>

YAML:

server:
    port: 9001

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

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

主启动类增加 @SpringBootApplication 和 @EnableDiscoveryClient 注解

controller 层:

@RestController
@Slf4j
public class PaymentController {

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

    @GetMapping(value = "/payment/nacos/{id}")
    public String getPayment(@PathVariable("id") Integer id) {
        return "nacos registry, serverPort: " + serverPort + "\t id" + id;
    }
}

新建消费者模块:

  • 建 module ---- cloudalibaba-consumer-nacos-order83
  • 改 Pom
  • 写 YML
  • 主启动
  • 业务类

POM 和 9001 一模一样、主启动类一样

YAML :

server:
    port: 83


spring:
    application:
        name: nacos-order-consumer
    cloud:
        nacos:
            discovery:
                server-addr: localhost:8848


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

Nacos 自带负载均衡,因为集成了 Ribbon。因此需要在配置类中将 RestTemplate 注册到 IOC 容器中。

1655545967362

@Configuration
public class MyConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return  new RestTemplate();
    }
}

Controller 层:

@RestController
@Slf4j
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;
    // 从 配置文件中获取服务名
    @Value("${service-url.nacos-user-service}")
    private String serverURL;

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

}

Nacos 服务注册中心与其他对比:

1655546376218

Nacos 服务配置中心

案例:新建模块 cloudalibaba-config-nacos-client3377

POM:

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

配置文件需要俩个:

  • application
  • bootstrap

Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。

springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application

application:

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

bootstrap:

# nacos配置
server:
    port: 3377

spring:
    application:
        name: nacos-config-client
    cloud:
        nacos:
            discovery:
                server-addr: localhost:8848 #Nacos服务注册中心地址
            config:
                server-addr: localhost:8848 #Nacos作为配置中心地址
                file-extension: yaml #指定yaml格式的配置
    
    
    # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
    # 因此对应的配置文件为  nacos-config-client-dev.yaml

在 Nacos 中 dataId 的命名:

1655621846888

${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。
  • spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 propertiesyaml 类型。

1655621767187

Controller 层:

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

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

主启动类 @SpringBootApplication + @EnableDiscoveryClient

Nacos 作为服务配置中心时遇到的坑:

  1. file-extension 与 dataId 的后缀名保持一致。dataId 必须写 后缀名。如果一个是 yaml 一个是 yml 是不会自动识别的。
  2. spring-profiles-active 指明生产环境 要配置在 bootstrap 里面。

分类配置

实际开发中,通常一个系统会准备
dev开发环境
test测试环境
prod生产环境。

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

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

那怎么对这些微服务配置进行管理呢?

我们就可以利用 Nacos 的配置管理进行管理【命名空间、group、dataId】

NameSpace、Group、dataId 三者的关系 ?

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

1655626154712

默认情况:

Namespace=public,Group=DEFAULT_GROUP, 默认Cluster是DEFAULT

Nacos默认的命名空间是public,Namespace主要用来实现隔离。
比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个 Namespace,不同的Namespace之间是隔离的。

Group默认是 DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去

Service就是微服务;一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。
比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,
这时就可以给杭州机房的Service微服务起一个集群名称(HZ),
给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。

最后是Instance,就是微服务的实例。

dataId :

通过 spring.profiles.active 配置 切换环境 匹配 dataId

Group:

1655627418886

spring:
    profiles:
        active: info
        # active: dev # 指定生产环境
        # active: test # 指定测试环境
    application:
        name: nacos-config-client  # 服务名
    cloud:
        nacos:
            discovery:
                server-addr: localhost:8848 #Nacos服务注册中心地址
            config:
                server-addr: localhost:8848 #Nacos作为配置中心地址
                file-extension: yaml #指定yaml格式的配置
                group: TEST_GROUP

通过 group 指定 组名

nameSpace:

1655627844159

1655628543842

# nacos配置
server:
    port: 3377

spring:
    profiles:
        # active: info
         active: dev # 指定生产环境
        # active: test # 指定测试环境
    application:
        name: nacos-config-client  # 服务名
    cloud:
        nacos:
            discovery:
                server-addr: localhost:8848 #Nacos服务注册中心地址
            config:
                server-addr: localhost:8848 #Nacos作为配置中心地址
                file-extension: yaml #指定yaml格式的配置
                group: TEST_GROUP # 指定组名
                namespace: a274adaf-c8c9-4139-95dc-59857ad816b0 # 指定命名空间
    
    
    # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
    # nacos-config-client-dev.yaml

Nacos 集群 和 持久化配置

默认情况下,Nacos 内置了一个数据库 【derby 】来实现数据的存储,所以,如果启动多个默认配置下的 Nacos 节点,每一个节点都带有一个 derby 数据库,数据存储是存在一致性问题的。
为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。

证明是内嵌数据库:https://github.com/alibaba/nacos/blob/develop/config/pom.xml

内嵌了一个 derby 依赖:

        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
        </dependency>

derby 切换 mysql 数据库步骤:

  • 1.安装数据库,版本要求:5.6.5+

  • 2.初始化mysql数据库,数据库初始化文件:nacos-mysql.sql 在 nacos/conf 目录下

    • CREATE DATABASE nacos_config;`nacos_config`
      
  • 3.修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。

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&serverTimezone=UTC
db.user=root
db.password=1234

如果新增加的 配置文件 保存到 数据库中说明切换成功:

1655633637943

Linux版 Nacos + Mysql + Nginx 集群版

预计需要,1个Nginx+3个nacos注册中心+1个mysql

1655644927845

下载地址: Release 2.1.0 (Apr 29, 2022) · alibaba/nacos (github.com)

将 Nacos 的 Windows mysql 切换为 Linux 数据库,:

  • sql 语句同样在 conf 目录下。
  • 修改 application.propertie 文件,修改前进行备份!
    • 修改内容和 windows 一致

Linux Nacos 集群配置

  • 对 cluster.conf.example 文件进行拷贝,并对其进行修改

    • 192.168.200.132:3333
      192.168.200.132:4444
      192.168.200.132:5555
      
- IP 不能乱写,必须是你的虚拟机IP

集群启动,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例。
命令:./startup.sh -p 3333 表示启动端口号为3333的nacos服务器实例,端口号和上一步的cluster.conf配置的一致。

修改 startup.sh :

修改前

1655731619861

1655646580289

修改后:

1655646529358

1655646587768

注意: Nacos 2.0 之后不需要我们修改 startup.sh 文件。只需要 修改 cluster.conf.example 即可。

启动 三个 Nacos:

sh startup.sh -p 3333
sh startup.sh -p 4444
sh startup.sh -p 5555

查询启动个数:

ps -ef | grep nacos | grep -v grep | wc -l

Nginx 实现负载均衡:

对 nginx.conf.default 文件进行备份。

1655660529585

# 指定配置文件启动Nginx
./nginx  -c /usr/local/nginx/conf/nginx.conf

二、Sentinel

就是 Hystrix 的升级版,服务降级,服务熔断,流量限制。。。。

**帮助文档:**https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

下载地址:https://github.com/alibaba/Sentinel/releases

1656140975189

1656312070042

运行环境:

Java8、8080端口不能被占用

运行命令:

java -jar sentinel-dashboard-1.8.4.jar

访问地址:

http://localhost:8080/#/dashboard

初始账号密码:

sentinel

**新建模块初始化监控:**cloudalibaba-sentinel-service8401

POM:

    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </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>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>

YAML:

server:
  port: 8401

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

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

主启动类:@SpringBootApplication 、 @EnableDiscoveryClient

Controller 层简单的俩个方法返回 Json 串

启动后发现 Sentinel 并没有监控到,这是因为 Sentinel采用的懒加载说明 ,需要访问 一次才能监控到 。

http://localhost:8401/testA

1656313986426

流控规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XAL0iVqn-1661673327678)(C:%5C15_%E5%BE%AE%E6%9C%8D%E5%8A%A1%5CSpringCloud%5C%E7%AC%94%E8%AE%B0%5C1656314749618.png)]

1656314805309

直接 – 直接失败

1656315196473

默认的流控规则 就是 直接–快速失败【达到限流规则直接失败】

QPS 、单机阈值 为 1 表示 1s 内 只能处理一次请求,多了就会限流

1656315324743

并发线程数 – 单机阈值:

1656316209329

表示只有一个线程正在处理请求,如果在该线程处理的过程中,别的请求进来了,那么就会出错 ~

关联 – 直接失败

当关联的资源达到阈值时,就限流自己。

也就是当 /testB 限流时,/testA 也会限流自己。

1656316733358

当 /testB 1s发送多个请求时,/testA 就会限流

预热 Warm up

官网:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

默认coldFactor为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值

在WarmUpController 中的源码体现:

1656488313449

举例说明:

1656488236697

阀值为10+预热时长设置5秒。
系统初始化的阀值为10 / 3 约等于3,即阀值刚开始为3;然后过了5秒后阀值才慢慢升高恢复到10。

排队等待

匀速排队,阈值必须设置为QPS

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。

image

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

1656488998084

设置含义:/testB每秒处理1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。

阈值类型只能设置成 QPS !!!

降级规则

避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

慢调用比例

1656489602248

最大 RT : 最大响应时间 ,请求的响应时间大于该值则统计为慢调用 。

单位统计时长statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。

1656491131865

每个请求最大的 响应时间为 200 ms ,如果在 1s 内请求数大于1 并且 响应时间大于 200ms 就会进行熔断10s,超过 10s 进入办开展状态。

1656491237780

使用 Jmeter 测试

异常比例

1656489896405

当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。

经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态,【半开】),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

举例:

1656491662703

  • 异常比例达到 50 %
  • 请求数 > 5 进入熔断状态

1656491870925

100 % 出错

异常数

1656489979172

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

热点规则

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

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

兜底方法 分为系统默认和客户自定义,两种

之前的case,限流出问题后,都是用sentinel系统默认的提示:Blocked by Sentinel (flow limiting)

我们能不能自定? 类似 hystrix,某个方法出问题了,就找对应的兜底降级方法?

结论:
从HystrixCommand 到@SentinelResource

测试 Controller 层:

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

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

1656493713206

注意,配置热点规则时的资源名是 @SentinelResource 注解所指定的,之前都是 Mapping指定的

**含义:**当第一个参数点击率 每 s 超过 1 次 ,就执行 blockHandler 所指定的兜底方法

其他配置项:

我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样。

比如我们希望 p1 =1 限流,但是 p1 = 5 会有另外一个限流规则

1656494511700

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

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

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

系统规则

1656495674839

1656495655566

@SentinelResource 是对单个方法进行控制,那么 系统规则就是对整个应用进行控制

@SentinelResource

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

@SentinelResource 中指明了 blockHandler 会使用自定义的 兜底方法,如果没有就会使用系统默认的

按资源名限流

1656576834820

这样配置,如果 将8401 服务关掉,设置的流控规则也会消失

上面兜底方案的问题:【和Hystrix一样】

1 系统默认的,没有体现我们自己的业务要求。

2 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。

3 每个业务方法都添加一个兜底的,那代码膨胀加剧。

4 全局统一的处理方法没有体现。

自定义限流处理逻辑:

新建一个类 CustomerBlockHandler 用来处理限流

public class CustomerBlockHandler {

    public static CommonResult customerBlockHandler(BlockException e) {
        return new CommonResult(444, "自定义限流处理逻辑-----1");
    }

    public static CommonResult customerBlockHandler2(BlockException e) {
        return new CommonResult(444, "自定义限流处理逻辑-----1");
    }
}

注意:返回值类型 和 被处理的保持一致

    @GetMapping("/rateLimit/customerBlockHandler")
    @SentinelResource(value = "customerBlockHandler",
            // 自定义兜底方法的类名
            blockHandlerClass = CustomerBlockHandler.class,
            // 类中的方法名
            blockHandler = "customerBlockHandler")
    public CommonResult byUrl() {
        return new CommonResult(200, "按客户自定义限流处理逻辑");
    }

blockHandlerClass : 标注兜底方法的类名

blockHandler : 类中的方法名

Sentinel整合 Ribbon

环境搭配:

新建 Mode cloudalibaba-provider-payment9003/9004

POM:

<dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <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>

YAML: 不要忘记更改端口号


server:
    port: 9003

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

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

Controller 层:

package com.atguigu.springcloud.alibaba.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

/**
 * Handsome Man.
 * <p>
 * Author: YZG
 * Date: 2022/6/30 16:51
 * Description:
 */
@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    public static HashMap<Long, Payment> hashMap = new HashMap<>();

    static {
        hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
        hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
        hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
    }

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

主启动类一样。

测试:http://localhost:9003/paymentSQL/2

新建消费者模块: cloudalibaba-consumer-nacos-order84

POM:

    <dependencies>
        <!--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>

YAMl:

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

Controller:

/**
 * @auther zzyy
 * @create 2020-02-13 20:24
 */
@RestController
@Slf4j
public class CircleBreakerController
{
    public static final String SERVICE_URL = "http://nacos-payment-provider";

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

由于 应用本身 出现了异常,返回给用户不好的页面,需要服务降级。

fallback 管运行异常

blockHandler 管配置违规

fallback 管运行时异常
@RestController
public class CircleBreakerController {
    // 调用微服务名
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    // @SentinelResource(value = "fallback")
    // 此时并没有配置有关 sentinel 中的东西
    @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);
    }
}
blockHandler 管配置违规
@RestController
public class CircleBreakerController {
    // 调用微服务名
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    // @SentinelResource(value = "fallback")
    // 此时并没有配置有关 sentinel 中的东西
    // @SentinelResource(value = "fallback",fallback = "handlerFallback") //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;
    }

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

    public CommonResult blockHandler(@PathVariable  Long id, BlockException blockException) {
        Payment payment = new Payment(id,"null");
        return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException  "+blockException.getMessage(),payment);
    }
}

配置管理:

1656581588920

第一次 访问: http://localhost:84/consumer/fallback/4 会出现异常界面,但是如果快速的点俩次,就会执行 blockHandler 指定的方法。

若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。

Sentinel 整合 OpenFeign

修改消费端 84端口

POM:

        <!--SpringCloud openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

YAML:

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

主启动类:

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

创建带有 @FeignClient 标注的 服务接口:

@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)//调用中关闭9003服务提供者
public interface PaymentService
{
    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}

创建 PaymentFallbackService 实现 PaymentService 接口,实现兜底方法

@Component
public class PaymentFallbackService implements PaymentService
{
    @Override
    public CommonResult<Payment> paymentSQL(Long id)
    {
        return new CommonResult<>(444,"服务降级返回,没有该流水信息",new Payment(id, "errorSerial......"));
    }
}

Controller 层:

    @Resource
    private PaymentService paymentService;

    @GetMapping(value = "/consumer/openfeign/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
    {
        if(id == 4)
        {
            throw new RuntimeException("没有该id");
        }
        return paymentService.paymentSQL(id);
    }

Sentinel 持久化规则

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

如何启用?

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

修改 8401 的POM :

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

YAML:

 
server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard地址
        port: 8719
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

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

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

对 Nacos 业务规则配置:

1656656379375

[
    {
        "resource": "byUrl",
        "limitApp": "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:是否集群。

这样重启 8401 之后他的流控规则就不会消失了 ,前提是访问一次该请求,因为sentinel是懒加载

三、Seata

分布式事务的问题?

1656658646112

从之前初学 Java 所有应用对应一个数据库, 然后到应用程序对应多个数据库,最后每个服务对应一个数据库或者多个数据库,那么数据的一致性就成为了问题。

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

1656658716117

因此,一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

简介

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

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

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

Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;

Transaction Manager ™:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;

Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

分布式事务处理的过程:

1656659607552

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

下载

https://seata.io/zh-cn/blog/download.html

Server端存储模式(store.mode)现有 file、db、redis 三种(后续将引入 raft , mongodb),file模式无需改动,直接启动即可,下面专门讲下db和redis启动步骤。
注:

file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;

db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;

redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.

第一步: 修改 /conf 目录下的 file.conf 配置文件,修改前备份:

**主要修改:**自定义事务组名称+事务日志存储模式为db+数据库连接信息

1657259335962

1657259479342

MYsql 8.0 版本的修改url 以及 driverClassName :

url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=CST"
 
driverClassName = "com.mysql.cj.jdbc.Driver"

MySQL 5.6 + 版本 驱动以及 url :

driverClassName = "com.mysql.jdbc.Driver" 

url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true

如果是 mysql 8.0 的需要下载 8.0 的jar 包,放到安装目录下的 lib 目录下,

1657260882092

切记,一定要删除 5+ 版本的 sql jar包!!!

**第二步:**创建 seata 数据库,文件中提供好了sql 脚本

1657259557062

第三步: 修改 registry.conf 配置文件,记得备份:

1657259725276

第四步: 在每个数据库中创建 日志回滚表:

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;

环境准备

这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。

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

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

下订单—>扣库存—>减账户(余额)

seata_order:存储订单的数据库;

seata_storage:存储库存的数据库;

seata_account:存储账户信息的数据库。

sql 语句:

seata_order 下: 
-------------------------------------------------
CREATE TABLE t_order (
  `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
  `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
  `count` INT(11) DEFAULT NULL COMMENT '数量',
  `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
  `status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' 
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
 
SELECT * FROM t_order;

seata_storage 下: 
-------------------------------------------------
 
CREATE TABLE t_storage (
 `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
 `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
 `total` INT(11) DEFAULT NULL COMMENT '总库存',
 `used` INT(11) DEFAULT NULL COMMENT '已用库存',
 `residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 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;
 

seata_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=2 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;
 
 

微服务模块准备

新建模块:seata-order-service2001

POM:

    <dependencies>
        <!--nacos-config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!--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>8.0.28</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>

注意: seata-all 的版本一定要和你使用的 seata 版本相符

YAML :

server:
    port: 2001

spring:
    application:
        name: seata-order-service
    cloud:
        alibaba:
            seata:
                #自定义事务组名称需要与seata-server中的对应
                tx-service-group: fsp_tx_group
        nacos:
            discovery:
                server-addr: localhost:8848
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/seata_order?serverTimezone=GMT&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
        username: root
        password: 1234

feign:
    hystrix:
        enabled: false

logging:
    level:
        io:
            seata: info

mybatis:
    mapperLocations: classpath:mapper/*.xml
 
 

在 resources 下新建一个 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://127.0.0.1:3306/seata?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=CST"
    user = "root"
    password = "1234"
    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
  }
}

在 resources 下新建一个 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"
  }
}


其他步骤请参考 MindManager

测试:

正常情况下 : http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

1657351159070

测试超时异常情况下:

AccountServiceImpl 中沉睡 30s

1657351183311

由于 feign 的超时控制,因此会报异常

1657351315844

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

1657351356735

在 OrderServiceImpl 中增加 @GlobalTransactional

@Override
// name 随便起的名字,不能重复
// rollbackFor 处理那些异常进行回滚
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
    
    .....
    }

这时增加 @GlobalTransactional 注解之后,数据库中的订单表就不会在增加了‘

踩坑:

1、报错 Could not found any index in the table: t_order

不要把主键写在 mapper.xml 文件中

2、报错 Failed to fetch schema of t_order

在 application.yaml 中的 url 中增加 useInformationSchema=false

3、使用的是 seata 0.9 版本,使用 seata 1.4+ 版本的用法 :

9. SpringCloud Alibaba Seata处理分布式事务 · 语雀 (yuque.com)

拓展知识:

1657352560134

简单理解 ,被 @GlobalTransactional 标注的 就是 TM,一个数据库可以看成一个 RM,

seata-server.bat 就是 TC 。

Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。 默认的 是 AT 模式

AT 模式的阶段:

在一阶段 — 加载,Seata 会拦截“业务 SQL”,
1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成 “before image”,
2 执行“业务 SQL”更新业务数据,在业务数据更新之后,
3 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

1657353908754

第二阶段 ---- 提交

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

1657354101184

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

deBug : 在 AccountServiceImpl 中打断点

1657355221739

回退日志:

{
  "@class": "io.seata.rm.datasource.undo.BranchUndoLog",
  "xid": "192.168.200.1:8091:2110989022",
  "branchId": 2110989034,
  "sqlUndoLogs": [
    "java.util.ArrayList",
    [
      {
        "@class": "io.seata.rm.datasource.undo.SQLUndoLog",
        "sqlType": "UPDATE",
        "tableName": "`t_account`",
        "beforeImage": {
          "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
          "tableName": "`t_account`",
          "rows": [
            "java.util.ArrayList",
            [
              {
                "@class": "io.seata.rm.datasource.sql.struct.Row",
                "fields": [
                  "java.util.ArrayList",
                  [
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "id",
                      "keyType": "PrimaryKey",
                      "type": -5,
                      "value": [
                        "java.lang.Long",
                        1
                      ]
                    },
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "residue",
                      "keyType": "NULL",
                      "type": 3,
                      "value": [
                        "java.math.BigDecimal",
                        800
                      ]
                    },
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "used",
                      "keyType": "NULL",
                      "type": 3,
                      "value": [
                        "java.math.BigDecimal",
                        200
                      ]
                    }
                  ]
                ]
              }
            ]
          ]
        },
        "afterImage": {
          "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
          "tableName": "`t_account`",
          "rows": [
            "java.util.ArrayList",
            [
              {
                "@class": "io.seata.rm.datasource.sql.struct.Row",
                "fields": [
                  "java.util.ArrayList",
                  [
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "id",
                      "keyType": "PrimaryKey",
                      "type": -5,
                      "value": [
                        "java.lang.Long",
                        1
                      ]
                    },
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "residue",
                      "keyType": "NULL",
                      "type": 3,
                      "value": [
                        "java.math.BigDecimal",
                        700
                      ]
                    },
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "used",
                      "keyType": "NULL",
                      "type": 3,
                      "value": [
                        "java.math.BigDecimal",
                        300
                      ]
                    }
                  ]
                ]
              }
            ]
          ]
        }
      }
    ]
  ]
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

鲨瓜2号

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

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

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

打赏作者

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

抵扣说明:

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

余额充值