該筆記整理至尚硅谷周陽老師的SpringCloud課程SpringCloud Alibaba篇
SpringCloud Alibaba入門簡介
Spring Cloud Netflix 專案進入維護模式,Spring Cloud Netflix 將不再開發新的組件,Spring Cloud 版本迭代算是比較快的,因而出現了很多重大 ISSUE 都還來不及 Fix 就又推另一個 Release 了,進入維護模式意思就是目前一直以后一段時間Spring Cloud Netflix提供的服務和功能就這么多了,不在開發新的組件和功能了,以后將以維護和Merge分支Full Request為主,新組件功能將以其他替代平代替的方式實作,基于該背景下,誕生了 SpringCloud Alibaba.
官網:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
SpringCloud Alibaba 特性
- 服務限流降級:默認支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降級功能的接入,可以在運行時通過控制臺實時修改限流降級規則,還支持查看限流降級 Metrics 監控,
- 服務注冊與發現:適配 Spring Cloud 服務注冊與發現標準,默認集成了 Ribbon 的支持,
- 分布式配置管理:支持分布式系統中的外部化配置,配置更改時自動重繪,
- 訊息驅動能力:基于 Spring Cloud Stream 為微服務應用構建訊息驅動能力,
- 阿里云物件存盤:阿里云提供的海量、安全、低成本、高可靠的云存盤服務,支持在任何應用、任何時間、任何地點存盤和訪問任意型別的資料,
- 分布式任務調度:提供秒級、精準、高可靠、高可用的定時(基于 Cron 運算式)任務調度服務,同時提供分布式的任務執行模型,如網格任務,網格任務支持海量子任務均勻分配到所有 Worker(schedulerx-client)上執行,
Spring Alibaba核心組件

官網:https://spring.io/projects/spring-cloud-alibaba#overview
英文檔案:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
中文檔案:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
Nacos服務注冊和配置中心
Nacos簡介
官網:https://nacos.io/zh-cn/index.html
GitHub:https://github.com/alibaba/Nacos
開發手冊:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
Nacos,全稱 Dynamic Naming and Configuration Service,Nacos = Eureka+Config +Bus,能夠替代 Eureka 做服務注冊中心和 Config 做服務配置中心,
各類注冊中心比較
| 服務注冊與發現框架 | CAP模型 | 控制臺管理 | 社區活躍度 |
|---|---|---|---|
| Euraka | AP | 支持 | 低(2.x 閉源) |
| Zookeeper | CP | 不支持 | 中 |
| Consul | CP | 支持 | 高 |
| Nacos | AP | 支持 | 高 |
Nacos安裝與運行
- 準備 Java8 + Maven 環境
- 下載 Nocas:https://github.com/alibaba/nacos/releases
- 解壓安裝包,直接運行 bin 目錄下的 startup.cmd
startup.cmd -m standalone
- 命令運行成功后直接訪問:http://localhost:8848/nacos
Nacos服務注冊中心
基于Nacos的服務提供者
-
新建 module:cloudalibaba-provider-payment9001
-
改 POM
- 修改父 POM,添加以下依賴
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>- 本模塊 POM
<dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- SpringBoot整合Web組件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> -
寫 YML
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
- 主啟動
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}
- 業務類
@RestController
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "https://www.cnblogs.com/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
- 測驗
- 訪問:http://localhost:9001/payment/nacos/1,控制臺輸出服務注冊成功提示;
- 訪問 Nacos 控制臺:http://localhost:8848/nacos,服務串列中顯示注冊成功的服務提供者
基于Nacos的服務消費者
- 根據 cloudalibaba-provider-payment9001 新建 cloudalibaba-provider-payment9002 演示負載均衡,
- 新建 module:cloudalibaba-consumer-nacos-order83
- 改 POM
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.xiaobai.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 寫 YML
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消費者將要去訪問的微服務名稱(注冊成功進nacos的微服務提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
- 主啟動
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain83
{
public static void main(String[] args)
{
SpringApplication.run(OrderNacosMain83.class,args);
}
}
-
業務類
- config
@Configuration public class ApplicationContextBean { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }- OrderNacosController
@RestController 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); } } -
測驗
- 啟動 Nocas 服務、服務提供者 9001、9002
- 訪問:http://localhost:83/consumer/payment/nacos/13,實作 83 訪問 9001/9002,輪詢負載OK
為什么 Nocas 支持負載輪詢? Nocas 中內置了 Ribbon !

服務注冊中心對比
Nocas 全景圖

