Spring Cloud 高级篇(SpringCloud Alibaba)

1.SpringCloud Alibaba入门简介

1.1.为什么会出现SpringCloud Alibaba

a. SpringCloud Netflix项目进入维护模式
https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now

Spring Cloud Netflix 项目进入维护模式
近日,Netflix宣布Hystrix 进入维护模式。Ribbon自 2016 年以来一直处于类似状态。 虽然 Hystrix 和 Ribbon 现在处于维护模式,但它们仍然在 Netflix 大规模部署。
Hystrix Dashboard 和 Turbine 已被 Atlas 取代。对这些项目的最后一次提交分别是 2 年前和 4 年前。Zuul 1 和 Archaius 1 都被不向后兼容的更高版本所取代。
以下 Spring Cloud Netflix 模块和相应的启动器将被置于维护模式:
1.spring-cloud-netflix-archaius
2.spring-cloud-netflix-hystrix-contract
3.spring-cloud-netflix-hystrix-dashboard
4.spring-cloud-netflix-hystrix-stream
5.spring-cloud-netflix-hystrix
6.spring-cloud-netflix-ribbon
7.spring-cloud-netflix-turbine-stream
8.spring-cloud-netflix-turbine
9.spring-cloud-netflix-zuul
这不包括 Eureka 或 concurrency-limits 模块。
什么是维护模式?
将模块置于维护模式,意味着 Spring Cloud 团队将不再向模块添加新功能。我们将修复block级别错误以及安全问题,我们也会考虑并审查来自社区的小型pull request。
我们打算继续支持这些模块,直到 Greenwich版本 被普遍采用至少一年。

b. 进入维护模式意味着什么

进入维护模式意味着SpringCloud Netflix将不再开发新的组件
我们都知道SpringCloud版本迭代算是比较快的,因而出现了很多重大ISSUE都还来不及Fix就又推另一个Release了。进入维护模式意思就是目前一直以后一段时间SpringCloud Netflix提供的服务和功能就这么多了,不再开发新的组件和功能了。以后将以维护和Merge分支Full Request为主
新组件功能将以其它替代平代替的方式实现

1.2.SpringCloud Alibaba带来了什么

a. 是什么

官网:
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
诞生:
2018.10.31,Spring Cloud Alibaba正式入驻了 Spring Cloud 官方孵化器,并在 Maven 中央库发布了第一个版本。

Spring Cloud for Alibaba 0.2.0 released
The Spring Cloud Alibaba project, consisting of Alibaba’s open-source components and several Alibaba Cloud products, aims to implement and expose well known Spring Framework patterns and abstractions to bring the benefits of Spring Boot and Spring Cloud to Java developers using Alibaba products.
Spring Cloud for Alibaba,它是由一些阿里巴巴的开源组件和云产品组成的。这个项目的目的是为了让大家所熟知的 Spring 框架,其优秀的设计模式和抽象理念,以给使用阿里巴巴产品的 Java 开发者带来使用 Spring Boot 和 Spring Cloud 的更多便利。

b. 能干嘛

服务限流降级:默认支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
消息驱动能力:基于 Spring Cloud Stream 为微服务应用架构消息驱动能力。
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网络任务。网络任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。

c. 去哪下
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

d. 怎么玩

Sentinel
阿里巴巴开源产品,把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Nacos
阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
RocketMQ
一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Dubbo
Apache Dubbo™ 是一款高性能 Java RPC 框架。
Seata
阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
Alibaba Cloud OSS
阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX
阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
Alibaba Cloud SMS
覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

1.3.SpringCloud Alibaba学习资料

a. 官网
https://spring.io/projects/spring-cloud-alibaba#overview


Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
SpringCloud Alibaba进入了SpringCloud官方孵化器,并且毕业了

b. 英文
1). https://github.com/alibaba/spring-cloud-alibaba
2). https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

c. 中文
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

2.SpringCloud Alibaba Nacos服务注册和配置中心

2.1.Nacos简介

a. 为什么叫Nacos
前四个字母分别为Naming和Configuration的前两个字母,最后的s为Servcie。

b. 是什么

一个更易于构建云原生应用的动态服务实现、配置管理和服务管理平台。
Nacos:Dynamic Naming and Configuration Service
Nacos就是注册中心 + 配置中心的组合 等价于 Nacos = Eureka + Config + Bus

c. 能干嘛

  • 替代Eureka做服务注册中心
  • 替代Config做服务配置中心

d. 去哪下

https://github.com/alibaba/Nacos
官网文档

f. 各种注册中心比较

服务注册与发现框架 CAP模型 控制台管理 社区活跃度
Eureka AP 支持 低(2.x版本闭源)
Zookeeper CP 不支持
Consul CP 支持
Nacos AP 支持
据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验

2.2.安装并运行Nacos

环境要求:本地已安装Java8+Maven环境
a. 先从官网下载Nacos
https://github.com/alibaba/nacos/releases

b. 解压安装包,直接运行bin目录下的startup.cmd

startup.cmd -m standalone

c. 命令运行成功后直接访问http://localhost:8848/nacos
默认账户密码都是nacos

d. 结果页面

2.3.Nacos作为服务注册中心演示

2.3.1.官方文档

https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_dependency_management

2.3.2.基于Nacos的服务提供者

a. 新建Module(cloudalibaba-provider-payment9001)

b. POM
1). 父POM
必须引入SpringCloud Alibaba依赖:

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

2). 本模块POM
引入SpringCloud Alibaba Nacos依赖:

        <!-- SpringCloud Aibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

c. YML

server:
  port: 9001

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

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

d. 主启动

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {

    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9001.class, args);
    }
}

f. 业务类

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

g. 测试

  • 访问http://localhost:9001/payment/nacos/1
  • nacos控制台
  • nacos服务注册中心 + 服务提供者9001都OK了

