SpringCloud-Alibaba笔记

A blog for SpringCloud-Alibaba-Study

Posted by if on 2021-12-08
Estimated Reading Time 73 Minutes
Words 18.5k In Total

SpringCloud-Alibaba笔记

@[TOC]

前言

本文的代码和笔记都放在了我的个人gitee上,有需要可以点击查看

https://gitee.com/ifyyf/springcloud-alibaba-study

如有需要查看springcloud-Netflix笔记的同学可以点击《SpringCloud-Netflix笔记》查看

本文主要讲解SpringCloud-Alibaba

springcloud-Netflix项目进入维护模式,基本上不会有大更新和新功能了

将模块置于维护模式,意味着Spring Cloud团队将不会再向模块添加新功能。
我们将修复block级别的bug以及安全问题,我们也会考虑并审查社区的小型pull request.

于是springcloudalibaba应运而生

版本选用

本文选用版本为

1
2
<spring-boot.version>2.2.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
Spring Cloud Alibaba Version Sentinel Version Nacos Version RocketMQ Version Dubbo Version Seata Version
2.2.7.RELEASE 1.8.1 2.0.3 4.6.1 2.7.13 1.3.0

官网

github链接https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

文档链接https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

wiki教程链接https://github.com/alibaba/spring-cloud-alibaba/wiki

它是springcloud-Netflix的后继,那么springcloud能干的它都能干

主要功能

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

更多功能请参考 Roadmap

组件

Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

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

RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。

Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。

Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。

Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。

Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

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

更多组件请参考 Roadmap

如何构建

  • 2020.0 分支对应的是 Spring Cloud 2020,最低支持 JDK 1.8。
  • master 分支对应的是 Spring Cloud Hoxton,最低支持 JDK 1.8。
  • greenwich 分支对应的是 Spring Cloud Greenwich,最低支持 JDK 1.8。
  • finchley 分支对应的是 Spring Cloud Finchley,最低支持 JDK 1.8。
  • 1.x 分支对应的是 Spring Cloud Edgware,最低支持 JDK 1.7。

Spring Cloud 使用 Maven 来构建,最快的使用方式是将本项目 clone 到本地,然后执行以下命令:

1
./mvnw install

执行完毕后,项目将被安装到本地 Maven 仓库。

如何使用:引入依赖

如果需要使用已发布的版本,在 dependencyManagement 中添加如下配置。

1
2
3
4
5
6
7
8
9
10
11
<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>

然后在 dependencies 中添加自己所需使用的依赖即可使用。

版本管理规范

项目的版本号格式为 x.x.x 的形式,其中 x 的数值类型为数字,从 0 开始取值,且不限于 0~9 这个范围。项目处于孵化器阶段时,第一位版本号固定使用 0,即版本号为 0.x.x 的格式。

由于 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
Spring Cloud Alibaba Version Sentinel Version Nacos Version RocketMQ Version Dubbo Version Seata Version
2.2.7.RELEASE 1.8.1 2.0.3 4.6.1 2.7.13 1.3.0
2.2.6.RELEASE 1.8.1 1.4.2 4.4.0 2.7.8 1.3.0
2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE 1.8.0 1.4.1 4.4.0 2.7.8 1.3.0
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE 1.8.0 1.3.3 4.4.0 2.7.8 1.3.0
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE 1.7.1 1.2.1 4.4.0 2.7.6 1.2.0
2.2.0.RELEASE 1.7.1 1.1.4 4.4.0 2.7.4.1 1.0.0
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE 1.7.0 1.1.4 4.4.0 2.7.3 0.9.0
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE 1.6.3 1.1.1 4.4.0 2.7.3 0.7.1

Nacos服务注册与配置中心

我们之前学的eureka只是注册中心,config只是配置中心

从名字可以看出来,nacos兼并了服务的注册与配置中心功能

nacos=naming+configuration+service

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

nacos和eureka的区别

模块 Nacos Eureka 说明
注册中心 服务治理基本功能,负责服务中心化注册
配置中心 Eureka需要配合Config实现配置中心,且不提供管理界面
动态刷新 Eureka需要配合MQ实现配置动态刷新,Nacos采用Netty保持TCP长连接实时推送
可用区AZ 对服务集群划分不同区域,实现区域隔离,并提供容灾自动切换
分组 Nacos可用根据业务和环境进行分组管理
元数据 提供服务标签数据,例如环境或服务标识
权重 Nacos默认提供权重设置功能,调整承载流量压力
健康检查 Nacos支持由客户端或服务端发起的健康检查,Eureka是由客户端发起心跳
负载均衡 均提供负责均衡策略,Eureka采用Ribbon
管理界面 Nacos支持对服务在线管理,Eureka只是预览服务状态
CAP AP AP Nacos和Eureka都是AP,zookeeper是CP
数据库 Nacos集群使用数据库解决数据一致性问题
配置文件 在线编辑 本地文件或者Git远程文件 Eureka需要结合Config等实现配置中心,nacos本身既是注册中心也是配置中心

下载安装

github的release地址https://github.com/alibaba/nacos/releases

下载安装包解压后,在bin目录下运行startup.cmd

如果默认的startup.cmd运行报错的话,试试startup.cmd -m standalone

启动命令(standalone代表着单机模式运行,非集群模式)

出现下列语句表示运行成功

2021-12-04 14:52:45,882 INFO Tomcat started on port(s): 8848 (http) with context path ‘/nacos’

2021-12-04 14:52:45,890 INFO Nacos started successfully in stand alone mode. use embedded storage