Nacos與其他注冊中心特性對比
| Nacos | Euraka | 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集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
| Dubbon集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
| K8s集成 | 支持 | 不支持 | 支持 | 支持 | 不支持 |
Nacos 服務發現實體模型

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=https://www.cnblogs.com/xiaobaiLX/p/CP'
Nacos服務配置中心
基礎配置
- 新建 module:cloudalibaba-config-nacos-client3377
- 改 POM
<dependencies>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般基礎配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
-
寫 YML
- bootstrap
# nacos配置 server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服務注冊中心地址 config: server-addr: localhost:8848 #Nacos作為配置中心地址 file-extension: yaml #指定yaml格式的配置 # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}- application
spring: profiles: active: dev # 表示開發環境 -
主啟動
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377
{
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
-
業務類
- controller
@RestController //通過Spring Cloud原生注解@RefreshScope實作配置自動更新 @RefreshScope //在控制器類加入@RefreshScope注解使當前類下的配置支持Nacos的動態重繪功能, public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } } -
在 Nacos 中添加配置資訊
Nacos中的匹配規則:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
設定DataId:\({spring.application.name}-\){spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
- prefix 默認為 spring.application.name 的值;
- spring.profile.active 即為當前環境對應的 profile,可以通過配置項 spring.profile.active 來配置;
- file-exetension 為配置內容的資料格式,可以通過配置項 spring.cloud.nacos.config.file-extension 來配置
Nacos 會記錄組態檔的歷史版本默認保留30天,此外還有一鍵回滾功能,回滾操作將會觸發配置更新,

- 測驗
- 啟動前需要在nacos客戶端-配置管理-配置管理欄目下有對應的yaml組態檔;
- 運行cloud-config-nacos-client3377的主啟動類;
- 呼叫介面查看配置資訊:http://localhost:3377/config/info
- 修改下 Nacos 中的yaml組態檔,再次呼叫查看配置的介面,就會發現配置已經重繪
分類配置
多環境多專案管理中面臨的問題:
- 實際開發中,通常一個系統會準備 dev開發環境、test測驗環境、prod生產環境,如何保證指定環境啟動時服務能正確讀取到Nacos上相應環境的組態檔呢?
- 一個大型分布式微服務系統會有很多微服務子專案,每個微服務專案又都會有相應的開發環境、測驗環境、預發環境、正式環境......
那怎么對這些微服務配置進行管理呢?
Nacos 的圖形化管理界面

Namespace+Group+Data ID三者關系

默認情況:Namespace=public,Group=DEFAULT_GROUP, 默認Cluster是DEFAULT
Nacos 默認的命名空間是 public,Namespace 主要用來實作隔離,比方說我們現在有三個環境:開發、測驗、生產環境,我們就可以創建三個Namespace,不同的Namespace之間是隔離的,
Group 默認是 DEFAULT_GROUP,Group 可以把不同的微服務劃分到同一個分組里面去,
Service 就是微服務;一個Service可以包含多個Cluster(集群),Nacos 默認 Cluster 是 DEFAULT,Cluster 是對指定微服務的一個虛擬劃分,比方說為了容災,將 Service 微服務分別部署在了杭州機房和廣州機房,這時就可以給杭州機房的 Service 微服務起一個集群名稱(HZ),給廣州機房的Service微服務起一個集群名稱(GZ),還可以盡量讓同一個機房的微服務互相呼叫,以提升性能,
最后是 Instance,就是微服務的實體,
三種方案加載配置
-
DataID方案:指定spring.profile.active和組態檔的DataID來使不同環境下讀取不同的配置,
- 新建 dev 配置 DataID

- 同上,新建 test 配置 DataID
- 通過spring.profile.active屬性就能進行多環境下組態檔的讀取

- 測驗,訪問:http://localhost:3377/config/info,回傳配置內容
-
Group方案:通過Group實作環境區分
- 新建 Group,在 nacos 圖形界面控制臺上面新建組態檔DataID

- 在config下增加一條group的配置即可,可配置為 DEV_GROUP 或 TEST_GROUP

- 測驗,訪問:http://localhost:3377/config/info,回傳配置內容
-
Namespace方案
- 新建dev/test的Namespace

- 回到服務管理-服務串列查看

- 按照域名配置填寫

-
修改 YML
- bootstrap:config 添加 namespace 配置
config: server-addr: localhost:8848 #Nacos作為配置中心地址 file-extension: yaml #這里我們獲取的yaml格式的配置 namespace: 5da1dccc-ee26-49e0-b8e5-7d9559b95ab0 #group: DEV_GROUP group: TEST_GROUP- application
# Nacos注冊配置,application.yml spring: profiles: #active: test active: dev #active: info -
測驗,訪問:http://localhost:3377/config/info,回傳配置內容
Nacos集群和持久化配置
官網說明
官方檔案:https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html

默認Nacos使用嵌入式資料庫實作資料的存盤,所以,如果啟動多個默認配置下的Nacos節點,資料存盤是存在一致性問題的,
為了解決這個問題,Nacos采用了集中式存盤的方式來支持集群化部署,目前只支持MySQL的存盤,


查看官網檔案說明:https://nacos.io/zh-cn/docs/deployment.html
Nacos持久化配置
Nacos默認自帶的是嵌入式資料庫derby,說明:https://github.com/alibaba/nacos/blob/develop/config/pom.xml
切換配置 MySQL 步驟
- 新建 nacos 資料庫,在 nacos-server-1.1.4\nacos\conf 目錄下找到 sql 腳本,執行:
create database nacos;
use nacos;
source D:\Devware\nacos-server-2.1.0-BETA\conf\nacos-mysql.sql
- nacos-server-1.1.4\nacos\conf 目錄下找到 application.properties,修改資料庫配置
#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
- 啟動Nacos,可以看到是個全新的空記錄界面,以前是記錄進derby
Linux版Nacos+MySQL、生產環境配置
-
環境準備
- 下載 Linux 版 Nacos: https://github.com/alibaba/nacos/releases/tag/1.1.4,解壓到 opt 目錄下
tar -xzvf /opt/ nacos-server-1.1.4.tar.gz -
集群配置
- 新建 nacos 資料庫,將 nacos 安裝目錄里的 nacos-mysql.sql 匯入到 mysql 資料庫中
create database nacos; use nacos; source /opt/nacos/confnacos-mysql.sql- 修改 application.properties 配置
cp application.properties.example application.properties vim application.properties
- Linux服務器上nacos的集群配置 cluster.conf
cp cluster.conf.example cluster.conf vim cluster.conf- 編輯Nacos的啟動腳本 startup.sh,使它能夠接受不同的啟動埠
cd /opt/nacos/bin vim startup.sh

#執行方式 ./startup.sh -p 3333 ./startup.sh -p 4444- Nginx的配置,由它作為負載均衡器
vim /usr/local/nginx/conf配置內容
upstream cluster{ server 127.0.0.1:3333; server 127.0.0.1:4444; server 127.0.0.1:5555; } server { listen 1111; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { #root html; #index index.html index.htm; proxy_pass http://cluster; } ......啟動 nginx,執行: ./nginx -C /usr/local/nginx/conf/nginx.conf
-
截止到此處,1個Nginx+3個nacos注冊中心+1個mysql
測驗通過nginx訪問nacos :http://192.168.4.15:1111/nacos/#/login
新建一個測驗配置

服務器插入一條資料

-
測驗
- 修改 cloudablibaba-provider-payment9002 yml
server-addr: 192.168.111.144:1111- 微服務 cloudalibaba-provider-payment9002 啟動,注冊進 nacos 集群

Sentinel實作熔斷與限流
Sentinel簡介
GitHub:https://github.com/alibaba/Sentinel
Sentinel 是一款功能強大的流量控制組件,以 flow 為突破點,覆寫流量控制、并發限制、斷路、自適應系統保護等多個領域,保障微服務的可靠性,

使用手冊:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
Sentinel安裝
Sentinel 分為兩個部分:
- 核心庫(Uava客戶端):不依賴任何框架/庫,能夠運行于所有ava運行時環境,同時對 Dubbo/Spring Cloud 等框架也有較好的支特,
- 控制臺(Dashboard):基于Spring Boot開發,打包后可以直接運行,不需要額外的 Tomcat 等應用容器,
- 下載:https://github.com/alibaba/Sentinel/releases
- 保證 Java8 環境且 8080 埠不被占用,運行
java -jar sentinel-dashboard-1.7.0.jar - 訪問sentinel管理界面:http://localhost:8080 ,登錄賬號密碼均為 sentinel
初始化工程
- 新建 module,cloudalibaba-sentinel-service8401
- 改 POM
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后續做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合Web組件+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 寫 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: 8719
management:
endpoints:
web:
exposure:
include: '*'
- 主啟動
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401
{
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
- controller
@RestController
@Log4j2
public class FlowLimitController
{
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
return "------testB";
}
}
-
測驗
- 啟動 nacos 執行
startup.cmd -m standalone,訪問:http://localhost:8848/nacos/#/login - 啟動 sentinel,執行
java -jar sentinel-dashboard-1.7.0.jar,訪問:http://localhost:8080 - 啟動8401微服務,查看 sentienl 控制臺,分別訪問:http://localhost:8401/testA、http://localhost:8401/testB
- 啟動 nacos 執行

Sentinel 采用的懶加載方式,只有在微服務被訪問之后 Sentienl 才進行監測,
流控規則

- 資源名:唯一名稱,默認請求路徑
- 針對來源:Sentinel可以針對呼叫者進行限流,填寫微服務名,默認default(不區分來源)
- 調值型別單機闊值:
- QPS(每秒鐘的請求數量):當呼叫該 API 的 QPS 達到閾值的時候,進行限流
- 執行緒數:當呼叫該p的執行緒數達到闊值的時候,進行限流
- 是否集群:不需要集群
- 流控模式:
- 直接:API 達到限流條件時,直接限流
- 關聯:當關聯的資源達到閾值時,就限流自己
- 鏈路:只記錄指定鏈路上的流量(指定資源從入口資源進來的流量,如果達到城值,就進行限流)【 API 級別的針對來源】
- 流控效果:
- 快速失敗:直接失敗,拋例外
- warm Up:根據 codeFactor (冷加載因子,默認3) 的值,從闊值/codeFactor,經過預熱時長,才達到設定的QPS闊值
- 排隊等待:勻速排隊,讓請求以勻速的速度通過,閾值型別必須設定為QPS,否則無效
流量模式
- 直接模式(默認):直接->快速失敗

表示1秒鐘內查詢1次就是OK,若超過次數1,就直接-快速失敗,報默認錯誤,
? 快速點擊訪問:http://localhost:8401/testA,結果 Blocked by Sentinel (flow limiting)
- 關聯模式:當關聯的資源達到閾值時,就限流自己

postman 模擬并發密集訪問 testB,大批量執行緒高并發訪問B,導致A失效了

- 鏈路模式:多個請求呼叫了同一個微服務
流控效果
-
默認的流控處理:直接->快速失敗
原始碼:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
-
預熱:閾值除以coldFactor(默認值為3),經過預熱時長后才會達到閾值

檔案:https://github.com/alibaba/Sentinel/wiki/流量控制
原始碼:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
例如:系統初始化的閥值為10 / 3 約等于3,即閥值剛開始為3;然后過了5秒后閥值才慢慢升高恢復到10

測驗:多次點擊:http://localhost:8401/testB,剛開始不行,后續慢慢OK
運用場景如:秒殺系統在開啟的瞬間,會有很多流量上來,很有可能把系統打死,預熱方式就是把為了保護系統,可慢慢的把流量放進來,慢慢的把閥值增長到設定的閥值,
- 等待排隊:勻速排隊,讓請求以均勻的速度通過,閥值型別必須設成QPS,否則無效,
官網:https://github.com/alibaba/Sentinel/wiki/流量控制
原始碼:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
測驗

降級規則
官網:https://github.com/alibaba/Sentinel/wiki/熔斷降級
Sentinel 熔斷降級會在呼叫鏈路中某個資源出現不穩定狀態時(例如呼叫超時或例外比例升高),對這個資源的呼叫進行限制,
讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤,
當資源被降級后,在接下來的降級時間視窗之內,對該資源的呼叫都自動熔斷(默認行為是拋出 DegradeException),
注意:Sentinel的斷路器是沒有半開狀態的

降級策略實戰
-
RT(平均回應時間,秒級):平均回應時間 超出閾值 且 在時間視窗內通過的請求>=5,兩個條件同時滿足后觸發降級,視窗期過后關閉斷路器,RT最大4900(更大的需要通過-Dcsp.sentinel.statistic.max.rt=XXXX才能生效),
- 代碼
@GetMapping("/testD") public String testD() { //暫停幾秒鐘執行緒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testD 測驗RT"); return "------testD"; }- 配置

- jmeter壓測

-
結論
永遠一秒鐘打進來10個執行緒(大于5個了)呼叫testD,我們希望200毫秒處理完本次任務,如果超過200毫秒還沒處理完,在未來1秒鐘的時間視窗內,斷路器打開(保險絲跳閘)微服務不可用,保險絲跳閘斷電了,后續我停止jmeter,沒有這么大的訪問量了,斷路器關閉(保險絲恢復),微服務恢復OK,
-
例外比列(秒級):QPS >= 5 且例外比例(秒級統計)超過閾值時,觸發降級;時間視窗結束后,關閉降級,
- 代碼
@GetMapping("/testD") public String testD() { log.info("testD 測驗RT"); int age = 10/0; return "------testD"; }- 配置

- jmeter壓測

-
結論
開啟jmeter后,直接高并發發送請求,多次呼叫達到我們的配置條件了,斷路器開啟(保險絲跳閘),微服務不可用了,不再報錯error而是服務降級了,
-
例外數(分鐘級):例外數(分鐘統計)超過閾值時,觸發降級;時間視窗結束后,關閉降級,
- 代碼
@GetMapping("/testE") public String testE() { log.info("testE 測驗例外比例"); int age = 10/0; return "------testE 測驗例外比例"; }- 配置

http://localhost:8401/testE,第一次訪問絕對報錯,因為除數不能為零,我們看到error視窗,但是達到5次報錯后,進入熔斷后降級,
- jmeter壓測

熱點key限流
官網:https://github.com/alibaba/Sentinel/wiki/熱點引數限流
熱點即經常訪問的資料,很多時候我們希望統計或者限制某個熱點資料中訪問頻次最高的TopN資料,并對其訪問進行限流或者其它操作,
@SentinelResource
@GetMapping("/testHotKey")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/testHotKey",blockHandler = "dealHandler_testHotKey")
public String testHotKey(@RequestParam(value = "https://www.cnblogs.com/xiaobaiLX/p/p1",required = false) String p1,
@RequestParam(value = "https://www.cnblogs.com/xiaobaiLX/p/p2",required = false) String p2){
return "------testHotKey";
}
public String dealHandler_testHotKey(String p1,String p2,BlockException exception)
{
return "-----dealHandler_testHotKey";
}

限流模式只支持QPS模式,固定寫死了(這才叫熱點),@SentinelResource注解的方法引數索引,0代表第一個引數,1代表第二個引數,以此類推單機閥值以及統計視窗時長表示在此視窗時間超過閥值就限流,上面的抓圖就是第一個引數有值的話,1秒的QPS為1,超過就限流,限流后呼叫dealHandler_testHotKey支持方法,
測驗:
- http://localhost:8401/testHotKey?p1=abc,快速訪問回傳例外
- http://localhost:8401/testHotKey?p1=abc&p2=33,快速訪問回傳例外
- http://localhost:8401/testHotKey?p2=abc,快速訪問成功返回
引數例外項
我們期望p1引數當它是某個特殊值時,它的限流值和平時不一樣,假如當p1的值等于5時,它的閾值可以達到200

- 訪問:http://localhost:8401/testHotKey?p1=5,快速訪問成功
- 訪問:http://localhost:8401/testHotKey?p1=3,快速訪問回傳例外
注意:熱點引數的注意點,引數必須是基本型別或者String
系統規則
官網:https://github.com/alibaba/Sentinel/wiki/系統自適應限流
系統保護規則是從應用級別的入口流量進行控制,從單臺機器的Iod、CPU使用率、平均RT、入口QPS和并發執行緒數等幾個維度監控應用指標,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性,
系統保護規則是應用整體維度的,而不是資源維度的,并且僅對入口流量生效,入口流量指的是進入應用的流量(EntryType.IW),比如Web服務或Dubbo服務端接收的請求,都屬于入口流量,
系統規則支持以下的模式:
- Load自適應(僅對Linux/Unix-ike機器生效):系統的load1作為啟發指標,進行自適應系統保護,當系統Ioād1超過設定的啟發值,且系統當前的并發執行緒數超過估算的系統容量時才會觸發系統保護(BBR階段),系統容量由系統的maxQps*mit估算得出,設定參考值一般是CPU cores 2.5,
- CPU usage(1.5.0+版本):當系統CPU使用率超過閾值即觸發系統保護(取值范圍0.0-1.0),比較靈敏,
- 平均T:當單臺機器上所有入口流量的平均T達到闊值即觸發系統保護,單位是毫秒,
- 并發執行緒數:當單臺機器上所有入口流量的并發執行緒數達到闊值即觸發系統保護,
- 入口QPS:當單臺機器上所有入口流量的QPS達到閾值即觸發系統保護,
@SentinelResource
按資源名限流+后續處理
啟動 Nacos,執行:startup.cmd -m standalone,訪問:http://localhost:8848/nacos/#/login 測驗
啟動 Sentinel,執行:java -jar sentinel-dashboard-1.7.0.jar
修改 cloudablibaba-sentinel-service8401
- 改 POM,添加以依賴
<dependencies>
<dependency><!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<groupId>com.xiaobai.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
- controller,新建 RateLimitController
@RestController
public class RateLimitController
{
@GetMapping("/byResource")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/byResource",blockHandler = "handleException")
public CommonResult byResource()
{
return new CommonResult(200,"按資源名稱限流測驗OK",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception)
{
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服務不可用");
}
}
-
流控規則配置
- 配置步驟

- 圖形配置與代碼關系

-
測驗,啟動 8401,訪問:http://localhost:8401/byResource,間隔時間大于等于 1s 訪問成功,間隔時間小于 1s 回傳自定義的限流處理資訊,
遺留問題: 當 8401 服務關閉,Sentinel 流量控制規則消失,
按Url地址限流+后續處理
- 修改 controller,添加如下方法
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/byUrl")
public CommonResult byUrl()
{
return new CommonResult(200,"按url限流測驗OK",new Payment(2020L,"serial002"));
}
- 初步測驗,訪問:http://localhost:8401/rateLimit/byUrl,訪問成功
- 配置流控規則

- 二次測驗,連續訪問:http://localhost:8401/rateLimit/byUrl,回傳 Sentinel 自帶的限流處理結果
上述兜底方案面臨的問題:
- 系統默認的,沒有體現我們自己的業務要求,
- 依照現有條件,我們自定義的處理方法又和業務代碼耦合在一塊,不直觀,
- 每個業務方法都添加一個兜底的,那代碼膨脹加劇,
- 全域統一的處理方法沒有體現,
客戶自定義限流處理邏輯
- 創建 CustomerBlockHandler 類用于自定義限流處理邏輯
public class CustomerBlockHandler
{
public static CommonResult handleException(BlockException exception){
return new CommonResult(2020,"自定義的限流處理資訊......CustomerBlockHandler-----1");
}
public static CommonResult handleException2(BlockException exception){
return new CommonResult(2020,"自定義的限流處理資訊......CustomerBlockHandler------2");
}
}
- RateLimitController 添加自定義限流處理邏輯
/**
* 自定義通用的限流處理邏輯,
* blockHandlerClass = CustomerBlockHandler.class
* blockHandler = handleException2
* 上述配置:找CustomerBlockHandler類里的handleException2方法進行兜底處理
*/
/**
* 自定義通用的限流處理邏輯
*/
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
public CommonResult customerBlockHandler()
{
return new CommonResult(200,"按客戶自定義限流處理邏輯");
}
- 啟動微服務后呼叫:http://localhost:8401/rateLimit/customerBlockHandler
- Sentinel 控制臺添加配置

- 再次呼叫:http://localhost:8401/rateLimit/customerBlockHandler
服務熔斷
Ribbon系列
服務提供者9003/9004
- 新建cloudalibaba-provider-payment9003/9004
- 改 POM
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency><!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<groupId>com.xiaobai.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 寫 YML(記得該埠號)
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
- 主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
- 業務類
@RestController
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
public static HashMap<Long,Payment> hashMap = new HashMap<>();
static
{
hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "https://www.cnblogs.com/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
{
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort: "+serverPort,payment);
return result;
}
}
- 測驗,訪問:http://localhost:9003/paymentSQL/1
服務消費者84
- 新建 cloudalibaba-consumer-nacos-order84
- 改 POM
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.xiaobai.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 寫 YML
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默認8719埠,假如被占用會自動從8719開始依次+1掃描,直至找到未被占用的埠
port: 8719
#消費者將要去訪問的微服務名稱(注冊成功進nacos的微服務提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
- 主啟動
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
- 配置類
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
- 業務類
@RestController
@Slf4j
public class CircleBreakerController
{
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/fallback")
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法引數例外....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,該ID沒有對應記錄,空指標例外");
}
return result;
}
}
- 測驗:http://localhost:84/consumer/fallback/4,直接給客戶展示 error 頁面,不友好
添加 fallback 配置
- 修改 CircleBreakerController,添加 fallback 配置
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/fallback",fallback = "handlerFallback") //fallback負責業務例外
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法引數例外....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,該ID沒有對應記錄,空指標例外");
}
return result;
}
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底例外handlerFallback,exception內容 "+e.getMessage(),payment);
}

- 測驗



添加 blockHandler
- 修改 CircleBreakerController,添加 fallback 配置
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/fallback",blockHandler = "blockHandler") //blockHandler負責在sentinel里面配置的降級限流
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("非法引數例外....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,該ID沒有對應記錄");
}
return result;
}
public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,無此流水: blockException "+blockException.getMessage(),payment);
}