为了下一章节演示nacos的负载均衡,参照9001新建9002
1.取巧不想新建重复体力劳动,直接拷贝虚拟端口映射



2.但是直接拷贝虚拟端口映射实际使用的还是9001,可能会出现问题,我们还是新建cloudalibaba-provider-payment9002

2.3.3.基于Nacos的服务消费者

a. 新建Module(cloudalibaba-consumer-nacos-order83)

b. POM
添加的部分代码

        <!-- SpringCloud Aibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.neo.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

为什么nacos支持负载均衡?
因为阿里的nacos整合得特别好,后面技术都会吸收前面技术的优点,天生一出来就自带负载均衡,何以见得?

nacos-discovery包天生集成了netflix-ribbon包,只要是ribbon的话,一支持负载均衡,二支持RestTemplate(RESTful风格的远程调用)

c. YML

server:
  port: 83

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

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

d. 主启动

@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {

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

f. 业务类
1). ApplicationContextBean

@Configuration
public class ApplicationContextConfig {

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

2). OrderNacosController

@RestController
@Slf4j
public class OrderNacosController {

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

}

g. 测试
1). nacos控制台

2). 访问http://localhost:83/consumer/payment/nacos/13
83访问9001/9002负载轮询OK

2.3.4.服务注册中心对比

各种注册中心对比
a. Nacos全景图所示

b. Nacos和CAP
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集成 支持 不支持 支持 支持 不支持

c. 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'

2.4.Nacos作为服务配置中心演示

2.4.1.Nacos作为配置中心-基础配置

a. 新建Module(cloudalibaba-config-nacos-client3377)

b. POM
添加的部分依赖:

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

c. YML
配置bootstrap.yml和application.yml
1). 为什么配置两个

Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application

2). 配置文件添加
2.1). bootstrap.yml

# nacos配置
server:
  port: 3377

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

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

2.2). application.yml

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

d. 主启动

@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {

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

e. 业务类

@RestController
@RefreshScope // 支持Nacos的动态刷新功能
public class ConfigClientController {

    @Value("${config.info}")
    private String configInfo;

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

f. 在Nacos中添加配置信息
Nacos中的匹配规则
1). 理论
Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则
官网:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html

说明:之所以需要配置 spring.application.name ,是因为它是构成 Nacos 配置管理 dataId字段的一部分。

在 Nacos Spring Cloud 中,dataId 的完整格式如下:

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

最后公式:
${spring.application.name}-${spring.profiles.active}-${spring.cloud.nacos.config.file-extension}

2). 实操
2.1). 配置新增
根据公式:${spring.application.name}-${spring.profiles.active}-${spring.cloud.nacos.config.file-extension}
设置DataId:nacos-config-client-dev.yaml

2.1). Nacos界面配置对应

小总结:

g. 测试
启动前需要在nacos客户端-配置管理-配置列表栏目下有对应的yaml配置文件
运行cloud-config-nacos-client3377的主启动类
调用http://localhost:3377/config/info接口查看配置信息

h. 自带动态刷新
修改下Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新

2.4.2.Nacos作为配置中心-分类配置

a. 问题:多环境多项目管理

问题1:
实际开发中,通常一个系统会准备:dev开发环境、test测试环境、prod生产环境;
如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:
一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境...
那么怎么对这些微服务配置进行管理呢?

b. Nacos的图形化管理界面
1). 配置管理

2). 命名空间

c. Namespace + Group + Data ID 三者关系?为什么这么设计?

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

默认情况: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,就是微服务的实例。

d. 三种方案加载配置
1). DataID方案
指定spring.profiles.active和配置文件的DataID来使不同环境下读取不同的配置
1.1). 默认空间+默认分组+新建devtest两个DataID
1.1.1). 新建dev配置DataID

1.1.2). 新建test配置DataID

1.2). 通过spring.profiles.acvice属性就能进行多环境下配置文件的读取

1.3). 测试
访问http://localhost:3377/config/info,配置什么就加载什么

2). Group方案
通过Group实现环境区分
2.1). 新建Group

新建配置

2.2). 在nacos图形界面控制台上面新增配置文件DataID

2.3). bootstrap.yml+application.yml
在config下增加一条group的配置即可,可配置为DEV_GROUP或TEST_GROUP

访问http://localhost:3377/config/info

3). Namespace方案
3.1). 新建dev、test的Namespace

注意下面的命名空间ID

3.2). 回到服务管理-服务列表查看

3.3). 按照域名配置填写

3.4). YML

访问http://localhost:3377/config/info

2.5.Nacos集群和持久化配置(重要)

2.5.1.官网说明

https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html
官网架构图:

真实情况:

部署环境说明
https://nacos.io/zh-cn/docs/deployment.html

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

重点说明

  • 1.安装数据库,版本要求:5.6.5+
  • 2.初始化mysql数据库,数据库初始化文件:nacos-mysql.sql
  • 3.修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。

2.5.2.Nacos持久化配置

Nacos默认自带的是嵌入式数据库derby
a. 切换derby到mysql配置步骤
1). nacos\config目录下找到sql脚本
执行脚本nacos-mysql.sql

CREATE DATABASE `nacos_config`;
USE `nacos_config`;

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info   */
/******************************************/
CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) DEFAULT NULL,
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) DEFAULT NULL,
  `c_use` varchar(64) DEFAULT NULL,
  `effect` varchar(64) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  `c_schema` text,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_aggr   */