运行成功后访问localhost:8848/nacos,默认账号和密码都是nacos

相比于eureka的单独一个服务并进行手动配置等,nacos直接就封装好可以打包运行并有良好的界面操作

nacos之服务提供者注册

配置9001子模块,注册到nacos

创建子模块provider-payment9001

添加依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>

添加配置

1
2
3
4
5
6
7
8
9
10
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
# 配置nacos地址
server-addr: localhost:8848

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

启动类添加@EnableDiscoveryClient注解开启服务注册发现功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableDiscoveryClient
public class NacosProvider9001Application {

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

配置controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-04 下午 09:31
*/
@RestController
public class PaymentNacosController {

@GetMapping("/payment/{string}")
public String hello(@PathVariable String string){
return "Hello 9001 ->"+string;
}
}

启动9001,访问http://localhost:9001/payment/test

得到正确结果

Hello 9001 -> test

同时访问nacos界面,可以看到服务列表中payment服务被注册进来了

甚至还有示例代码可以直接复制粘贴用于联调

配置9002子模块

同样的配置,我们再搞个9002出来,用于接下来的负载均衡测试

步骤省略,port为9002

注:spring.application.name还是nacos-payment-provider,让其一个服务下存在2个实例即可

nacos消费者注册和负载均衡

配置83子模块、注册和测试

与之前一样,端口注册83,服务名为nacos-order-consumer即可

1
2
3
4
5
6
7
8
9
10
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
# 配置nacos地址
server-addr: localhost:8848

配置ApplicationContextConfig

配置RestTemplate的bean进入ioc容器,用于远程调用

@LoadBalanced注解配置负载均衡实现RestTemplate

因为当ribbon根据服务名找到了多个实例时,如果没有负载均衡会导致不知道该找哪个服务然后导致报错

所以@LoadBalanced注解必须要加,不然访问即报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-04 下午 09:22
*/
@Configuration
public class ApplicationContextConfig {

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

配置OrderNacosController接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-04 下午 09:24
*/
@RestController
@Slf4j
public class OrderNacosController {

@Resource
private RestTemplate restTemplate;

/**consumer消费者将要去访问的服务名称(前提是已经注册进了nacos)*/
private static final String SERVICE_URL ="http://nacos-payment-provider";

@GetMapping("/consumer/payment/{string}")
public String paymentInfo(@PathVariable String string){
return restTemplate.getForObject(SERVICE_URL +"/payment/"+string,String.class);
}
}

多次访问http://localhost:83/consumer/payment/consumer123

发现返回数据不一致,成功实现负载均衡

Hello 9001 ->consumer123

Hello 9002 ->consumer123

负载均衡

nacos的spring-cloud-starter-alibaba-nacos-discoverypom依赖中包含了netflix-ribbon

ribbon大家都知道了,就是用于负载均衡的,所以说nacos自带了负载均衡是正确的

nacos配置中心之基础配置

nacos自带了一个小型的内嵌式的数据库来保存这些配置信息

配置3377配置中心

pom.xml依赖文件

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>

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

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

创建bootstrap.yml

值得一提的是这个file-extension配置,不过严格上来说应该正式写yaml