- Sentinel 配置

- 測驗

添加 fallback 和 blockHandler 配置
- 修改 CircleBreakerController,添加 fallback 和 blockHandler 配置
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/fallback",fallback = "handlerFallback",blockHandler = "blockHandler")
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("非法引數例外....");
}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,"fallback,無此流水,exception "+e.getMessage(),payment);
}
public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,無此流水: blockException "+blockException.getMessage(),payment);
}

- Sentinel 配置

- 測驗

若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出 BlockException 時只會進入 blockHandler 處理邏輯,
忽略屬性...
- 修改 controller
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "https://www.cnblogs.com/xiaobaiLX/p/fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("非法引數例外....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,該ID沒有對應記錄");
}
return result;
}

Feign系列
修改84模塊
- 修改 POM,添加依賴
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 修改 YML,添加配置
# 激活Sentinel對Feign的支持
feign:
sentinel:
enabled: true
-
業務類
- 帶 @FeignClient 注解的業務介面
/** 使用 fallback 方式是無法獲取例外資訊的, * 如果想要獲取例外資訊,可以使用 fallbackFactory引數 */ @FeignClient(value = "https://www.cnblogs.com/xiaobaiLX/p/nacos-payment-provider",fallback = PaymentFallbackService.class)//呼叫中關閉9003服務提供者 public interface PaymentService { @GetMapping(value = "https://www.cnblogs.com/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id); }- 添加 fallback 類
@Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult<Payment> paymentSQL(Long id) { return new CommonResult<>(444,"服務降級回傳,沒有該流水資訊",new Payment(id, "errorSerial......")); } } -
controller
//==================OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "https://www.cnblogs.com/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
{
if(id == 4)
{
throw new RuntimeException("沒有該id");
}
return paymentService.paymentSQL(id);
}
- 主啟動,添加 @EnableFeignClients 啟動 Feign 的功能
- 測驗,訪問:http://localhost:84/consumer/paymentSQL/1,故意關閉9003微服務提供者,看84消費側自動降級,不會被耗死
熔斷框架比較
| Sentinel | Hystrix | resilience4j | |
|---|---|---|---|
| 隔離策略 | 信號量隔離(并發執行緒數限質) | 執行緒池隔離/信號量隔離 | 信號量隔離 |
| 熔斷策略 | 基于回應時間、例外比率、例外數 | 基于例外比率 | 基于例外比率、回應時間 |
| 實時統計實作 | 滑動視窗 | 滑動視窗 | Ring Bit Buffer |
| 動態規則配置 | 支持多種資料源 | 支持多種資料源 | 有限支持 |
| 擴展性 | 多個擴展點 | 插件形式 | 介面形式 |
| 基于注解支持 | 支持 | 支持 | 支持 |
| 限流 | 基于QPS,支持基于呼叫關系的限流 | 有限支持 | Rate Limiter |
| 流量整形 | 支持預熱模式、勻速器模式、預熱排隊模式 | 不支持 | 簡單的 Rate Limiter |
| 系統自適應保護 | 支持 | 不支持 | 不支持 |
| 控制臺 | 提供開箱即用的控制臺,可配置規則、查看秒級監控 | 簡單的監控查看 | 不提供控制臺,可對接其他監控系統 |
規則持久化
- 修改 cloudalibaba-sentinel-service8401
- 修改 POM,添加依賴
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 修改 YML,添加 Nacos 資料源配置
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
- 添加 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表示排隊等待;
- clusterMode:是否集群,
- 啟動8401后重繪 sentinel 發現業務規則有了