/******************************************/
CREATE TABLE `config_info_aggr` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  `content` longtext NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_beta   */
/******************************************/
CREATE TABLE `config_info_beta` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_tag   */
/******************************************/
CREATE TABLE `config_info_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_tags_relation   */
/******************************************/
CREATE TABLE `config_tags_relation` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = group_capacity   */
/******************************************/
CREATE TABLE `group_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = his_config_info   */
/******************************************/
CREATE TABLE `his_config_info` (
  `id` bigint(64) unsigned NOT NULL,
  `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL,
  `group_id` varchar(128) NOT NULL,
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL,
  `md5` varchar(32) DEFAULT NULL,
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `src_user` text,
  `src_ip` varchar(50) DEFAULT NULL,
  `op_type` char(10) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = tenant_capacity   */
/******************************************/
CREATE TABLE `tenant_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
	`username` varchar(50) NOT NULL PRIMARY KEY,
	`password` varchar(500) NOT NULL,
	`enabled` boolean NOT NULL
);

CREATE TABLE `roles` (
	`username` varchar(50) NOT NULL,
	`role` varchar(50) NOT NULL,
	UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
    `role` varchar(50) NOT NULL,
    `resource` varchar(255) NOT NULL,
    `action` varchar(8) NOT NULL,
    UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

2). nacos\config目录下找到application.properties
增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
注:由于使用了MySQL最新版驱动,在项目代码-数据库连接 URL 后,加上(注意大小写必须一致)&serverTimezone=UTC

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

b. 启动Nacos,可以看到全新的空记录界面
以前记录是存进derby,现在是持久化到MySQL数据库

2.5.3.Linux版Nacos+MySQL生产环境配置

需要1台Nginx + 3个nacos注册中心 + 1个MySQL
a. Nacos下载Linux版

1). 下载nacos-server-2.0.3.tar.gz

wget https://github.com/alibaba/nacos/releases/tag/2.0.3

2). 解压安装

tar -zxvf nacos-server-2.0.3.tar.gz
mv nacos /opt/mynacos/

b. 集群配置步骤(重点)
1). Linux服务器上mysql数据库配置
nacos/config目录下找到nacos-mysql.sql脚本,在Linux服务器MySQL数据库执行

2). application.properties配置

添加mysql数据源的url、用户名和密码。
注:由于使用了MySQL最新版驱动,在项目代码-数据库连接 URL 后,加上(注意大小写必须一致)&serverTimezone=UTC

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

3). Linux服务器上nacos的集群配置cluster.conf
梳理出3台nacos集群的不同服务端口号,复制出cluster.conf

添加3台nacos机器服务端口号

注:IP不能写127.0.0.1,必须是Linux命令hostname -i能够识别的IP

4). 编辑Nacos的启动脚本startup.sh,使它能够接收不同的启动端口
nacos/bin目录下有startup.sh,平时单机版的启动,都是./startup.sh即可。但是,集群启动,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例。
命令:./startup.sh -p 3333 表示启动端口号3333的nacos服务器实例,和上一步的cluster.conf配置的一致。

修改nacos/bin/startup.sh脚本:

执行方式:

5). Nginx的配置,由它作为负载均衡器
/usr/local/nginx/conf目录下,修改nginx.conf配置文件

按照指定配置文件启动nginx:

[jessy@localhost sbin]$ pwd
/usr/local/nginx/sbin
[jessy@localhost sbin]$ ./nginx -c /usr/local/nginx/conf/nginx.conf

6). 截止到此处,1个Nginx+3个nacos注册中心+1个mysql
启动3台nacos:

6.1). 测试通过nginx访问nacos:
http://192.168.200.128:1111/nacos/#/login

6.2). 新建配置测试



linux服务器的mysql插入了新增的配置记录,测试通过

c. 测试
微服务cloudalibaba-provider-payment9002启动注册进nacos集群
1). 修改yml
配置Nacos地址,换成nginx的1111端口

2). 结果
nacos-payment-provider微服务成功注册进了Nacos集群

d. 高可用小总结

3.SpringCloud Alibaba Sentinel实现熔断与限流

3.1.Sentinel

1.官网
https://github.com/alibaba/Sentinel
中文:https://github.com/alibaba/Sentinel/wiki/介绍

Hystrix:
1.需要我们程序员自己手工搭建监控平台
2.没有一套web界面可以给我们进行更加细粒度化的配置流控、速率控制、服务熔断、服务降级等
Sentinel:
1.单独一个组件,可以独立出来
2.直接界面化的细粒度统一配置
约定>配置>编码
都可以写在代码里面,但是本次还是大规模的学习使用配置和注解的方式,尽量少写代码

2.是什么
就是之前的Hystrix

3.去哪下
https://github.com/alibaba/Sentinel/releases

4.能干嘛

5.怎么玩
https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
服务使用中的各种问题:

  • 服务雪崩
  • 服务降级
  • 服务熔断
  • 服务限流

3.2.安装Sentinel控制台

1.sentinel组件由两部分构成
Sentinel分为两个不部分(后台和前台8080):

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

2.安装步骤
2.1.下载
https://github.com/alibaba/Sentinel/releases,下载到本地sentinel-dashboard-1.8.2.jar

2.2.运行命令
前提:Java8运行环境正常、8080端口不能被占用
命令:java -jar sentinel-dashboard-1.8.2.jar

2.3.访问sentinel管理界面
http://locahost:8080,登录账户密码均为sentinel

3.3.初始化演示工程

1.启动Nacos8848成功
http://localhost:8848/nacos/#/login

2.Module
2.1.cloudalibaba-sentinel-service8401

2.2.POM
添加nacos、sentinel依赖:

        <!-- nacos-discovery -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!-- sentinel-datasource-nacos后续做持久化使用 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

2.3.YML

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

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

2.4.主启动

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

2.5.业务类FlowLimitController

@RestController
public class FlowLimitController {

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

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

3.启动Sentinel8080
java -jar sentinel-dashboard-1.8.2.jar

4.启动微服务8401

5.启动8401微服务后查看sentinel控制台
Sentinel采用的懒加载机制

执行一次访问即可
http://localhost:8401/testA和http:/localhost:8401/testB
效果:

结论:
sentinel8080正在监控微服务8401

3.4.流控规则⭐

3.4.1.基本介绍

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

3.4.2.流控模式

1). 直接(默认)
1.1). 直接->快速失败(系统默认)