  • 这写yml,dataId后缀就必须是yml
  • 这写yaml,dataId后缀就必须是yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
# nacos注册中心地址
server-addr: localhost:8848
config:
# nacos配置中心地址
server-addr: localhost:8848
# 指定文件格式(严格和dataId匹配)
file-extension: yml

创建application.yml

1
2
3
4
spring:
profiles:
# 表示开发环境
active: dev

创建ConfigClientController

这里的configInfo是读取配置中心的配置,请看下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-04 下午 10:08
*/
@RestController
//支持nacos配置的动态刷新功能
@RefreshScope
public class ConfigClientController {

//这里读取配置中心的配置内容
@Value("${config.info}")
private String configInfo;

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

nacos中dataId的匹配规则

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

1
${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 来配置。
    • 目前只支持 propertiesyml 类型。

用配置文件properties的格式来说,最终就是

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

即匹配出来的文件名为 nacos-config-client-dev.yml

在nacos界面添加配置

上文说了dataId的匹配规则,我们进入nacos界面,找到

新建一个配置,并命名为nacos-config-client-dev.yml且格式为yaml的配置

测试配置中心及其动态刷新

添加了配置后,我们访问http://localhost:3377/config/info

得到结果,配置中心测试成功

nacos config center , version = 1

然后我们修改配置中心的值(随便改一下,我这将version=2)

然后我们再访问http://localhost:3377/config/info

得到正确结果,@RefreshScope注解实现了配置的动态刷新功能

nacos config center , version = 2

nacos还可以查看历史版本和进行回滚

nacos配置中心之分类配置

一个项目有许多环境,例如开发环境、预发环境、生产环境等等

一个项目也有许多子模块,子模块又有对应的环境,那么这么多的情况我们怎么做呢?

答:nacos配置中心的分类配置

namespace和group

看nacos的左侧导航栏可以看见命名空间,然后在配置列表中也可以看见group分组,这些都是什么呢?

答:

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

默认情况下

  • 命名空间namespace:public
  • 分组group:DEFAULT_GROUP
  • 集群cluster:DEFAULT

nacos集群和持久化配置

官网教程https://nacos.io/zh-cn/docs/deployment.html

这个SLB在老版本应该是叫vip,个人理解为用于代理到nacos集群,可以当做是nginx的实例

所以集群相当于:1个nginx+3个nacos+1个mysql

nacos自带了一个小型的内嵌式的derby数据库来保存这些配置信息,如果启动了多个默认配置的nacos,会出现数据一致性问题

  • 单机模式 - 用于测试和单机试用。
  • 集群模式 - 用于生产环境,确保高可用。
  • 多集群模式 - 用于多数据中心场景。

mysql持久化配置

Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。

单机模式支持mysql

在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。

0.7版本增加了支持mysql数据源能力,具体的操作步骤:

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

db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow

再以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql

配置中心新写的配置文件也被写入了config_info表中了

nacos小总结

nacos具有服务注册和配置中心的多重功能

生产者与消费者的启动类都加上@EnableDiscoveryClient,并在yml配置文件中配置服务名称和nacos注册中心的地址,将根据其服务名称注册进注册中心,一般一个服务对应多个不同端口的实例共同实现同一服务

nacos自带ribbon依赖,消费者使用RestTemplate时加上@LoadBalanced注解达到负载均衡来访问生产者

nacos配置中心可以在nacos界面中在线编辑配置和@RefreshScope注解实现动态刷新配置

根据bootstrap.yml和application.yml配置文件,选中配置中心中对应的dataId的配置进行读取

nacos的配置默认使用内嵌的derby数据库,可以连接mysql后将配置持久化到mysql数据库

Sentinel限流、熔断与降级

什么是sentinel

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

Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制熔断降级系统自适应保护等多个维度来帮助您保障微服务的稳定性。

Sentinel 的使用可以分为两个部分:

  • 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持(见 主流框架适配)。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

Sentinel 的主要工作机制如下:

  • 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
  • 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
  • Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。

下载与安装

Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。

Sentinel 控制台包含如下功能:

  • 查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。
  • 监控 (单机和集群聚合):通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信息,最终可以实现秒级的实时监控。
  • 规则管理和推送:统一管理推送规则。
  • 鉴权:生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。

https://github.com/alibaba/Sentinel/releases

去github下载sentinel的控制台jar包sentinel-dashboard-1.8.1.jar,版本根据springcloud版本来定

下载后使用命令启动,默认端口为8080

注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。

其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080

从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel

可以参考 鉴权模块文档 配置用户名和密码。

1
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar

运行得到表示在8080端口启动成功

Tomcat started on port(s): 8080 (http) with context path ‘’

访问localhost:8080,输入用户名密码sentinel进入监控界面

sentinel初始化监控

创建子模块8401

导入sentinel依赖和nacos-discovery依赖(因为要注册进注册中心)

1
2
3
4
5
6
7
8
9
10
11
        <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<!-- sentinel依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>

配置文件

spring.cloud.sentinel.transport.port:指定应用与Sentinel控制台交互的端口,应用本地会起一个HttpServer

配置了该端口后,会在应用对应的机器上启动一个 Http Server,该Server会与Sentinel控制台做交互

比如 Sentinel 控制台添加了1个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8401
spring:
application:
name: sentinel-service
cloud:
nacos:
discovery:
# 配置nacos地址
server-addr: localhost:8848
sentinel:
transport:
# 配置sentinel的控制台地址
dashboard: localhost:8080
# 默认8719端口,如被占用则从8719开始依次+1扫描直至找到未被占用的端口
port: 8719

创建测试接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-05 下午 02:06
*/
@RestController
public class FlowLimitController {

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

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

疯狂访问接口后查看sentinel的监控台

左边导航栏的sentinel-service是表示注册进的服务名称

右边具体就是接口流量的详细监控数据了

sentinel流控规则

流量控制基本介绍

FlowSlot 会根据预设的规则,结合前面 NodeSelectorSlotClusterNodeBuilderSlotStatistcSlot 统计出来的实时信息进行流量控制。

限流的直接表现是在执行 Entry nodeA = SphU.entry(资源名字) 的时候抛出 FlowException 异常。FlowExceptionBlockException 的子类,您可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

同一个资源可以对应多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型,QPS 或线程数
  • strategy: 根据调用关系选择策略

简单介绍

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

sentinel控制台添加流控规则

在控制台左边选中对应的服务,选择簇点链路流控规则都可以进行添加

阈值选用QPS和1时,表示每秒最多有1个请求进行处理,1秒内超出1个请求则进行限流

红圈标出来的是高级选项,如果不更改的话,默认就是直接模式+快速失败效果

此时我们访问http://localhost:8401/b,如果很慢的情况下访问是没有超过阈值1的QPS的

但是如果疯狂刷新的情况下,就会出现限流

Blocked by Sentinel (flow limiting)

而这种在控制台的配置与业务代码进行了解耦处理,并且流控配置是热更新的,非常方便

思考:限流后的报错语句是sentinel自带的,不是很人性化

@SentinelResource自定义限流熔断异常

还记得之前Hystrix组件是怎么做的嘛?

对接口加一个注解,并指定了一个返回值的方法,在熔断降级后会直接调用该方法

1
2
3
4
5
6
7
8
9
10
11
   @GetMapping("/get/{deptno}")
//指定当前方法熔断的解决方法名称
@HystrixCommand(fallbackMethod = "hystrixGet")
public Dept get(@PathVariable("deptno") Long deptno){}

public Dept hystrixGet(Long deptno){
return new Dept()
.setDeptno(deptno)
.setDname("不存在id为 "+deptno+" 的数据")
.setSource("no this database in mysql");
}

那换成sentinel怎么做呢?和hystrix差不多,也是一个注解+指定方法

不过需要注意的是@SentinelResource注解配置了之后,在控制台会显示这个my是属于/b

如果需要走自定义熔断语句的话,需要配置到和注解的value一样的链路,即配置my而不是/b

如果走/b链路的话,自定义就不生效了,还是走的默认的Blocked by Sentinel (flow limiting)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/b")
@SentinelResource(
value = "my",
//sentinel的降级限流
blockHandler = "blockHandler",
//业务异常
fallback="fallback",
//忽略的异常
exceptionsToIgnore=NullPointerException.class)
public String testB(){
return "testB!";
}

public String blockHandler(BlockException blockException){
return "服务挂了没关系,还有我呢来处理-> blockHandler";
}
public String fallback(Throwable throwable){
return "服务挂了没关系,还有我呢来处理-> fallback";
}

此时我们疯狂刷新访问http://localhost:8401/b

得到

服务挂了没关系,还有我呢来处理-> blockHandler

blockHandler处理的是sentinel的降级,管的是sentinel的异常BlockException

fallback处理的是业务的熔断,当业务中出现异常时,会执行fallback

exceptionsToIgnore表示忽略某些异常,则sentinel不进行捕获处理

@SentinelResource属性介绍

  • Value:资源名称,必需项(不能为空)。
  • entryType:entry类型,标记流量的方向,取值IN/OUT,可选项(默认为EntryType.OUT)
  • blockHandler:处理BlockException的函数名称(可以理解对Sentinel的配置进行方法兜底)
    • 函数要求:必须是public修饰
    • 返回类型与原方法一致
    • 参数类型需要和原方法相匹配,并在最后加BlockException类型的参数
    • 默认需和原方法在同一个类中,若希望使用其他类的函数,可配置blockHandlerClass,并指定blockHandlerClass里面的方法。
  • blockHandlerClass:存放blockHandler的类。对应的处理函数必须是public static修饰,否则无法解析,其他要求:同blockerHandler。
  • fallback:用于在抛出异常的时候提供fallback处理逻辑(可以理解为对java异常情况方法兜底)。fallback函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理
    • 函数要求:返回类型与原方法一致
    • 参数类型需要和原方法相匹配,并需要在方法最后加Throwable类型的参数
    • 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass,并制定fallbackClass里面的方法。
  • fallbackClass:存放fallback的类。对应的处理函数必须static修饰,否则无法解析,其他要求:同fallback。
  • defaultFallback:用于通用的fallback逻辑。默认fallback函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了fallback和defaultFallback,以fallback为准
    • 函数要求:返回类型与原方法一致。
    • 方法参数列表为空,或者有一个Throwable类型的参数。
    • 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass,并指定fallbackClass里面的方法。
  • exceptionsToIgnore:指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
  • exceptionsToTrace:需要trace的异常。

流控模式①——直接(默认)

直接:api达到限流条件时,直接限流

上文的demo示例就是直接模式,这里不再重复

流控模式②——关联

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

可能有点不好理解,举个例子,假设A和B接口在同一个controller下

然后B到达了阈值,那么根据关联模式,就会将A限流

等B的压力消退后,A就会回归正常

比如:支付接口达到了阈值,那么我就将订单接口进行限流(减缓达到阈值接口的压力)

创建关联模式需要配置资源名和关联的资源名

流控模式③——链路

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

阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流

如果只希望统计从/a进入到/b的请求,从而对/a进行限流,则可以使用链路模式配置

流控效果①——快速失败(默认)

快速失败:直接失败,抛出异常

Blocked by Sentinel (flow limiting)

流控效果②——预热warm up

预热Warm Up:根据codeFactor (冷加载因子,默认3)的值,在预热时长内,慢慢从阈值/codeFactor达到设置的QPS阈值

假设阈值为10,设置预热时长为5

根据阈值/codeFactor为3,得出在最开始的阈值会被设定为3

在预热时长5秒内,阈值会慢慢从3增长到设置的10

假设系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。

通过预热Warm Up效果,让通过的流量缓慢增加,在一定时间内才逐渐增加到阈值上限,给系统一个预热的时间, 避免系统被压垮

流控效果③——排队等待

排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效

达到阈值就排队,在超时时间内就排队,超时了就放弃

这种方式主要用于处理间隔性突发的流量,例如消息队列。

想象一下这样的场景, 在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

sentinel的熔断降级

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一

如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用

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

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断

默认行为是抛出DegradeException

sentinel的熔断是没有半开状态的:

​ 半开状态会自动检测是否请求有异常

​ 没有异常就关闭断路器,恢复使用

​ 有异常就打开断路器,不可用

熔断降级规则参数

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

熔断策略

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO)RT超时比例大于阈值单位时间内请求数大于最小请求数
    • 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用
    • 单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断
    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断
  • 异常比例 (ERROR_RATIO)异常比例大于阈值单位时间内请求数大于最小请求数
    • 单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断
    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断
    • 异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%
  • 异常数 (ERROR_COUNT)单位时间内异常数量大于阈值
    • 单位统计时长(statIntervalMs)内的异常数目超过阈值之后会自动进行熔断
    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断

注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。

为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常

sentinel热点key

何为热点?热点即经常访问的数据

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

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

注:使用了热点规则的话,熔断降级后会直接向前台抛出异常,切记需要@SentinelResource

而注解@SentinelResourceblockHandler属性是不管业务异常的,只管sentinel的配置规则异常!

如果需要接管业务异常,需要配置注解的fallback属性

参数索引就是表示第几个参数,例如/get?a=xxx&b=xxx,那么a的参数索引就是0,b就是1

注:这个参数指的是方法中的位置,而不是实际请求的位置

也就是说当参数索引设置0时,/get?b=xxx是不会被限流的

对于热点规则也有特例

例如我想实现当一个参数打过来,当他表示为vip时,阈值就大,不是vip时,阈值就小

这个怎么实现呢?sentinel热点规则的高级参数设置就有

参数索引第0个,单机阈值是10,当参数是string且值为’vip’时,阈值变为200

例如当我访问/test?a=xxx时,会被限流到10

当我变成了/test?a=vip时,阈值就变成了200

sentinel系统自适应保护

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

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

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

不走注解自定义异常返回@SentinelResource

系统规则支持以下的阈值类型:

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

适合对全系统的总控,细粒度不够,有可能一杆子打死所有服务

sentinel整合Feign

Feign相对于Ribbon,我个人是更加喜欢feign一些,不需要配置和调用RestTemplate,用接口方式的配置更便捷

我们创建模块consumer-feign84

配置nacos、sentinel和feign的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 84
spring:
application:
name: nacos-feign-consumer
cloud:
nacos:
discovery:
# 配置nacos地址
server-addr: localhost:8848
sentinel:
transport:
# 配置sentinel的控制台地址
dashboard: localhost:8080
# 默认8719端口,如被占用则从8719开始依次+1扫描直至找到未被占用的端口
port: 8719
# 开启sentinel支持feign
feign:
sentinel:
enabled: true

接口远程调用

指定远程调用的服务名为nacos-payment-provider

服务降级调用的回调类ClientServiceFallbackFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.ifyyf.config.ClientServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-05 下午 10:11
*/
@FeignClient(value = "nacos-payment-provider",fallbackFactory = ClientServiceFallbackFactory.class)
@Service
public interface ClientService {
@GetMapping("/payment/{string}")
String hello(@PathVariable("string") String string);
}

服务降级回调类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.ifyyf.consumer.ClientService;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

/**
* @Author if
* @Description: 服务降级配置类,需要返回feign被降级的接口实例
* 降级的实现方法写在接口类的实现方法上
* @Date 2021-12-05 下午 10:15
*/
@Component
public class ClientServiceFallbackFactory implements FallbackFactory {
@Override
public ClientService create(Throwable throwable) {
return new ClientService() {
@Override
public String hello(String string) {
return "服务暂时不可用,Feign引用了服务降级方法";
}
};
}
}

Feign启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-05 下午 10:13
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.ifyyf"})
public class FeignConsumer84Application {
public static void main(String[] args) {
SpringApplication.run(FeignConsumer84Application.class,args);
}
}

接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.ifyyf.consumer.ClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;

/**
* @Author if
* @Description: What is it
* @Date 2021-12-05 下午 10:17
*/
@RestController
public class ClientController {

@Autowired
private ClientService clientService;

@GetMapping("/consumer/payment/{string}")
@SentinelResource(
value = "hello",
blockHandler = "blockHandler",
fallback="fallback")
public String hello(@PathVariable String string){
if(Objects.equals("exception",string)){
//模拟抛出异常
throw new NullPointerException();
}
return clientService.hello(string);
}
public String blockHandler(String string,BlockException blockException){
return "服务挂了没关系,还有我来处理-> blockHandler";
}
public String fallback(String string,Throwable throwable){
return "出现异常了没关系,还有我来处理-> fallback";
}
}

我们之前已经启动了9001和9002的nacos-payment-provider 服务实例

接下来我们启动feign84消费者,并在sentinel控制台为hello添加QPS阈值为1

测试访问:负载均衡、熔断、异常、降级

每隔1秒以上访问一次http://localhost:84/consumer/payment/hello

可以得到两个结果,服务调用和负载均衡生效

Hello 9001 ->hello

Hello 9002 ->hello

疯狂访问http://localhost:84/consumer/payment/hello

得到blockHandler的结果

服务挂了没关系,还有我来处理-> blockHandler

访问http://localhost:84/consumer/payment/exception

得到fallback结果

出现异常了没关系,还有我来处理-> fallback

我们把9001和9002服务实例断开后再进行访问http://localhost:84/consumer/payment/hello

调用到了ClientServiceFallbackFactory,feign服务降级实现成功

服务暂时不可用,Feign引用了服务降级方法

分布式事务

分布式事务的由来

事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败

我们平时的单机应用中,基本上都用的mysql数据库事务或者框架应用的@Transaction来控制事务回滚/提交

可是在微服务分布式架构中,模块与模块之间解耦并使用了独立的数据库,多个数据库从逻辑上来说是连通的,但是物理上却是隔绝的。本地的数据一致性可以解决,但是全局的数据的一致性就成了一个问题

这种分布式系统环境下由不同的服务之间通过网络远程协作完成的事务称之为分布式事务

例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务

CAP定理

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。

  • Consistency(一致性)
  • Availability(可用性)
  • Partition tolerance (分区容错性)

它们的第一个字母分别是 C、A、P。

Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理

分区容错性Partition tolerance

Partition tolerance,中文叫做"分区容错"。

大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在上海,另一台服务器放在北京,这就是两个区,它们之间可能因网络问题无法通信。

一致性Consistency

Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值

举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

接下来,用户的读操作就会得到 v1。这就叫一致性。

问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。

为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。

这样的话,用户向 G2 发起读操作,也能得到 v1。

可用性Availability

Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应(对和错不论)。

用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。

不管是200还是400状态,反正要返回而不是让它一直转着等待到超时

一致性C和可用性A的矛盾

一致性和可用性,不可能同时成立

为什么?答案很简单,因为可能通信失败(即出现分区容错)。

如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性。

如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。

综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。

  • 如果追求一致性,那么无法保证所有节点的可用性;
  • 如果追求所有节点的可用性,那就没法做到一致性。

C还是A?

  • 怎样才能同时满足CA?

    除非是单点架构

  • 何时要满足CP?

    对一致性要求高的场景。例如我们的Zookeeper就是这样的,在服务节点间数据同步时,服务对外不可用。

  • 何时满足AP?

    对可用性要求较高的场景。例如Eureka,必须保证注册中心随时可用,不然拉取不到服务就可能出问题。

BASE理论

BASE是三个单词的缩写:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

而我们解决分布式事务,就是根据上述理论来实现。

还以上面的下单减库存和扣款为例:

订单服务、库存服务、用户服务及他们对应的数据库就是分布式应用中的三个部分。

  • CP方式:现在如果要满足事务的强一致性,就必须在订单服务数据库锁定的同时,对库存服务、用户服务数据资源同时锁定。等待三个服务业务全部处理完成,才可以释放资源。此时如果有其他请求想要操作被锁定的资源就会被阻塞,这样就是满足了CP。

    这就是强一致,弱可用

  • AP方式:三个服务的对应数据库各自独立执行自己的业务,执行本地事务,不要求互相锁定资源。但是这个中间状态下,我们去访问数据库,可能遇到数据不一致的情况,不过我们需要做一些后补措施,保证在经过一段时间后,数据最终满足一致性。

    这就是高可用,但弱一致(最终一致)。

由上面的两种思想,延伸出了很多的分布式事务解决方案:

  • XA
  • TCC
  • 可靠消息最终一致
  • AT

解决方案1:DTP和XA

DTP分布式事务处理模型

XADTP中通信中间件与TM事务管理器之间联系的接口规范

1994 年,X/Open 组织(即现在的 Open Group )定义了分布式事务处理的DTP 模型

  • 应用程序( AP ):我们的微服务
  • 事务管理器( TM ):全局事务管理者
  • 资源管理器( RM ):一般是数据库
  • 通信资源管理器( CRM ):是TM和RM间的通信中间件

在该模型中,一个分布式事务(全局事务)可以被拆分成许多个本地事务,运行在不同的AP和RM上。

每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功,若有一个本地事务失败,则所有其它事务都必须回滚。但问题是,本地事务处理过程中,并不知道其它事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态

因此,各个本地事务的通信必须有统一的标准,否则不同数据库间就无法通信。XA就是 X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。

而分布式事务的解决手段之一,就是两阶段提交协议(2PC:Two-Phase Commit)

什么是两阶段提交协议?

二阶提交协议就是根据DTP&XA思想衍生出来的,将全局事务拆分为两个阶段来执行

  • 阶段一:准备阶段,各个本地事务完成本地事务的准备工作
  • 阶段二:执行阶段,各个本地事务根据上一阶段执行结果,进行提交或回滚

这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)