- 快速訪問測驗介面:http://localhost:8401/rateLimit/byUrl,回傳限流資訊
- 停止 8401 再看 sentinel

- 重新啟動 8401 再看 sentinel,呼叫:http://localhost:8401/rateLimit/byUrl,配置重新出現了,持久化驗證通過
Seata處理分布式事務
分布式事務問題
單體應用被拆分成微服務應用,原來的三個模塊被拆分成 三個獨立的應用,分別使用 三個獨立的資料源,業務操作需要呼叫三個服務來完成,此時 每個服務內部的資料一致性由本地事務來保證,但是全域的資料一致性問題沒法保證,

總結:一次業務操作需要跨多個資料源或需要跨多個系統進行遠程呼叫,就會產生分布式事務問題,
Seata簡介
官網:http://seata.io/zh-cn/
下載:https://github.com/seata/seata/releases
Seata是一款開源的分布式事務解決方案,致力于在微服務架構下提供高性能和簡單易用的分布式事務服務,
一個典型的分布式事務程序由一個事務ID + 三個組件組成,三個組件包括:
- Transaction Coordinator (TC):事務協調器,維護全域事務的運行狀態,負責協調并驅動全域事務的提交或回滾;
- Transaction Manager (TM):控制全域事務的邊界,負責開啟一個全域事務,并最終發起全域提交或全域回滾的決議;
- Resource Manager (RM):控制分支事務,負責分支注冊、狀態匯報,并接收事務協調器的指令,驅動分支(本地)事務的提交和回滾,