表示1秒钟内查询1次就是OK,如果超过次数1,就直接-快速失败,报默认错误

1.2). 测试
快速点击访问http://localhost:8401/testA,结果出现Blocked by Sentinel (flow limiting)

思考:
a. 直接调用默认报错信息,技术方面OK,但是是否应该由我们自己的后续处理?类似有个fallback的兜底方法

2). 关联
2.1). 是什么
当与A关联的资源B达到阈值时,就限流A自己,即:B惹事,A挂了

2.2). 配置A

设置效果:
当关联资源/testB的QPS阈值超过1时,就限流/testA的REST访问地址,当关联资源达到阈值后限制配置好的资源名

2.3). postman模拟并发密集访问testB

先访问testB成功

postman里新建多线程集合组,将访问地址添加进新线程组,并运行

大批量线程高并发访问B,导致A失效了

2.4). 运行后发现testA挂了
点击访问http://localhost:8401/testA
结果:Blocked by Sentinel (flow limiting)

3). 链路
多个请求调用了同一个微服务

3.4.3.流控效果

1). 直接->快速失败(默认的流控处理)

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

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

2.2). 官网

默认coldFactor为3,即请求 QPS 从 threshold/3 开始,经过预热时长逐渐升至设定的 QPS 阈值。
限流 冷启动:https://github.com/alibaba/Sentinel/wiki/Flow-Control

2.3). 源码

com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController

2.4). WarmUp配置

默认coldFactor为3,即请求 QPS 从 (threshold/3) 开始,经过预热时长逐渐升至设定的 QPS 阈值。
案例,阈值为10+预热时长设置5秒。
系统初始化的阈值为10/3约等于3,即阈值刚开始为3;然后过了5秒后阈值才慢慢升高恢复到10

a. 多次点击http://localhost:8401/testB
刚开始不行,后续慢慢OK

b. 应用场景
如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是为了保护系统,慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。

3). 排队等待
大家一起去大学食堂排队打饭,大家都是排成一条直线匀速通过

匀速排队,让请求以均匀的速度通过,阈值类型必须设成QPS,否则无效。
设置含义:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。

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

3.2). 官网

3.3). 源码
com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

3.4). 测试
QPS达到阈值,请求排队等待通过

3.5.降级规则⭐

3.5.1.官网
https://github.com/alibaba/Sentinel/wiki/熔断降级

3.5.2.基本介绍

慢调用比例(秒级)

  • 统计时长内,实际请求数目大于最小请求数目,慢调用比例 > 比例阈值时,触发降级
  • 慢调用:当调用的时间(响应的实际时间)> 设置的RT时,这个调用叫做慢调用
  • 慢调用比例:在所有调用中,慢调用占有实际的比例 = 慢调用次数 / 调用次数
  • 比例阈值:自己设定的,慢调用次数 / 调用次数 = 比例阈值

异常比例(秒级)

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

异常数(分钟级)

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

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
进入熔断状态判断依据:
①当统计时长内,实际请求数目大于最小请求数目,慢调用比例 > 比例阈值 ,进入熔断状态
②熔断状态:在接下来的熔断时长内请求会自动被熔断
③探测恢复状态:熔断时长结束后进入探测恢复状态
④结束熔断:在探测恢复状态,如果接下来的一个请求响应时间小于设置的慢调用 RT,则结束熔断;否则继续熔断。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

Sentinel的断路器是没有半开状态的

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

3.5.3.降级策略实战
a. 慢调用比例
1). 是什么

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

2). 测试
2.1). 代码

    @GetMapping("/testD")
    public String testD() throws InterruptedException {
        // 暂停几秒钟
        TimeUnit.SECONDS.sleep(1);
        log.info("testD 测试慢调用比例");
        return "------testD";
    }

2.2). 配置

2.3). jmeter压测
①设置线程组
每秒10个请求:

②设置测试地址

③测试
Ⅰ.运行jmeter线程组

Ⅱ.控制台显示

Ⅲ.浏览器测试熔断

关闭jmeter线程压测,再进行浏览器访问

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

b. 异常比例
1). 是什么

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

2). 测试
2.1). 代码

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

2.2). 配置

2.3). jmeter压测
Ⅰ.运行jmeter线程组

Ⅱ.浏览器测试熔断

关闭jmeter线程压测,再进行浏览器访问

2.4). 结论

异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
按照上述配置,单独访问一次,必然调一次报错一次(int age = 10 / 0)。
开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件,断路器开启(保险丝跳闸),微服务不可用,不再报错error而是服务降级了。

c. 异常数
1). 是什么

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

2). 测试
2.1). 代码

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

2.2). 配置

2.3). 浏览器测试熔断
第一次访问http://localhost:8401/testE,报错error

单位时间秒内连续访问2次,进入熔断后降级

3.6.热点key限流

3.6.1.基本介绍

3.6.2.官网
https://github.com/alibaba/Sentinel/wiki/热点参数限流

兜底方法

分为系统默认和客户自定义两种
之前的case,限流出现问题后,都是用sentinel系统默认的提示:Blocked by Sentinel(flow limiting)
我们能不能自定义?类似hystrix,某个方法出问题了,就找对应的兜底降级方法?
结论
从HystrixCommand到@SentinelResource

3.6.3.代码
com.alibaba.csp.sentinel.slots.block.BlockException

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

    public String deal_testHotKey(String p1, String p2, BlockException exception) {
        return "------deal_testHotKey,/(ㄒoㄒ)/~~"; //sentinel系统默认的提示:Blocked by Sentinel(flow limiting)
    }

3.6.4.配置

1). @SentinelResource(value = "testHotKey")
异常达到了前台用户界面看到不友好

2). @SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理,使用了我们自定义的降级方法

3.6.5.测试
❌error