  • 投票阶段协调组询问各个事务参与者,是否可以执行事务

    • 每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行成功的信息agree或是Disagree表示事务执行失败
  • 提交阶段:协调组接收每个参与者反馈的commit指令

    • 接收到所有参与者执行成功指令agree,向各个参与者发出commit指令,各个事务参与者提交事务
    • 协调组发现有一个或多个参与者返回的是Disagree,认为执行失败。于是向各个事务参与者发出abort指令,各个事务参与者回滚事务

正常情况

协调者向各个参与者发送完成本地事务的指令后接受各个参与者返回来的提交的信息,完成整体事务保持一致性

异常情况

其中有一个参与者返回的是Disagree,则说明执行失败

于是向各个事务参与者发出abort指令,各个事务参与者回滚事务。

优势与缺点

  • 优势
    • 对事务保证了强一致性,要么全部提交成功要么一起回滚
  • 缺点
    • 在准备阶段、提交阶段,每个事物参与者都会锁定本地资源,并等待其它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了
  • 适用场景
    • 对事务有强一致性要求,对事务执行效率不敏感,并且不希望有太多代码侵入

解决方案2:TCC

TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间

它本质是一种补偿的思路。事务运行过程包括三个方法,

  • Try:资源的检测和预留;
  • Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
  • Cancel:预留资源释放。

执行分两个阶段:

  • 准备阶段(try):资源的检测和预留;
  • 执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法。如果上一步中所有事务参与者都成功,则这里执行confirm。反之,执行cancel

样例说明

假设账户A原来余额是100,需要余额扣减30元

  • 一阶段(Try):余额检查,并冻结用户部分金额,此阶段执行完毕,事务已经提交
    • 检查用户余额是否充足,如果充足,冻结部分余额
    • 在账户表中添加冻结金额字段,值为30,余额不变
  • 二阶段
    • 提交(Confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额清空
      • 修改冻结金额为0,修改余额为100-30 = 70元
    • 补偿(Cancel):释放之前冻结的金额,并非回滚
      • 余额不变,修改账户冻结金额为0

粗看似乎与两阶段提交没什么区别,但其实差别很大:

  • try、confirm、cancel都是独立的事务,不受其它参与者的影响,不会阻塞等待它人
  • try、confirm、cancel由程序员在业务层编写,锁粒度有代码控制

优势与缺点

  • 优势

    • TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其它事务的执行结果

    • 如果其它事务执行失败,最后不是回滚,而是执行补偿操作

    • 这样就避免了资源的长期锁定和阻塞等待执行效率比较高,属于性能比较好的分布式事务方式

  • 缺点

    • 代码侵入:需要人为编写代码实现try、confirm、cancel,代码侵入较多
    • 开发成本高:一个业务需要拆分成3个步骤,分别编写业务实现,业务编写比较复杂
    • 安全性考虑:cancel动作如果执行失败,资源就无法释放,需要引入重试机制,而重试可能导致重复执行,还要考虑重试时的幂等问题
  • 使用场景

    • 对事务有一定的一致性要求(最终一致
    • 对性能要求较高
    • 开发人员具备较高的编码能力和幂等处理经验

解决方案3:可靠消息服务

这种实现方式的思路,其实是源于ebay,其基本的设计思想是将远程分布式事务拆分成一系列的本地事务

一般分为事务的发起者A和事务的其它参与者B:

  • 事务发起者A执行本地事务
  • 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B
  • 事务参与者B接收到消息后执行本地事务

这个过程有点像你去学校食堂吃饭:

  • 拿着钱去收银处,点一份红烧牛肉面,付钱
  • 收银处给你发一个小票,还有一个号牌,你别把票弄丢!
  • 你凭小票和号牌一定能领到一份红烧牛肉面,不管需要多久

几个注意事项:

  • 事务发起者A必须确保本地事务成功后,消息一定发送成功
  • MQ必须保证消息正确投递和持久化保存
  • 事务参与者B必须确保消息最终一定能消费,如果失败需要多次重试
  • 事务B执行失败,会重试,但不会导致事务A回滚

优势与缺点

  • 优点
    • 业务相对简单,不需要编写三个阶段业务
    • 是多个本地事务的结合,因此资源锁定周期短,性能好
  • 缺点
    • 代码侵入
    • 依赖于MQ的可靠性
    • 消息发起者可以回滚,但是消息参与者无法引起事务回滚
    • 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况

解决方案4:AT模式

2019年 1 月份,Seata 开源了 AT 模式。

官网介绍https://seata.io/zh-cn/docs/dev/mode/at-mode.html

AT 模式是一种无侵入的分布式事务解决方案

可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。

在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段

Seata 框架会自动生成事务的二阶段提交和回滚操作

跟TCC的执行很像,都是分两个阶段

但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部有Seata自己实现了。也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务。

Seata分布式事务

seata是什么

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

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

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

术语

一ID+3组件模型

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

3组件↓↓↓

  • TC 事务协调者 (Transaction Coordinator) ,seata-server
    • 维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM 事务管理器 (Transaction Manager),事务的发起方
    • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM 资源管理器 (Resource Manager),被调用的参与事务的服务
    • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

过程

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

怎么用

本地事务:@Transactional

全局事务:@GlobalTransactional

没错,在seata-server存在下,只需要一个@GlobalTransactional注解在业务方法上即可实现分布式事务

@GlobalTransactional注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* The interface Global transactional.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface GlobalTransactional {

/**
* Global transaction timeoutMills in MILLISECONDS.
* 全局事务超时(毫秒)默认60000 ms,即60s
*/
int timeoutMills() default TransactionInfo.DEFAULT_TIME_OUT;
public static final int DEFAULT_TIME_OUT = 60000;

/**
* Given name of the global transaction instance.
* 全局事务实例的给定名称
*/
String name() default "";

/**
* roll back for the Class
* 回滚的异常类
*/
Class<? extends Throwable>[] rollbackFor() default {};

/**
* roll back for the class name
* 回滚的类名称
*/
String[] rollbackForClassName() default {};

/**
* not roll back for the Class
* 不回滚的异常类
*/
Class<? extends Throwable>[] noRollbackFor() default {};

/**
* not roll back for the class name
* 不回滚的类名
*/
String[] noRollbackForClassName() default {};

/**
* the propagation of the global transaction
* 全局事务的传播级别,默认 REQUIRED必需的
*/
Propagation propagation() default Propagation.REQUIRED;
}

seata-server的安装

github链接https://github.com/seata/seata/releases

  1. 我们下载1.3.0版本并解压
  2. 添加一个logs的文件夹,用于存放日志
  3. 修改/conf/file.conf配置文件
    1. 自定义事务组名称(新版本没有)
    2. 事务日志存储模式dbmode = "db"
    3. 数据库连接信息
  4. mysql数据库创建库seata
  5. 在seata库中建表
    1. 在readme文件中有地址https://github.com/seata/seata/tree/develop/script/server
    2. 复制sql语句进行建表
  6. 修改/conf/registry.conf文件
    1. type = "nacos"
    2. 修改nacos配置信息(8848端口啥的默认的就不用改了)
  7. 启动8848端口的nacos
  8. 再启动seata-server

启动成功

{dataSource-1} inited
Server started, listen port: 8091

在nacos服务列表中能看到分组为SEATA_GROUP ,名称为seata-server 的服务,这个就是seata-server了

模拟分布式事务

我们创建三个服务:订单服务,库存服务和账户服务

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

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

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

seata建表sql在项目里的sql文件夹下的seata.sql

业务sql在项目里的sql文件夹下的seata_order.sql

注意一点,参与seata分布式事务的数据库需要提供一张undo_log表,用于事务的回滚日志

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

搭建seata-order-service2001

先和平时的springboot项目一样,不过篇幅受限,我这只列举核心代码

pom依赖

导入nacos和feign用于远程调用

导入seata用于分布式事务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>

yml配置文件

seata-order-service是配置到nacos的服务名称

alibaba.seata.tx-service-group=fsp_tx_group

这个fsp_tx_group属于seata的组,用于seata的分布式事务的分组,及其通知和回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server:
port: 2001

spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
username:
password:
driver-class-name: com.mysql.cj.jdbc.Driver

feign:
hystrix:
enabled: false

logging:
level:
io:
seata: info

file.conf和registry.conf

都从seata的conf文件夹下复制到项目下的resource文件夹下

register.conf可以不用改,file.conf需要添加下列

vgroupMapping.fsp_tx_group = "default",表示属于yml配置文件中的fsp_tx_group事务组

default.grouplist = "127.0.0.1:8091",表示seata服务的ip:port

1
2
3
4
5
6
7
8
9
10
11
12
13
service {
#vgroup->rgroup
vgroupMapping.fsp_tx_group = "default" #修改自定义事务组名称
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}

main启动类

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

feign远程调用storage服务

1
2
3
4
5
6
7
8
@Component
@FeignClient(value = "seata-storage-service")
public interface StorageService {

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

feign远程调用account服务

1
2
3
4
5
6
7
8
@Component
@FeignClient(value = "seata-account-service")
public interface AccountService {

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

order业务实现类

通过调用远程服务,实现下订单 -> 扣库存 -> 减余额 -> 改订单状态的业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

@Resource
private OrderMapper orderMapper;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;

@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
@Override
public void create(Order order) {
log.info("-------->开始创建新订单");
orderMapper.create(order);

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

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

log.info("-------修改订单状态");
orderMapper.update(order.getUserId(),0);
log.info("-------修改订单状态结束");

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

搭建seata-storage-service2002

和上述一样

只是修改一下yml配置文件的端口和nacos服务名称

seata-storage-service为注册到nacos的服务名称,用于被远程调用

tx-service-group: fsp_tx_group保持和其他服务一致,属于同一分布式事务组

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery:
# 配置nacos地址
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService{

@Resource
private StorageMapper storageDao;

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

搭建seata-account-service2003

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
# 配置nacos地址
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group

值得一提的是

我们首先保证接口能够成功实现业务情况下

手动模拟超时调用,抛出异常,然后看看 @GlobalTransactional注解能不能帮助实现分布式事务的回滚

所以我们在此加上 Thread.sleep(3000);等待3秒让其超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
@Slf4j
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService{
@Resource
private AccountMapper accountDao;

@Override
public void decrease(Long userId, BigDecimal money) {
log.info("账户扣除余额开始---");

//模拟超时
try{
Thread.sleep(3000);
}catch(Exception e){
e.printStackTrace();
}finally{
//do something
}
accountDao.decrease(userId, money);
log.info("账户扣除余额结束---");
}
}

测试分布式事务回滚

我们启动nacos和seata服务

正常情况下,根据OrderServiceImpl的调用顺序应该是

  • 创建一笔订单,count为10,收取100的金币,订单状态为未完成
  • 扣减10个库存
  • 用户扣减100金币
  • 订单状态修改为已完成

可是我们在账户扣减余额服务中sleep了

那么余额扣减一定会超时出错

如果没有回滚情况下,下订单和减库存都会被执行

如果回滚了,那么数据库不会发生改变

那么我们访问接口http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

发现接口在一段时间后,报出了错误页面

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Tue Dec 07 23:28:01 CST 2021

There was an unexpected error (type=Internal Server Error, status=500).

Read timed out executing POST http://seata-account-service/account/decrease?userId=1&money=100

我们查看数据库,发现数据库并没有发生改变

证明@GlobalTransactional注解生效,分布式事务进行了回滚

seata的AT模式

官网介绍AT模式https://seata.io/zh-cn/docs/dev/mode/at-mode.html

术语

  • TC 事务协调者 (Transaction Coordinator) ,seata-server
    • 维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM 事务管理器 (Transaction Manager),事务的发起方
    • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM 资源管理器 (Resource Manager),被调用的参与事务的服务
    • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

流程在上文已经提过了,这里再加深一下印象

前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

举例说明

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁

正常情况:

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

回滚情况:

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

AT模式是如何做分布式事务的

上文提到了,对于使用到seata的分布式事务的数据库,需要一张undo_log表用于回滚日志的记录,下文将讲解其作用

一阶段:加载

  • 前置镜像seata拦截业务SQL,生成before image,前置镜像
  • 业务:让数据库执行业务sql
  • 后置镜像:执行sql后,根据前镜像的结果,通过 主键 定位数据 ,生成after image,后置镜像
  • 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
  • 生成全局锁:提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
  • 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO_LOG 一并提交后将本地事务提交的结果上报给 TC

二阶段:回滚

如果业务出现问题,则在二阶段的seata需要回滚一阶段已经执行的业务sql,还原业务数据

回滚方式是before image来逆向sql,还原数据

但还需在还原前校验脏写对比当前数据库的数据和after image

如果当前数据和after image一致,证明数据没有被脏写,则直接还原数据

下面是具体实现

  • 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作
  • 通过 XID Branch ID 查找到相应的 UNDO_LOG 记录
  • 数据校验:拿 UNDO_LOG 中的后置镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理
  • 根据 UNDO_LOG 中的前置镜像和业务 SQL 的相关信息生成并执行回滚的语句
  • 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC

二阶段:提交

  • 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  • 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO_LOG 记录

本个人博客提供的内容仅用于个人学习,不保证内容的正确性。通过使用本站内容随之而来的风险与本站无关!