處理程序:
- TM 向 TC 申請開啟一個全域事務,全域事務創建成功并生成一個全域唯一的 XID;
- XID 在微服務呼叫鏈路的背景關系中傳播;
- RM 向 TC 注冊分支事務,將其納入 XID 對應全域事務的管轄;
- TM 向 TC 發起針對 XID 的全域提交或回滾決議;
- TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求,

Seata 只需兩個注解:本地@Transactional 和 全域@GlobalTransactional 即可實作分布式事務控制,
Seata-Server安裝
-
下載:https://github.com/seata/seata/releases,課程版本下載的是seata-server-0.9.0.zip
-
seata-server-0.9.0.zip 解壓到指定目錄并修改 conf 目錄下的 file.conf 組態檔
-
備份 file.conf 檔案
-
修改自定義事務組名稱+事務日志存盤模式為db+資料庫連接資訊
service 模塊
service { vgroup_mapping.my_test_tx_group = "fsp_tx_group" }store 模塊
## transaction log store store { ## store mode: file、db mode = "db" ## database store db { driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "root" } }
-
-
mysql5.7 資料庫新建庫 seata,運行在 seata 安裝目錄中 conf 目錄下的 db_store.sql
create database seata;
use seata;
source D:\Devware\seata-server-0.9.0\conf\db_store.sql
- 修改 seata 安裝目錄下 conf 目錄下的 registry.conf 組態檔
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #指明注冊中心為nacos
nacos {
serverAddr = "localhost:8848" #修改nacos連接資訊
}
- 啟動 Nacos 埠號8848,執行
startup.cmd -m standalone - 啟動 seata-server,運行 seata 安裝目錄中 bin 目錄下的
seata-server.bat
訂單/庫存/賬戶業務資料庫準備
創建三個服務,一個訂單服務,一個庫存服務,一個賬戶服務,當用戶下單時,會在訂單服務中創建一個訂單,然后通過遠程呼叫庫存服務來扣減下單商品的庫存,再通過遠程呼叫賬戶服務來扣減用戶賬戶里面的余額,最后在訂單服務中修改訂單狀態為已完成,該操作跨越三個資料庫,有兩次遠程呼叫,很明顯會有分布式事務問題,
- 建庫
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
- 按照上述 3 庫分別建對應業務表
#seata_order庫下建t_order表
CREATE TABLE t_order (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用戶id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '產品id',
`count` INT(11) DEFAULT NULL COMMENT '數量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金額',
`status` INT(1) DEFAULT NULL COMMENT '訂單狀態:0:創建中;1:已完結'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
#seata_storage庫下建t_storage 表
CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '產品id',
`total` INT(11) DEFAULT NULL COMMENT '總庫存',
`used` INT(11) DEFAULT NULL COMMENT '已用庫存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余庫存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0', '100');
SELECT * FROM t_storage;
#seata_account庫下建t_account 表
CREATE TABLE t_account (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用戶id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '總額度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余額',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用額度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
- 按照上述3庫分別建對應的回滾日志表,匯入 seata 安裝目錄 conf 下的 _undo_log.sql 檔案
source D:\Devware\seata-server-0.9.0\conf\db_undo_log.sql
訂單/庫存/賬戶業務微服務準備
seata-order-service2001
- 新建 moudle,seata-order-service2001
- 修改 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>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 寫 YML
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定義事務組名稱需要與seata-server中的對應
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
- 組態檔
- file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroup_mapping.fsp_tx_group = "default" #修改自定義事務組名稱
default.grouplist = "127.0.0.1:8091"
enableDegrade = false
disable = false
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
## transaction log store
store {
## store mode: file、db
mode = "db"
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
lock {
## the lock store mode: local、remote
mode = "remote"
local {
## store locks in user's database
}
remote {
## store locks in the seata's server
}
}
recovery {
#schedule committing retry period in milliseconds
committing-retry-period = 1000
#schedule asyn committing retry period in milliseconds
asyn-committing-retry-period = 1000
#schedule rollbacking retry period in milliseconds
rollbacking-retry-period = 1000
#schedule timeout retry period in milliseconds
timeout-retry-period = 1000
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
## metrics settings
metrics {
enabled = false
registry-type = "compact"
# multi exporters use comma divided
exporter-list = "prometheus"
exporter-prometheus-port = 9898
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
- registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
-
domain
- CommonResult
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code,message,null); } }- Order
@Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; /** * 訂單狀態:0:創建中;1:已完結 */ private Integer status; } -
Dao 介面實作及實作
- OrderDao
@Mapper public interface OrderDao { /** * 創建訂單 */ void create(Order order); /** * 修改訂單金額 */ void update(@Param("userId") Long userId, @Param("status") Integer status); }- resource 目錄下添加 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.xiaobai.springcloud.alibaba.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.xiaobai.springcloud.alibaba.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="product_id" property="productId" jdbcType="BIGINT"/> <result column="count" property="count" jdbcType="INTEGER"/> <result column="money" property="money" jdbcType="DECIMAL"/> <result column="status" property="status" jdbcType="INTEGER"/> </resultMap> <insert id="create"> INSERT INTO `t_order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`) VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0); </insert> <update id="update"> UPDATE `t_order` SET status = 1 WHERE user_id = #{userId} AND status = #{status}; </update> </mapper> -
Service介面及實作
- OrderService
public interface OrderService { /** * 創建訂單 */ void create(Order order); }- OrderServiceImpl
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * 創建訂單->呼叫庫存服務扣減庫存->呼叫賬戶服務扣減賬戶余額->修改訂單狀態 * 簡單說: * 下訂單->減庫存->減余額->改狀態 */ @Override @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { log.info("------->下單開始"); //本應用創建訂單 orderDao.create(order); //遠程呼叫庫存服務扣減庫存 log.info("------->order-service中扣減庫存開始"); storageService.decrease(order.getProductId(),order.getCount()); log.info("------->order-service中扣減庫存結束"); //遠程呼叫賬戶服務扣減余額 log.info("------->order-service中扣減余額開始"); accountService.decrease(order.getUserId(),order.getMoney()); log.info("------->order-service中扣減余額結束"); //修改訂單狀態為已完成 log.info("------->order-service中修改訂單狀態開始"); orderDao.update(order.getUserId(),0); log.info("------->order-service中修改訂單狀態結束"); log.info("------->下單結束"); } }- StorageService
@FeignClient(value = "https://www.cnblogs.com/xiaobaiLX/p/seata-storage-service") public interface StorageService { /** * 扣減庫存 */ @PostMapping(value = "https://www.cnblogs.com/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }- AccountService
@FeignClient(value = "https://www.cnblogs.com/xiaobaiLX/p/seata-account-service") public interface AccountService { /** * 扣減賬戶余額 */ //@RequestMapping(value = "https://www.cnblogs.com/account/decrease", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @PostMapping("/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); } -
controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 創建訂單
*/
@GetMapping("/order/create")
public CommonResult create( Order order) {
orderService.create(order);
return new CommonResult(200, "訂單創建成功!");
}
}
-
Config配置
- MyBatisConfig
@Configuration @MapperScan({"com.xiaobai.springcloud.alibaba.dao"}) public class MyBatisConfig { } }- DataSourceProxyConfig
@Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
seata-storage-service2002
-
新建moudle,seata-storage-service2002
-
改 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>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 寫 YML
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
tx-service-group: fsp_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: 123456
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
-
組態檔
- file.conf
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup vgroup_mapping.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" disableGlobalTransaction = false } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }- registry.conf
registry { # file 、nacos 、eureka、redis、zk type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6381" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } file { name = "file.conf" } } config { # file、nacos 、apollo、zk type = "file" nacos { serverAddr = "localhost" namespace = "" cluster = "default" } apollo { app.id = "fescar-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } file { name = "file.conf" } } -
domain
- CommanResult
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code,message,null); } }- Storage
@Data public class Storage { private Long id; /** * 產品id */ private Long productId; /** * 總庫存 */ private Integer total; /** * 已用庫存 */ private Integer used; /** * 剩余庫存 */ private Integer residue; } -
Dao介面及實作
- StorageDao
@Mapper public interface StorageDao { /** * 扣減庫存 */ void decrease(@Param("productId") Long productId, @Param("count") Integer count); }- resources檔案夾下新建mapper檔案夾后添加 StorageMapper.xml
<mapper namespace="com.xiaobai.springcloud.alibaba.dao.StorageDao"> <resultMap id="BaseResultMap" type="com.xiaobai.springcloud.alibaba.domain.Storage"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="product_id" property="productId" jdbcType="BIGINT"/> <result column="total" property="total" jdbcType="INTEGER"/> <result column="used" property="used" jdbcType="INTEGER"/> <result column="residue" property="residue" jdbcType="INTEGER"/> </resultMap> <update id="decrease"> UPDATE t_storage SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId} </update> </mapper> -
Service介面及實作
- StorageService
public interface StorageService { /** * 扣減庫存 */ void decrease(Long productId, Integer count); }- StorageServiceImpl
@Service public class StorageServiceImpl implements StorageService { private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class); @Resource private StorageDao storageDao; /** * 扣減庫存 */ @Override public void decrease(Long productId, Integer count) { LOGGER.info("------->storage-service中扣減庫存開始"); storageDao.decrease(productId,count); LOGGER.info("------->storage-service中扣減庫存結束"); } } -
Controller
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
/**
* 扣減庫存
*/
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200,"扣減庫存成功!");
}
}
-
Config配置
- MyBatisConfig
@Configuration @MapperScan({"com.xiaobai.springcloud.alibaba.dao"}) public class MyBatisConfig { }- DataSourceProxyConfig
@Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } } -
主啟動
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageServiceApplication2002.class, args);
}
}
seata-account-service2003
- 新建moudle,seata-account-service2003
- 改 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>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 寫YML
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
tx-service-group: fsp_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: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
-
組態檔
- file.conf
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { vgroup_mapping.fsp_tx_group = "default" #修改自定義事務組名稱 default.grouplist = "127.0.0.1:8091" enableDegrade = false disable = false max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" disableGlobalTransaction = false } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "123456" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }- registry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } } -
domain
- CommonResult
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code,message,null); } }- Account
@Data @AllArgsConstructor @NoArgsConstructor public class Account { private Long id; /** * 用戶id */ private Long userId; /** * 總額度 */ private BigDecimal total; /** * 已用額度 */ private BigDecimal used; /** * 剩余額度 */ private BigDecimal residue; } -
Dao介面及實作
- AccountDao
@Mapper public interface AccountDao { /** * 扣減賬戶余額 */ void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money); }- resources檔案夾下新建mapper檔案夾后添加AccountMapper.xml
<mapper namespace="com.xiaobai.springcloud.alibaba.dao.AccountDao"> <resultMap id="BaseResultMap" type="com.xiaobai.springcloud.alibaba.domain.Account"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="total" property="total" jdbcType="DECIMAL"/> <result column="used" property="used" jdbcType="DECIMAL"/> <result column="residue" property="residue" jdbcType="DECIMAL"/> </resultMap> <update id="decrease"> UPDATE t_account SET residue = residue - #{money},used = used + #{money} WHERE user_id = #{userId}; </update> </mapper> -
Service介面及實作
- AccountService
public interface AccountService { /** * 扣減賬戶余額 * @param userId 用戶id * @param money 金額 */ void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }- AccountServiceImpl
/** * 賬戶業務實作類 * Created by zzyy on 2019/11/11. */ @Service public class AccountServiceImpl implements AccountService { private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class); @Resource AccountDao accountDao; /** * 扣減賬戶余額 */ @Override public void decrease(Long userId, BigDecimal money) { LOGGER.info("------->account-service中扣減賬戶余額開始"); //模擬超時例外,全域事務回滾 //暫停幾秒鐘執行緒 //try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } accountDao.decrease(userId,money); LOGGER.info("------->account-service中扣減賬戶余額結束"); } } -
Controller
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣減賬戶余額
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult(200,"扣減賬戶余額成功!");
}
}
-
Config配置
- MyBatisConfig
@Configuration @MapperScan({"com.xiaobai.springcloud.alibaba.dao"}) public class MyBatisConfig { }- DataSourceProxyConfig
@Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } } -
主啟動
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003
{
public static void main(String[] args)
{
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
訂單/庫存/賬戶業務微服務測驗
- 添加訂單:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
- 初次測驗,t_order、t_storage、t_account 三個 表添加資料均符合預期;
- 給 AccountServiceImpl 添加超時,再次測驗;
- 再次測驗,當庫存和賬戶金額扣減后,訂單狀態并沒有設定為已經完成,沒有從零改為1,而且由于feign的重試機制,賬戶余額還有可能被多次扣減;
- 使用 @GlobalTransactional 對 OrderServiceImpl 進行全域事務處理;
- 再次測驗,下單后資料庫資料并沒有任何改變,記錄都添加不進來,
補充內容
再看TC/TM/RM三大組件

分布式事務的執行流程:
- TM 開啟分布式事務(TM 向 TC 注冊全域事務記錄);
- 按業務場景,編排資料庫、服務等事務內資源(RM 向 TC 匯報資源準備狀態 );
- TM 結束分布式事務,事務一階段結束(TM 通知 TC 提交/回滾分布式事務);
- TC 匯總事務資訊,決定分布式事務是提交還是回滾;
- TC 通知所有 RM 提交/回滾 資源,事務二階段結束,
AT模式如何做到對業務的無侵入
在一階段,Seata 會攔截 業務 SQL ,
- 決議 SQL 語意,找到 業務 SQL 要更新的業務資料,在業務資料被更新前,將其保存成 before image ;
- 執行 業務 SQL 更新業務資料,在業務資料更新之后,
- 其保存成 after image,最后生成行鎖,
以上操作全部在一個資料庫事務內完成,這樣保證了一階段操作的原子性,

二階段如是順利提交的話,因為“業務 SQL”在一階段已經提交至資料庫,所以Seata框架只需 將一階段保存的快照資料和行鎖刪掉,完成資料清理即可,

二階段如果是回滾的話,Seata 就需要回滾一階段已經執行的 業務 SQL ,還原業務資料,回滾方式便是用 before image 還原業務資料;但在還原前要首先要校驗臟寫,對比 資料庫當前業務資料 和 after image ,如果兩份資料完全一致就說明沒有臟寫,可以還原業務資料,如果不一致就說明有臟寫,出現臟寫就需要轉人工處理,

原始碼邏輯:

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/539135.html
標籤:Java
上一篇:Spring回圈依賴
下一篇:微信支付 “商家轉賬到零錢”