http://localhost:8401/testHotKey?p1=abc
http://localhost:8401/testHotKey?p1=abc&p2=33
✔right
http://localhost:8401/testHotKey?p2=abc

3.6.6.参数例外项
上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流
a. 特殊情况
普通:超过1秒钟一个后了,达到阈值1后马上被限流
我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样
特例:假如当p1的值等于5时,它的阈值可以达到200

b. 配置

c. 测试
http://localhost:8401/testHotKey?p1=5
http://localhost:8401/testHotKey?p1=3
当p1等于5的时候,阈值变为200
当p1不等于5的时候,阈值就是平常的1

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

3.6.7.其它
手贱添加异常:int age = 10/0;

@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
RuntimeException
int age = 10/0,这个是Java运行时异常RuntimeException,@SentinelResource不管
总结
@SentinelResouce主管配置出错,运行出错该走异常走异常

3.7.系统规则

3.7.1.是什么
https://github.com/alibaba/Sentinel/wiki/系统自适应限流

3.7.2.各项配置参数说明

3.7.3.配置全局QPS

3.8.@SentinelResource

3.8.1.按资源名称限流+后续处理
启动Nacos,启动Sentinel
a. 修改微服务8401
1). pom.xml引入自定义的api通用包

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

2). 增加业务类RateLimitController

@RestController
public class RateLimitController {

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

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

b. 配置流控规则
1). 配置步骤

2). 图形配置和代码关系
表示1秒钟内查询次数大于1,就跑到我们自定义的处理限流

c. 测试
c1. 1秒钟点击1下,OK
c2. 超过上述,疯狂点击,返回了自定义的限流处理信息,限流发生
{"code":444,"message":"com.alibaba.csp.sentinel.slots.block.flow.FlowException服务不可用","data":null}

d. 额外问题
此时关闭微服务8401,Sentinel控制台流控规则消失了

3.8.2.按照Url地址限流+后续处理
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息
a. 业务类RateLimitController

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

b. Sentinel控制台配置

c. 测试
疯狂点击http://localhost:8401/rateLimit/byUrl
结果:会返回Sentinel自带的限流处理结果

3.8.3.上面兜底方案面临的问题

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

3.8.4.客户自定义限流处理逻辑
创建CustomerBlockHandler类用于自定义限流处理逻辑
a. 自定义限流处理类
com.neo.springcloud.alibaba.myhandler.CustomerBlockHandler

public class CustomerBlockHandler {

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

b. RateLimitController

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

c. Sentinel控制台配置

d. 说明
测试后自定义的出来了

3.8.5.更多注解属性说明
https://github.com/alibaba/Sentinel/wiki/注解支持

所有的代码都要用try-catch-finally方式进行处理

Sentinel主要有三个核心API

  • SphU定义资源
  • Tracer定义统计
  • ContextUtil定义上下文

3.9.服务熔断功能⭐

Sentinel整合ribbon+openFeign+fallback

3.9.1.Ribbon系列

a. 启动nacos和sentinel

b. 提供者9003/9004
b1. 新建cloudalibaba-provider-payment9003/9004

b2. POM

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

b3. YML
两个微服务端口分别为9003、9004

server:
  port: 9003

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

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

b4. 主启动

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

b5. 业务类

@RestController
public class PaymentController {

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

    public static HashMap<Long, Payment> hashMap = Maps.newHashMap();

    static {
        hashMap.put(1L, new Payment(1L, "3lzwvpo7hu2ty5ab0idr4fmexgsck1"));
        hashMap.put(2L, new Payment(2L, "ptc6h3lfz284b0g7j9yrdowsaik5ue"));
        hashMap.put(3L, new Payment(3L, "7g1au46qbjzkw2shodptfnlve58309"));
    }

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

b6. 测试
访问地址http://localhost:9003/paymentSQL/1

c. 消费者84
c1. 新建cloudalibaba-consumer-nacos-order84

c2. POM

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

c3. YML

server:
  port: 84

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

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

c4. 主启动

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

c5. 业务类
1). ApplicationContextConfig

@Configuration
public class ApplicationContextConfig {

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

2). CircleBreakerController

@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    private String serverURL;

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class, id);

        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常...");
        } else if (result.getData() == null) {
            throw new NullPointerException("NullPointerException, 该ID没有对应记录, 空指针异常");
        }
        return result;
    }
}

2.1). 目的
Ⅰ.fallback处理运行异常
Ⅱ.blockHandler处理Sentinel配置违规

2.2). 测试
访问http://localhost:84/consumer/fallback/1,结果微服务84带轮询负载均衡算法,成功访问了9003、9004。

由于@SentinelResource(value = "fallback")没有任何配置,访问http://localhost:84/consumer/fallback/4http://localhost:84/consumer/fallback/5,返回客户error页面,很不友好

2.3). 配置fallback属性
@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback处理运行异常

@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    private String serverURL;

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback处理运行异常
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/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);
    }
}

运行结果:

2.4). 配置blockHandler属性
@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只负责Sentinel配置违规

@RestController
@Slf4j
public class CircleBreakerController {

    @Value("${service-url.nacos-user-service}")
    private String serverURL;

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@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(serverURL + "/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 e) {
        Payment payment = new Payment(id, "null");
        return new CommonResult(444, "blockHandler-Sentinel限流,无此流水,blockException:" + e.getMessage(), payment);
    }
}

Sentinel配置fallback接口熔断规则:

运行结果:

2.5). 同时配置fallback和blockHandler属性
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")

Sentinel配置fallback接口流控规则:

测试1:每秒请求QPS>1访问http://localhost:84/consumer/fallback/1,blockHandler生效

测试2:正常访问http://localhost:84/consumer/fallback/4,fallback生效

测试3:每秒请求QPS>1访问http://localhost:84/consumer/fallback/4,blockHandler有效

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

2.6). exceptionsToIgnore异常忽略属性
exceptionsToIgnore = {IllegalArgumentException.class},忽略IllegalArgumentException异常,不走fallback方法

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback处理运行异常
    //@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只负责Sentinel配置违规
    @SentinelResource(value = "fallback",
            fallback = "handlerFallback",
            blockHandler = "blockHandler",
            exceptionsToIgnore = {IllegalArgumentException.class})
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class, id);

        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常...");
        } else if (result.getData() == null) {
            throw new NullPointerException("NullPointerException, 该ID没有对应记录, 空指针异常");
        }
        return result;
    }

3.9.2.Feign系列

修改84模块

  • 84消费者调用提供者9003
  • Feign组件一般是消费侧

a. POM
增加openfeign依赖:

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

注释掉热部署spring-boot-devtools依赖,启动时openfeign配置类FeignAutoConfiguration$HystrixFeignTargeterConfiguration.class可能因为热部署会报错

b. YML
激活Sentinel对Feign的支持

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

c. 主启动
启动类上增加@EnableFeignClients,启用feign

d. 业务类
1). feign实现REST远程调用
feign:接口+注解
增加PaymenService接口:

@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {

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

为了避免与业务逻辑耦合,增加全局统一降级回调方法类:

@Component
public class PaymentFallbackService implements PaymentService {

    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
        return new CommonResult<>(4444, "服务降级返回,PaymentFallbackService", new Payment(id, "errorSerial"));
    }
}

2). Controller

    @Resource
    private PaymentService paymentService;

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

e. 测试
请求地址http://localhost:84/consumer/paymentSQL/2,可以正常访问

假设:故意关闭9003、9004微服务提供者
测试:测试84调用9003、9004,84消费侧是否自动降级

结论:84消费侧会自动降级,不会被耗死

3.9.3.熔断框架比较

Sentinel Hystrix resilience4j
隔离策略 信号量隔离(并发线程数限流) 线程池隔离/信号量隔离 信号量隔离
熔断降级策略 基于时间响应、异常比率、异常数 基于异常比率 基于异常比率、响应时间
实时统计实现 滑动窗口(LeapArray) 滑动窗口(基于RxJava) Ring Bit Buffer
动态规则配置 支持多种数据源 支持多种数据源 有限支持
扩展性 多个扩展点 插件形式 接口形式
基于注解的支持 支持 支持 支持
限流 基于QPS、支持基于调用关系的限流 有限的支持 Rate Limiter
流量整形 支持预热模式、匀速器模式、预热排队模式 不支持 简单的Rate Limiter模式
系统自适应保护 支持 不支持 不支持
控制台 提供开箱即用的控制台、可配置规则、查看秒级监控、机器发现等 简单的监控查看 不提供控制台,可对接其它监控系统

3.10.规则持久化

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

a. 方案

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

b. 步骤
修改cloudalibaba-sentinel-service8401

1). POM

        <!-- sentinel-datasource-nacos做持久化使用 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

2). YML
添加Nacos数据源配置

spring:
  cloud:
    sentinel:
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: ${spring.application.name}
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

3). 添加Nacos业务规则配置

[
    {
        "resource": "/rateLimit/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表示排队等待

启动8401,刷新sentinel,发现业务规则有了

4). 测试
快速访问接口http://localhost:8401/rateLimit/byUrl

停止8401再看sentinel

重新启动8401再看sentinel

  • 乍一看还是没有流控规则
  • 调用http://localhost:8401/rateLimit/byUrl后刷新sentinel,流控规则出现了,持久化验证通过

4.SpringCloud Alibaba Seata处理分布式事务

4.1.分布式事务问题

a. 分布式前

  • 单机单库没有这个问题
  • 从1: 1 -> 1: N -> N: N

b. 分布式后
用例
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
仓储服务:对给定的商品扣除仓储数量。
订单服务:根据采购需求创建订单。
帐户服务:从用户帐户中扣除余额。
架构图

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

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

4.2.Seata简介

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

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

  • 分布式事务处理过程的一ID + 三组件模型
    • XID(Transaction ID):全局唯一的事务ID
    • 3组件概念
      • TC (Transaction Coordinator) - 事务协调者:维护全局事务和分支事务的状态,驱动全局提交或回滚。
      • TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务,提交或回滚全局事务。
      • RM (Resource Manager) - 资源管理器:管理正在处理的分支事务的资源,与TC对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。
  • 处理过程
    • 1.TM要求TC开始一项新的全局事务。TC生成代表全局事务的XID。
    • 2.XID通过微服务的调用链传播。
    • 3.RM将本地事务注册为XID到TC的相应全局事务的分支。
    • 4.TM要求TC提交或回退相应的XID全局事务。
    • 5.TC驱动XID的相应全局事务下的所有分支事务以完成分支提交或回滚。

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

怎么使用

  • 本地@Transactional
  • 全局@GlobalTransactional
    • Seata的分布式交易解决方案
    • 我们只需要使用一个 @GlobalTransactional 注解在业务方法上

4.3.Seata-Server安装

4.3.1.官网地址
http://seata.io/zh-cn/

4.3.2.下载版本
https://github.com/seata/seata/releases

4.3.3.seata-server-1.4.2.zip解压到指定目录并修改conf目录下的file.conf配置文件
先备份原始file.conf、registry.conf文件
主要修改:事务日志存储模式为db + 数据库连接信息

a. 修改seata解压目录中的conf目录下的file.conf文件
store模块:

b. 修改seata解压目录中的conf目录下的registry.conf文件
registry和config模块:

4.3.4.在本地新建一个seata数据库,导入如下数据库脚本

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

或者去官网拷贝数据库脚本,网址:
https://github.com/seata/seata/blob/1.4.2/script/server/db/mysql.sql

4.3.5.去官网复制config.txt配置和nacos-config.sh配置
config.txt网址:https://github.com/seata/seata/blob/1.4.2/script/config-center/config.txt
nacos-config.sh网址:https://github.com/seata/seata/blob/1.4.2/script/config-center/nacos/nacos-config.sh
config.txt配置放在安装seata的目录下,与bin目录同级

nacos-config.sh复制下来的文件放在安装seata目录下的conf目录

4.3.6.修改config.txt中的部分配置
自定义事务组名称:
service.vgroupMapping.my_test_tx_group=fsp_tx_group

4.3.7.启动seata

  • 先启动Nacos
  • 再启动bin目录下的seata-server.bat 即可启动seata

4.3.8.运行nacos-config.sh
seata/config.txt文件内容配置到nacos

sh nacos-config.sh -h 127.0.0.1

启动seata-server服务,打开nacos控制台-服务列表中,能看到seata-server就算搭建完成

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

先启动Nacos后启动Seata,否则Seata没启动报错:no available server to connect

4.4.1.分布式事务业务说明

  • 业务说明
    • 创建三个微服务:订单服务、库存服务、账户服务。

    • 当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存;

    • 再通过远程调用账户服务来扣减用户账户里面的余额;

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

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

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

4.4.2.创建业务数据库

  • seata_order:存储订单的数据库;
  • seata_storage:存储库存的数据库;
  • seata_account:存储账户信息的数据库。
  • 建库SQL
    • create database seata_order;
    • create database seata_storage;
    • create database seata_account;

4.4.3.按照上述2库分别建对应业务表

a. seata_order库下建t_order表

DROP TABLE IF EXISTS `t_order`;

CREATE TABLE `t_order` (
	`id` varchar(32) NOT NULL 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, 2) DEFAULT NULL COMMENT '金额',
	`status` int(1) DEFAULT NULL COMMENT '订单状态:0-创建中,1-已完结'
) ENGINE = innodb AUTO_INCREMENT = 7 CHARSET = utf8;

b. seata_storage库下建t_storage表

DROP TABLE IF EXISTS `t_storage`;

CREATE TABLE `t_storage` (
	`id` bigint(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
	`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
	`total` int(11) DEFAULT NULL COMMENT '总库存',
	`used` int(11) DEFAULT NULL COMMENT '已用库存',
	`residue` int(1) DEFAULT NULL COMMENT '剩余库存'
) ENGINE = innodb AUTO_INCREMENT = 2 CHARSET = utf8;

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

c. seata_account库下建t_account表

DROP TABLE IF EXISTS `t_account`;

CREATE TABLE `t_account` (
	`id` bigint(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'id',
	`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
	`total` decimal(11, 2) DEFAULT NULL COMMENT '总额度',
	`used` decimal(11, 2) DEFAULT NULL COMMENT '已用金额',
	`residue` decimal(11, 2) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE = innodb AUTO_INCREMENT = 2 CHARSET = utf8;

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

4.4.4.按照上述3库分别建对应的回滚日志表
订单-库存-账户3个库下都需要建各自的回滚日志表
db_undo_log.sql:

DROP TABLE IF EXISTS `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 `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARSET = utf8;

4.4.5.最终效果

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

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

4.4.1.新建订单Order-Module
a. 新建seata-order-service2001

b. POM

    <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>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.4.2</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>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</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>

c. YML

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server的registry.conf中的vgroupMapping一致
        tx-service-group: my_test_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: root

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

d. file.conf
复制安装seata目录中的conf目录下的file.conf文件,到项目类路径下

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    ## redis mode: single、sentinel
    mode = "single"
    ## single mode property
    single {
      host = "127.0.0.1"
      port = "6379"
    }
    ## sentinel mode property
    sentinel {
      masterName = ""
      ## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
      sentinelHosts = ""
    }
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }
}

e. registry.conf
复制安装seata目录中的conf目录下的registry.conf文件,到项目类路径下

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = "public"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  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 = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "public"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://192.168.1.204:8801"
    apolloConfigService = "http://192.168.1.204:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
    cluster = "seata"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

f. domain
1). 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);
    }
}

2). Order

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

    private String id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    /*
     * 订单状态:0-创建中,1-已完结
     *
     * */
    private Integer status;
}

g. Dao接口及实现
1). OrderDao

@Mapper
public interface OrderDao {

    //1.新建订单
    int create(Order order);

    //2.修改订单状态,从0修改为1
    int update(@Param("orderId") String orderId, @Param("status") Integer status);
}

2). resources文件夹下新建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.neo.springcloud.alibaba.dao.OrderDao">

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

    <insert id="create" parameterType="com.neo.springcloud.alibaba.domain.Order">
        insert into t_order(id, user_id, product_id, count, money, status)
        values(#{id}, #{userId}, #{productId}, #{count}, #{money}, 0)
    </insert>

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

</mapper>

h. Service接口及实现
1). OrderService

public interface OrderService {

    void create(Order order);
}

1.1). 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) {
        String bizSeq = UUID.randomUUID().toString().replace("-", "");
        log.info("开始新建订单,bizSeq:" + bizSeq);
        //1.新建订单
        orderDao.create(order);

        log.info("订单微服务开始调用库存,做扣减Count");
        //2.扣减库存
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("订单微服务开始调用库存,做扣减end");

        log.info("订单微服务开始调用账户,做扣减Money");
        //3.扣减库存
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("订单微服务开始调用账户,做扣减end");

        //4.修改订单状态,从0到1,1代表已完成
        log.info("修改订单状态开始");
        orderDao.update(order.getId(), 0);
        log.info("修改订单状态结束");

        log.info("下订单结束");
    }
}

2). StorageService

@FeignClient(value = "seata-storage-service")
public interface StorageService {

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

3). AccountService

@FeignClient(value = "seata-account-service")
public interface AccountService {

    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId,
                          @RequestParam("money") BigDecimal money);
}

i. Controller

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

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

j. Config配置
1). MyBatisConfig

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

2). DataSourceProxyConfig

@Configuration
public class DataSourceProxyConfig {

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

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

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

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

k. 主启动

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源的自动创建
public class SeataOrderMainApp2001 {

    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

4.4.2.新建库存Storage-Module
a. 新建seata-storage-service2002

b. POM
与seata-order-service2001一致

c. YML

server:
  port: 2002

spring:
  application:
    name: seata-storage-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server的registry.conf中的vgroupMapping一致
        tx-service-group: my_test_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage
    username: root
    password: root

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

d. file.conf
与seata-order-service2001一致

e. registry.conf
与seata-order-service2001一致

f. domain
1). CommonResult
与seata-order-service2001一致

2). Storage

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {

    private Long id;
    /**
     * 产品id
     */
    private Long productId;
    /**
     * 总库存
     */
    private Integer total;
    /**
     * 已用库存
     */
    private Integer used;
    /**
     * 剩余库存
     */
    private Integer residue;
}

g. Dao接口及实现
1). StorageDao

@Mapper
public interface StorageDao {

    //扣减库存
    int decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

2). resources文件夹下新建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.neo.springcloud.alibaba.dao.StorageDao">

    <resultMap id="BaseResultMap" type="com.neo.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 residue = residue - #{count},
        used = used + #{count}
        where product_id = #{productId}
    </update>

</mapper>

h. Service接口及实现
1). StorageService

public interface StorageService {

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

1.1). StorageServiceImpl

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    @Resource
    private StorageDao storageDao;

    @Override
    public void decrease(Long productId, Integer count) {
        log.info("库存微服务扣减库存开始");
        storageDao.decrease(productId, count);
        log.info("库存微服务扣减库存结束");
    }
}

i. Controller

@RestController
public class StorageController {

    @Resource
    private StorageService storageService;

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

j. Config配置
1). MyBatisConfig
与seata-order-service2001一致

2). DataSourceProxyConfig
与seata-order-service2001一致

k. 主启动

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataStorageMainApp2002 {

    public static void main(String[] args) {
        SpringApplication.run(SeataStorageMainApp2002.class, args);
    }
}

4.4.3.新建账户Account-Module
a. 新建seata-account-service2003

b. POM
与seata-order-service2001一致

c. YML

server:
  port: 2003

spring:
  application:
    name: seata-account-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server的registry.conf中的vgroupMapping一致
        tx-service-group: my_test_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account
    username: root
    password: root

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

d. file.conf
与seata-order-service2001一致

e. registry.conf
与seata-order-service2001一致

f. domain
1). CommonResult
与seata-order-service2001一致

2). Account

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

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

g. Dao接口及实现
1). AccountDao

@Mapper
public interface AccountDao {

    //扣减账户余额
    int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

2). resources文件夹下新建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.neo.springcloud.alibaba.dao.AccountDao">

    <resultMap id="BaseResultMap" type="com.neo.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 used = used + ${money},
        residue = residue - ${money}
        where user_id = ${userId}
    </update>

</mapper>

h. Service接口及实现
1). AccountService

public interface AccountService {

    /**
     * 扣减账户余额
     */
    void decrease(Long userId, BigDecimal money);
}

1.1). AccountServiceImpl

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {

    @Resource
    private AccountDao accountDao;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("账户微服务扣减账户余额开始");
        accountDao.decrease(userId, money);
        log.info("账户微服务扣减账户余额结束");
    }
}

i. Controller

@RestController
public class AccountController {

    @Resource
    private AccountService accountService;

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

}

j. Config配置
1). MyBatisConfig
与seata-order-service2001一致

2). DataSourceProxyConfig
与seata-order-service2001一致

k. 主启动

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataAccountMainApp2003 {

    public static void main(String[] args) {
        SpringApplication.run(SeataAccountMainApp2003.class, args);
    }
}

4.5.测试

下订单->减库存->扣余额->改(订单)状态

4.5.1.数据库初始情况


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

数据库情况:


4.5.3.模拟超时异常
AccountServiceImpl添加超时:

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

访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

数据库情况:
Ⅰ.订单状态未完成

Ⅱ.库存被扣减了

Ⅲ.账户被扣减了

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

4.5.3.处理超时异常,全局分布式事务回滚(@GlobalTransactional
OrderServiceImpl添加@GlobalTransactional

    @Override
    @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
    public void create(Order order) {
        String bizSeq = UUID.randomUUID().toString().replace("-", "");
        log.info("开始新建订单,bizSeq:" + bizSeq);
        order.setId(bizSeq);
        //1.新建订单
        orderDao.create(order);

        log.info("订单微服务开始调用库存,做扣减Count");
        //2.扣减库存
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("订单微服务开始调用库存,做扣减end");

        log.info("订单微服务开始调用账户,做扣减Money");
        //3.扣减库存
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("订单微服务开始调用账户,做扣减end");

        //4.修改订单状态,从0到1,1代表已完成
        log.info("修改订单状态开始");
        orderDao.update(order.getId(), 0);
        log.info("修改订单状态结束");

        log.info("下订单结束");
    }

访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

数据库情况:


下单后,@GlobalTransactional似乎并不能解决超时Feign异常事务回滚问题

4.6.Seata工作原理

4.6.1.Seata
2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
2020起始,参加工作后用1.0以后的版本

4.6.2.TC/TM/RM三大组件

TC:seata的服务器
TM:事物的发起者,业务的入口。 @GlobalTransactional(name = “txl-create-order”, rollbackFor = Exception.class)
RM:事务的参与者,一个数据库就是一个RM。

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

4.6.3.AT模式如何做到对业务的无侵入
Ⅰ.是什么

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

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

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

4.6.4.总结

posted @ 2021-10-06 23:37  冰枫丶  阅读(822)  评论(0编辑  收藏  举报