1.SpringCloud Alibaba入門簡介
1.1.為什么會出現SpringCloud Alibaba
a. SpringCloud Netflix項目進入維護模式
https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now
Spring Cloud Netflix 項目進入維護模式
近日,Netflix宣布Hystrix 進入維護模式。Ribbon自 2016 年以來一直處於類似狀態。 雖然 Hystrix 和 Ribbon 現在處於維護模式,但它們仍然在 Netflix 大規模部署。
Hystrix Dashboard 和 Turbine 已被 Atlas 取代。對這些項目的最后一次提交分別是 2 年前和 4 年前。Zuul 1 和 Archaius 1 都被不向后兼容的更高版本所取代。
以下 Spring Cloud Netflix 模塊和相應的啟動器將被置於維護模式:
1.spring-cloud-netflix-archaius
2.spring-cloud-netflix-hystrix-contract
3.spring-cloud-netflix-hystrix-dashboard
4.spring-cloud-netflix-hystrix-stream
5.spring-cloud-netflix-hystrix
6.spring-cloud-netflix-ribbon
7.spring-cloud-netflix-turbine-stream
8.spring-cloud-netflix-turbine
9.spring-cloud-netflix-zuul
這不包括 Eureka 或 concurrency-limits 模塊。
什么是維護模式?
將模塊置於維護模式,意味着 Spring Cloud 團隊將不再向模塊添加新功能。我們將修復block級別錯誤以及安全問題,我們也會考慮並審查來自社區的小型pull request。
我們打算繼續支持這些模塊,直到 Greenwich版本 被普遍采用至少一年。
b. 進入維護模式意味着什么
進入維護模式意味着SpringCloud Netflix將不再開發新的組件
我們都知道SpringCloud版本迭代算是比較快的,因而出現了很多重大ISSUE都還來不及Fix就又推另一個Release了。進入維護模式意思就是目前一直以后一段時間SpringCloud Netflix提供的服務和功能就這么多了,不再開發新的組件和功能了。以后將以維護和Merge分支Full Request為主
新組件功能將以其它替代平代替的方式實現
1.2.SpringCloud Alibaba帶來了什么
a. 是什么
官網:
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
誕生:
2018.10.31,Spring Cloud Alibaba正式入駐了 Spring Cloud 官方孵化器,並在 Maven 中央庫發布了第一個版本。
Spring Cloud for Alibaba 0.2.0 released
The Spring Cloud Alibaba project, consisting of Alibaba’s open-source components and several Alibaba Cloud products, aims to implement and expose well known Spring Framework patterns and abstractions to bring the benefits of Spring Boot and Spring Cloud to Java developers using Alibaba products.
Spring Cloud for Alibaba,它是由一些阿里巴巴的開源組件和雲產品組成的。這個項目的目的是為了讓大家所熟知的 Spring 框架,其優秀的設計模式和抽象理念,以給使用阿里巴巴產品的 Java 開發者帶來使用 Spring Boot 和 Spring Cloud 的更多便利。
b. 能干嘛
服務限流降級:默認支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降級功能的接入,可以在運行時通過控制台實時修改限流降級規則,還支持查看限流降級 Metrics 監控。
服務注冊與發現:適配 Spring Cloud 服務注冊與發現標准,默認集成了 Ribbon 的支持。
分布式配置管理:支持分布式系統中的外部化配置,配置更改時自動刷新。
消息驅動能力:基於 Spring Cloud Stream 為微服務應用架構消息驅動能力。
阿里雲對象存儲:阿里雲提供的海量、安全、低成本、高可靠的雲存儲服務。支持在任何應用、任何時間、任何地點存儲和訪問任意類型的數據。
分布式任務調度:提供秒級、精准、高可靠、高可用的定時(基於 Cron 表達式)任務調度服務。同時提供分布式的任務執行模型,如網絡任務。網絡任務支持海量子任務均勻分配到所有 Worker(schedulerx-client)上執行。
c. 去哪下
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
d. 怎么玩
Sentinel
阿里巴巴開源產品,把流量作為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。
Nacos
阿里巴巴開源產品,一個更易於構建雲原生應用的動態服務發現、配置管理和服務管理平台。
RocketMQ
一款開源的分布式消息系統,基於高可用分布式集群技術,提供低延時的、高可靠的消息發布與訂閱服務。
Dubbo
Apache Dubbo™ 是一款高性能 Java RPC 框架。
Seata
阿里巴巴開源產品,一個易於使用的高性能微服務分布式事務解決方案。
Alibaba Cloud OSS
阿里雲對象存儲服務(Object Storage Service,簡稱 OSS),是阿里雲提供的海量、安全、低成本、高可靠的雲存儲服務。您可以在任何應用、任何時間、任何地點存儲和訪問任意類型的數據。
Alibaba Cloud SchedulerX
阿里中間件團隊開發的一款分布式任務調度產品,提供秒級、精准、高可靠、高可用的定時(基於 Cron 表達式)任務調度服務。
Alibaba Cloud SMS
覆蓋全球的短信服務,友好、高效、智能的互聯化通訊能力,幫助企業迅速搭建客戶觸達通道。
1.3.SpringCloud Alibaba學習資料
a. 官網
https://spring.io/projects/spring-cloud-alibaba#overview
Spring Cloud Alibaba 致力於提供微服務開發的一站式解決方案。此項目包含開發分布式應用微服務的必需組件,方便開發者通過 Spring Cloud 編程模型輕松使用這些組件來開發分布式應用服務。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以將 Spring Cloud 應用接入阿里微服務解決方案,通過阿里中間件來迅速搭建分布式應用系統。
SpringCloud Alibaba進入了SpringCloud官方孵化器,並且畢業了
b. 英文
1). https://github.com/alibaba/spring-cloud-alibaba
2). https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
c. 中文
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
2.SpringCloud Alibaba Nacos服務注冊和配置中心
2.1.Nacos簡介
a. 為什么叫Nacos
前四個字母分別為Naming和Configuration的前兩個字母,最后的s為Servcie。
b. 是什么
一個更易於構建雲原生應用的動態服務實現、配置管理和服務管理平台。
Nacos:Dynamic Naming and Configuration Service
Nacos就是注冊中心 + 配置中心的組合 等價於 Nacos = Eureka + Config + Bus
c. 能干嘛
- 替代Eureka做服務注冊中心
- 替代Config做服務配置中心
d. 去哪下
f. 各種注冊中心比較
服務注冊與發現框架 | CAP模型 | 控制台管理 | 社區活躍度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x版本閉源) |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP | 支持 | 高 |
據說 Nacos 在阿里巴巴內部有超過 10 萬的實例運行,已經過了類似雙十一等各種大型流量的考驗 |
2.2.安裝並運行Nacos
環境要求:本地已安裝Java8+Maven環境
a. 先從官網下載Nacos
https://github.com/alibaba/nacos/releases
b. 解壓安裝包,直接運行bin目錄下的startup.cmd
startup.cmd -m standalone
c. 命令運行成功后直接訪問http://localhost:8848/nacos
默認賬戶密碼都是nacos
d. 結果頁面
2.3.Nacos作為服務注冊中心演示
2.3.1.官方文檔
https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_dependency_management
2.3.2.基於Nacos的服務提供者
a. 新建Module(cloudalibaba-provider-payment9001)
b. POM
1). 父POM
必須引入SpringCloud Alibaba依賴:
<!-- spring cloud alibaba 2.2.6.RELEASE -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2). 本模塊POM
引入SpringCloud Alibaba Nacos依賴:
<!-- SpringCloud Aibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
c. YML
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
d. 主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}
f. 業務類
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: " + serverPort + "\t id" + id;
}
}
g. 測試
- 訪問http://localhost:9001/payment/nacos/1
- nacos控制台
- nacos服務注冊中心 + 服務提供者9001都OK了
為了下一章節演示nacos的負載均衡,參照9001新建9002
1.取巧不想新建重復體力勞動,直接拷貝虛擬端口映射
2.但是直接拷貝虛擬端口映射實際使用的還是9001,可能會出現問題,我們還是新建cloudalibaba-provider-payment9002
2.3.3.基於Nacos的服務消費者
a. 新建Module(cloudalibaba-consumer-nacos-order83)
b. POM
添加的部分代碼
<!-- SpringCloud Aibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.neo.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
為什么nacos支持負載均衡?
因為阿里的nacos整合得特別好,后面技術都會吸收前面技術的優點,天生一出來就自帶負載均衡,何以見得?
nacos-discovery包天生集成了netflix-ribbon包,只要是ribbon的話,一支持負載均衡,二支持RestTemplate(RESTful風格的遠程調用)
c. YML
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
#消費者將要去訪問的微服務名稱(注冊成功進nacos的微服務提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
d. 主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class, args);
}
}
f. 業務類
1). ApplicationContextBean
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
2). OrderNacosController
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);
}
}
g. 測試
1). nacos控制台
2). 訪問http://localhost:83/consumer/payment/nacos/13
83訪問9001/9002負載輪詢OK
2.3.4.服務注冊中心對比
各種注冊中心對比
a. Nacos全景圖所示
b. Nacos和CAP
Nacos與其他注冊中心特性對比
Nacos | Eureka | Consul | CoreDNS | Zookeeper | |
---|---|---|---|---|---|
一致性協議 | CP+AP | AP | CP | / | CP |
健康檢查 | TCP/HTTP/MySQL/Client Beat | Client Beat | TCP/HTTP/gRPC/Cmd | / | Client Beat |
負載均衡 | 權重/DSL/metadata/CMDB | Ribbon | Fabio | RR | / |
雪崩保護 | 支持 | 支持 | 不支持 | 不支持 | 不支持 |
自動注銷實例 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
訪問協議 | HTTP/DNS/UDP | HTTP | HTTP/DNS | DNS | TCP |
監聽支持 | 支持 | 支持 | 支持 | 不支持 | 支持 |
多數據中心 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
跨注冊中心 | 支持 | 不支持 | 支持 | 不支持 | 不支持 |
SpringCloud集成 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
K8s集成 | 支持 | 不支持 | 支持 | 支持 | 不支持 |
c. Nacos支持AP和CP模式的切換
C是所有節點在同一時間看到的數據是一致的;而A的定義是所有請求都會收到響應(可用性)。
何時選擇使用何種模式?
一般來說,如果不需要存儲服務級別的信息且服務實例是通過nacos-client注冊,並能夠保持心跳上報,那么就可以選擇AP模式。當前主流的服務如 Spring Cloud 和 Dubbo 服務,都適用於AP模式,AP模式是為了服務的可用性而減弱了一致性,因此AP模式下只支持注冊臨時實例。
如果需要在服務級別編輯或者存儲配置信息,那么 CP 是必須的,K8s服務和DNS服務則適用於CP模式。
CP模式下則支持注冊持久化實例,此時則是以 Raft 協議為集群運行模式,該模式下注冊實例之前必須先注冊服務,如果服務不存在,則會返回錯誤。
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
2.4.Nacos作為服務配置中心演示
2.4.1.Nacos作為配置中心-基礎配置
a. 新建Module(cloudalibaba-config-nacos-client3377)
b. POM
添加的部分依賴:
<!-- nacos-config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- nacos-discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
c. YML
配置bootstrap.yml和application.yml
1). 為什么配置兩個
Nacos同springcloud-config一樣,在項目初始化時,要保證先從配置中心進行配置拉取,拉取配置之后,才能保證項目的正常啟動。
springboot中配置文件的加載是存在優先級順序的,bootstrap優先級高於application
2). 配置文件添加
2.1). bootstrap.yml
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服務注冊中心地址
config:
server-addr: localhost:8848 #Nacos作為配置中心地址
file-extension: yaml #指定yaml格式的配置
# ${spring.application.name}-${spring.profiles.active}-${spring.cloud.nacos.config.file-extension}
2.2). application.yml
spring:
profiles:
active: dev # 表示開發環境
d. 主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
e. 業務類
@RestController
@RefreshScope // 支持Nacos的動態刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
f. 在Nacos中添加配置信息
Nacos中的匹配規則
1). 理論
Nacos中的dataid的組成格式及與SpringBoot配置文件中的匹配規則
官網:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
說明:之所以需要配置
spring.application.name
,是因為它是構成 Nacos 配置管理dataId
字段的一部分。在 Nacos Spring Cloud 中,
dataId
的完整格式如下:${prefix}-${spring.profiles.active}.${file-extension}
prefix
默認為spring.application.name
的值,也可以通過配置項spring.cloud.nacos.config.prefix
來配置。spring.profiles.active
即為當前環境對應的 profile,詳情可以參考 Spring Boot文檔。 注意:當spring.profiles.active
為空時,對應的連接符-
也將不存在,dataId 的拼接格式變成${prefix}.${file-extension}
file-exetension
為配置內容的數據格式,可以通過配置項spring.cloud.nacos.config.file-extension
來配置。目前只支持properties
和yaml
類型。
通過 Spring Cloud 原生注解@RefreshScope
實現配置自動更新:
最后公式:
${spring.application.name}-${spring.profiles.active}-${spring.cloud.nacos.config.file-extension}
2). 實操
2.1). 配置新增
根據公式:${spring.application.name}-${spring.profiles.active}-${spring.cloud.nacos.config.file-extension}
設置DataId:nacos-config-client-dev.yaml
2.1). Nacos界面配置對應
小總結:
g. 測試
啟動前需要在nacos客戶端-配置管理-配置列表欄目下有對應的yaml配置文件
運行cloud-config-nacos-client3377的主啟動類
調用http://localhost:3377/config/info接口查看配置信息
h. 自帶動態刷新
修改下Nacos中的yaml配置文件,再次調用查看配置的接口,就會發現配置已經刷新
2.4.2.Nacos作為配置中心-分類配置
a. 問題:多環境多項目管理
問題1:
實際開發中,通常一個系統會准備:dev開發環境、test測試環境、prod生產環境;
如何保證指定環境啟動時服務能正確讀取到Nacos上相應環境的配置文件呢?
問題2:
一個大型分布式微服務系統會有很多微服務子項目,每個微服務項目又都會有相應的開發環境、測試環境、預發環境、正式環境...
那么怎么對這些微服務配置進行管理呢?
b. Nacos的圖形化管理界面
1). 配置管理
2). 命名空間
c. Namespace + Group + Data ID 三者關系?為什么這么設計?
1.是什么
類似Java里面的package名和類名,最外層的namespace是可以用於區分部署環境的,Group和DataID邏輯上區分兩個目標對象。
2.三者情況
默認情況:Namespace=public,Group=DEFAULT_GROUP,默認Cluster是DEFAULT
Nacos默認的命名空間是public, Namespace主要用來實現隔離。
比方說我們現在有三個環境:開發、測試、生產環境,我們就可以創建三個Namespace, 不同的Namespace之間是隔離的。
Group默認是DEFAULT_GROUP, Group可以把不同的微服務划分到同一個分組里面去
Service就是微服務; 一個Service可以包含多個Cluster(集群),Nacos默認Cluster是DEFAULT, Cluster是對指定微服務的一個虛擬划分。
比方說為了容災,將Service微服務分別部署在了杭州機房和廣州機房,這時就可以給杭州機房的Service微服務起一個集群名稱(HZ),給廣州機房的Service微服務起一個集群名稱 (GZ) ,還可以盡量讓同一個機房的微服務互相調用,以提升性能。
最后是Instance,就是微服務的實例。
d. 三種方案加載配置
1). DataID方案
指定spring.profiles.active和配置文件的DataID來使不同環境下讀取不同的配置
1.1). 默認空間+默認分組+新建dev
和test
兩個DataID
1.1.1). 新建dev配置DataID
1.1.2). 新建test配置DataID
1.2). 通過spring.profiles.acvice屬性就能進行多環境下配置文件的讀取
1.3). 測試
訪問http://localhost:3377/config/info,配置什么就加載什么
2). Group方案
通過Group實現環境區分
2.1). 新建Group
新建配置
2.2). 在nacos圖形界面控制台上面新增配置文件DataID
2.3). bootstrap.yml+application.yml
在config下增加一條group的配置即可,可配置為DEV_GROUP或TEST_GROUP
訪問http://localhost:3377/config/info
3). Namespace方案
3.1). 新建dev、test的Namespace
注意下面的命名空間ID
3.2). 回到服務管理-服務列表查看
3.3). 按照域名配置填寫
3.4). YML
訪問http://localhost:3377/config/info
2.5.Nacos集群和持久化配置(重要)
2.5.1.官網說明
https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html
官網架構圖:
真實情況:
部署環境說明
https://nacos.io/zh-cn/docs/deployment.html
默認Nacos使用嵌入式數據庫實現數據的存儲。所以,如果啟動多個默認配置下的Nacos節點,數據存儲是存在一致性問題的。
為了解決這個問題,Nacos采用了集中式存儲的方式來支持集群化部署,目前只支持MySQL的存儲。
重點說明
- 1.安裝數據庫,版本要求:5.6.5+
- 2.初始化mysql數據庫,數據庫初始化文件:nacos-mysql.sql
- 3.修改conf/application.properties文件,增加支持mysql數據源配置(目前只支持mysql),添加mysql數據源的url、用戶名和密碼。
2.5.2.Nacos持久化配置
Nacos默認自帶的是嵌入式數據庫derby
a. 切換derby到mysql配置步驟
1). nacos\config目錄下找到sql腳本
執行腳本nacos-mysql.sql
CREATE DATABASE `nacos_config`;
USE `nacos_config`;
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = config_info */
/******************************************/
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租戶字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '內容',
`gmt_modified` datetime NOT NULL COMMENT '修改時間',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租戶字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租戶字段';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租戶字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整個集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配額,0表示使用默認值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '單個配置大小上限,單位為字節,0表示使用默認值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大個數,,0表示使用默認值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '單個聚合數據的子配置大小上限,單位為字節,0表示使用默認值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大變更歷史數量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租戶字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租戶改造';
/******************************************/
/* 數據庫全名 = nacos_config */
/* 表名稱 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配額,0表示使用默認值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '單個配置大小上限,單位為字節,0表示使用默認值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大個數',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '單個聚合數據的子配置大小上限,單位為字節,0表示使用默認值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大變更歷史數量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租戶容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '創建時間',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
2). nacos\config目錄下找到application.properties
增加支持mysql數據源配置(目前只支持mysql),添加mysql數據源的url、用戶名和密碼。
注:由於使用了MySQL最新版驅動,在項目代碼-數據庫連接 URL 后,加上(注意大小寫必須一致)&serverTimezone=UTC
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
db.user=root
db.password=root
b. 啟動Nacos,可以看到全新的空記錄界面
以前記錄是存進derby,現在是持久化到MySQL數據庫
2.5.3.Linux版Nacos+MySQL生產環境配置
需要1台Nginx + 3個nacos注冊中心 + 1個MySQL
a. Nacos下載Linux版
1). 下載nacos-server-2.0.3.tar.gz
wget https://github.com/alibaba/nacos/releases/tag/2.0.3
2). 解壓安裝
tar -zxvf nacos-server-2.0.3.tar.gz
mv nacos /opt/mynacos/
b. 集群配置步驟(重點)
1). Linux服務器上mysql數據庫配置
nacos/config目錄下找到nacos-mysql.sql腳本,在Linux服務器MySQL數據庫執行
2). application.properties配置
添加mysql數據源的url、用戶名和密碼。
注:由於使用了MySQL最新版驅動,在項目代碼-數據庫連接 URL 后,加上(注意大小寫必須一致)&serverTimezone=UTC
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
db.user=root
db.password=root
3). Linux服務器上nacos的集群配置cluster.conf
梳理出3台nacos集群的不同服務端口號,復制出cluster.conf
添加3台nacos機器服務端口號
注:IP不能寫127.0.0.1,必須是Linux命令hostname -i能夠識別的IP
4). 編輯Nacos的啟動腳本startup.sh,使它能夠接收不同的啟動端口
nacos/bin目錄下有startup.sh,平時單機版的啟動,都是./startup.sh即可。但是,集群啟動,我們希望可以類似其它軟件的shell命令,傳遞不同的端口號啟動不同的nacos實例。
命令:./startup.sh -p 3333 表示啟動端口號3333的nacos服務器實例,和上一步的cluster.conf配置的一致。
修改nacos/bin/startup.sh腳本:
執行方式:
5). Nginx的配置,由它作為負載均衡器
/usr/local/nginx/conf目錄下,修改nginx.conf配置文件
按照指定配置文件啟動nginx:
[jessy@localhost sbin]$ pwd
/usr/local/nginx/sbin
[jessy@localhost sbin]$ ./nginx -c /usr/local/nginx/conf/nginx.conf
6). 截止到此處,1個Nginx+3個nacos注冊中心+1個mysql
啟動3台nacos:
6.1). 測試通過nginx訪問nacos:
http://192.168.200.128:1111/nacos/#/login
6.2). 新建配置測試
linux服務器的mysql插入了新增的配置記錄,測試通過
c. 測試
微服務cloudalibaba-provider-payment9002啟動注冊進nacos集群
1). 修改yml
配置Nacos地址,換成nginx的1111端口
2). 結果
nacos-payment-provider微服務成功注冊進了Nacos集群
d. 高可用小總結
3.SpringCloud Alibaba Sentinel實現熔斷與限流
3.1.Sentinel
1.官網
https://github.com/alibaba/Sentinel
中文:https://github.com/alibaba/Sentinel/wiki/介紹
Hystrix:
1.需要我們程序員自己手工搭建監控平台
2.沒有一套web界面可以給我們進行更加細粒度化的配置流控、速率控制、服務熔斷、服務降級等
Sentinel:
1.單獨一個組件,可以獨立出來
2.直接界面化的細粒度統一配置
約定>配置>編碼
都可以寫在代碼里面,但是本次還是大規模的學習使用配置和注解的方式,盡量少寫代碼
2.是什么
就是之前的Hystrix
3.去哪下
https://github.com/alibaba/Sentinel/releases
4.能干嘛
5.怎么玩
https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
服務使用中的各種問題:
- 服務雪崩
- 服務降級
- 服務熔斷
- 服務限流
3.2.安裝Sentinel控制台
1.sentinel組件由兩部分構成
Sentinel分為兩個不部分(后台和前台8080):
- 核心庫(Java客戶端)不依賴任何框架/庫,能夠運行於所有Java運行時環境,同時對Dubbo/Spring Cloud等框架也有較好的支持。
- 控制台(Dashboard)基於Spring Boot開發,打包后可以直接運行,不需要額外的Tomcat等應用容器。
2.安裝步驟
2.1.下載
https://github.com/alibaba/Sentinel/releases,下載到本地sentinel-dashboard-1.8.2.jar
2.2.運行命令
前提:Java8運行環境正常、8080端口不能被占用
命令:java -jar sentinel-dashboard-1.8.2.jar
2.3.訪問sentinel管理界面
http://locahost:8080,登錄賬戶密碼均為sentinel
3.3.初始化演示工程
1.啟動Nacos8848成功
http://localhost:8848/nacos/#/login
2.Module
2.1.cloudalibaba-sentinel-service8401
2.2.POM
添加nacos、sentinel依賴:
<!-- nacos-discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel-datasource-nacos后續做持久化使用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2.3.YML
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos服務注冊中心地址
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默認8719端口,假如被占用會自動從8719開始依次+1掃描,直到找到未被占用的端口
port: 8718
management:
endpoints:
web:
exposure:
include: '*'
2.4.主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
2.5.業務類FlowLimitController
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "--------testA";
}
@GetMapping("/testB")
public String testB() {
return "--------testB";
}
}
3.啟動Sentinel8080
java -jar sentinel-dashboard-1.8.2.jar
4.啟動微服務8401
5.啟動8401微服務后查看sentinel控制台
Sentinel采用的懶加載機制
執行一次訪問即可
http://localhost:8401/testA和http:/localhost:8401/testB
效果:
結論:
sentinel8080正在監控微服務8401
3.4.流控規則⭐
3.4.1.基本介紹
- 資源名:唯一名稱,默認請求路徑
- 針對來源:Sentinel可以針對調用者進行限流,填寫微服務名,默認default(不區分來源)
- 閾值類型/單機閾值:
- QPS(每秒鍾的請求數量):當調用該api的QPS達到閾值的時候,進行限流
- 線程數:當調用該api的線程數達到閾值的時候,進行限流
- 是否集群:不需要集群
- 流控模式:
- 直接:api達到限流條件時,直接限流
- 關聯:當關聯的資源達到閾值時,就限流自己
- 鏈路:只記錄指定鏈路上的流量(指定資源從入口資源進來的流量,如果達到閾值,就進行限流)【api級別的針對來源】
- 流控效果:
- 快速失敗:直接失敗,拋異常
- Warm Up:根據codeFactor(冷加載因子,默認3)的值,從閾值/codeFactor,經過預熱時長,才達到設置的QPS閾值
- 排隊等待:勻速排隊,讓請求以勻速的速度通過,閾值類型必須設置為QPS,否則無效
3.4.2.流控模式
1). 直接(默認)
1.1). 直接->快速失敗(系統默認)
表示1秒鍾內查詢1次就是OK,如果超過次數1,就直接-快速失敗,報默認錯誤
1.2). 測試
快速點擊訪問http://localhost:8401/testA,結果出現Blocked by Sentinel (flow limiting)
思考:
a. 直接調用默認報錯信息,技術方面OK,但是是否應該由我們自己的后續處理?類似有個fallback的兜底方法
2). 關聯
2.1). 是什么
當與A關聯的資源B達到閾值時,就限流A自己,即:B惹事,A掛了
2.2). 配置A
設置效果:
當關聯資源/testB的QPS閾值超過1時,就限流/testA的REST訪問地址,當關聯資源達到閾值后限制配置好的資源名
2.3). postman模擬並發密集訪問testB
先訪問testB成功
postman里新建多線程集合組,將訪問地址添加進新線程組,並運行
大批量線程高並發訪問B,導致A失效了
2.4). 運行后發現testA掛了
點擊訪問http://localhost:8401/testA
結果:Blocked by Sentinel (flow limiting)
3). 鏈路
多個請求調用了同一個微服務
3.4.3.流控效果
1). 直接->快速失敗(默認的流控處理)
- 直接失敗,拋出異常:
Blocked by Sentinel (flow limiting)
- 源碼:
com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
2). 預熱
2.1). 說明
公式:閾值除以coldFactor(默認值為3),經過預熱時長后才會達到閾值
2.2). 官網
默認coldFactor為3,即請求 QPS 從 threshold/3 開始,經過預熱時長逐漸升至設定的 QPS 閾值。
限流 冷啟動:https://github.com/alibaba/Sentinel/wiki/Flow-Control
2.3). 源碼
com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
2.4). WarmUp配置
默認coldFactor為3,即請求 QPS 從 (threshold/3) 開始,經過預熱時長逐漸升至設定的 QPS 閾值。
案例,閾值為10+預熱時長設置5秒。
系統初始化的閾值為10/3約等於3,即閾值剛開始為3;然后過了5秒后閾值才慢慢升高恢復到10
a. 多次點擊http://localhost:8401/testB
剛開始不行,后續慢慢OK
b. 應用場景
如:秒殺系統在開啟的瞬間,會有很多流量上來,很有可能把系統打死,預熱方式就是為了保護系統,慢慢的把流量放進來,慢慢的把閾值增長到設置的閾值。
3). 排隊等待
大家一起去大學食堂排隊打飯,大家都是排成一條直線勻速通過
勻速排隊,讓請求以均勻的速度通過,閾值類型必須設成QPS,否則無效。
設置含義:/testA每秒1次請求,超過的話就排隊等待,等待的超時時間為20000毫秒。
3.1). 勻速排隊,閾值必須設置為QPS
3.2). 官網
3.3). 源碼
com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
3.4). 測試
QPS達到閾值,請求排隊等待通過
3.5.降級規則⭐
3.5.1.官網
https://github.com/alibaba/Sentinel/wiki/熔斷降級
3.5.2.基本介紹
慢調用比例(秒級)
- 統計時長內,實際請求數目大於最小請求數目,慢調用比例 > 比例閾值時,觸發降級
- 慢調用:當調用的時間(響應的實際時間)> 設置的RT時,這個調用叫做慢調用
- 慢調用比例:在所有調用中,慢調用占有實際的比例 = 慢調用次數 / 調用次數
- 比例閾值:自己設定的,慢調用次數 / 調用次數 = 比例閾值
異常比例(秒級)
- QPS>=最小請求數 且異常比例(秒級統計)超過閾值時,觸發降級;時間窗口期結束后,關閉降級
異常數(分鍾級)
- 異常數(分鍾統計)超過閾值時,觸發降級;時間窗口結束后,關閉降級
Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。
進入熔斷狀態判斷依據:
①當統計時長內,實際請求數目大於最小請求數目,慢調用比例 > 比例閾值 ,進入熔斷狀態
②熔斷狀態:在接下來的熔斷時長內請求會自動被熔斷
③探測恢復狀態:熔斷時長結束后進入探測恢復狀態
④結束熔斷:在探測恢復狀態,如果接下來的一個請求響應時間小於設置的慢調用 RT,則結束熔斷;否則繼續熔斷。
當資源被降級后,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出 DegradeException)。
Sentinel的斷路器是沒有半開狀態的
- 半開的狀態系統自動去檢測是否請求有異常,沒有異常就關閉斷路器恢復使用,有異常則繼續打開斷路器不可用。
- 復習Hystrix
3.5.3.降級策略實戰
a. 慢調用比例
1). 是什么
慢調用比例 (SLOW_REQUEST_RATIO):選擇以慢調用比例作為閾值,需要設置允許的慢調用 RT(即最大的響應時間),請求的響應時間大於該值則統計為慢調用。當單位統計時長(statIntervalMs)內請求數目大於設置的最小請求數目,並且慢調用的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求響應時間小於設置的慢調用 RT 則結束熔斷,若大於設置的慢調用 RT 則會再次被熔斷。
2). 測試
2.1). 代碼
@GetMapping("/testD")
public String testD() throws InterruptedException {
// 暫停幾秒鍾
TimeUnit.SECONDS.sleep(1);
log.info("testD 測試慢調用比例");
return "------testD";
}
2.2). 配置
2.3). jmeter壓測
①設置線程組
每秒10個請求:
②設置測試地址
③測試
Ⅰ.運行jmeter線程組
Ⅱ.控制台顯示
Ⅲ.瀏覽器測試熔斷
關閉jmeter線程壓測,再進行瀏覽器訪問
2.4). 結論
Ⅰ.選擇以慢調用比例作為閾值,需要設置允許的慢調用 RT(即最大的響應時間),請求的響應時間大於該值則統計為慢調用。當單位統計時長(statIntervalMs)內請求數目大於設置的最小請求數目,並且慢調用的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。
Ⅱ.經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求響應時間小於設置的慢調用 RT 則結束熔斷,若大於設置的慢調用 RT 則會再次被熔斷。
b. 異常比例
1). 是什么
異常比例 (ERROR_RATIO):當單位統計時長(statIntervalMs)內請求數目大於設置的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的閾值范圍是 [0.0, 1.0],代表 0% - 100%。
2). 測試
2.1). 代碼
@GetMapping("/testD")
public String testD() throws InterruptedException {
log.info("testD 異常比例");
int age = 10 / 0;
return "------testD";
}
2.2). 配置
2.3). jmeter壓測
Ⅰ.運行jmeter線程組
Ⅱ.瀏覽器測試熔斷
關閉jmeter線程壓測,再進行瀏覽器訪問
2.4). 結論
異常比例 (ERROR_RATIO):當單位統計時長(statIntervalMs)內請求數目大於設置的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的閾值范圍是 [0.0, 1.0],代表 0% - 100%。
按照上述配置,單獨訪問一次,必然調一次報錯一次(int age = 10 / 0)。
開啟jmeter后,直接高並發發送請求,多次調用達到我們的配置條件,斷路器開啟(保險絲跳閘),微服務不可用,不再報錯error而是服務降級了。
c. 異常數
1). 是什么
異常數 (ERROR_COUNT):當單位統計時長內的異常數目超過閾值之后會自動進行熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。
2). 測試
2.1). 代碼
@GetMapping("/testE")
public String testE() {
log.info("testE 測試異常數");
int age = 10 / 0;
return "------testE";
}
2.2). 配置
2.3). 瀏覽器測試熔斷
第一次訪問http://localhost:8401/testE,報錯error
單位時間秒內連續訪問2次,進入熔斷后降級
3.6.熱點key限流
3.6.1.基本介紹
3.6.2.官網
https://github.com/alibaba/Sentinel/wiki/熱點參數限流
兜底方法
分為系統默認和客戶自定義兩種
之前的case,限流出現問題后,都是用sentinel系統默認的提示:Blocked by Sentinel(flow limiting)
我們能不能自定義?類似hystrix,某個方法出問題了,就找對應的兜底降級方法?
結論
從HystrixCommand到@SentinelResource
3.6.3.代碼
com.alibaba.csp.sentinel.slots.block.BlockException
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
return "------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException exception) {
return "------deal_testHotKey,/(ㄒoㄒ)/~~"; //sentinel系統默認的提示:Blocked by Sentinel(flow limiting)
}
3.6.4.配置
1). @SentinelResource(value = "testHotKey")
異常達到了前台用戶界面看到不友好
2). @SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
方法testHotKey里面第一個參數只要QPS超過每秒1次,馬上降級處理,使用了我們自定義的降級方法
3.6.5.測試
❌error
http://localhost:8401/testHotKey?p1=abc
http://localhost:8401/testHotKey?p1=abc&p2=33
✔right
http://localhost:8401/testHotKey?p2=abc
3.6.6.參數例外項
上述案例演示了第一個參數p1,當QPS超過1秒1次點擊后馬上被限流
a. 特殊情況
普通:超過1秒鍾一個后了,達到閾值1后馬上被限流
我們期望p1參數當它是某個特殊值時,它的限流值和平時不一樣
特例:假如當p1的值等於5時,它的閾值可以達到200
b. 配置
c. 測試
✔ http://localhost:8401/testHotKey?p1=5
❌ http://localhost:8401/testHotKey?p1=3
當p1等於5的時候,閾值變為200
當p1不等於5的時候,閾值就是平常的1
d. 前提條件
熱點參數的注意點:參數必須是基本類型或者String
3.6.7.其它
手賤添加異常:int age = 10/0;
@SentinelResource
處理的是Sentinel控制台配置的違規情況,有blockHandler方法配置的兜底處理;
RuntimeException
int age = 10/0,這個是Java運行時異常RuntimeException,@SentinelResource不管
總結
@SentinelResouce主管配置出錯,運行出錯該走異常走異常
3.7.系統規則
3.7.1.是什么
https://github.com/alibaba/Sentinel/wiki/系統自適應限流
3.7.2.各項配置參數說明
3.7.3.配置全局QPS
3.8.@SentinelResource
3.8.1.按資源名稱限流+后續處理
啟動Nacos,啟動Sentinel
a. 修改微服務8401
1). pom.xml引入自定義的api通用包
<!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.neo.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
2). 增加業務類RateLimitController
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按資源名稱限流測試OK", new Payment(2021L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "服務不可用");
}
}
b. 配置流控規則
1). 配置步驟
2). 圖形配置和代碼關系
表示1秒鍾內查詢次數大於1,就跑到我們自定義的處理限流
c. 測試
c1. 1秒鍾點擊1下,OK
c2. 超過上述,瘋狂點擊,返回了自定義的限流處理信息,限流發生
{"code":444,"message":"com.alibaba.csp.sentinel.slots.block.flow.FlowException服務不可用","data":null}
d. 額外問題
此時關閉微服務8401,Sentinel控制台流控規則消失了
3.8.2.按照Url地址限流+后續處理
通過訪問的URL來限流,會返回Sentinel自帶默認的限流處理信息
a. 業務類RateLimitController
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200, "按url限流測試OK", new Payment(2021L, "serial002"));
}
b. Sentinel控制台配置
c. 測試
瘋狂點擊http://localhost:8401/rateLimit/byUrl
結果:會返回Sentinel自帶的限流處理結果
3.8.3.上面兜底方案面臨的問題
1.系統默認的,沒有體現我們自己的業務要求。
2.依照現有條件,我們自定義的處理方法又和業務代碼耦合在一起,不直觀。
3.每個業務方法都添加一個兜底的,那代碼膨脹加劇。
4.全局統一的處理方法沒有體現。
3.8.4.客戶自定義限流處理邏輯
創建CustomerBlockHandler類用於自定義限流處理邏輯
a. 自定義限流處理類
com.neo.springcloud.alibaba.myhandler.CustomerBlockHandler
public class CustomerBlockHandler {
public static CommonResult handleException(BlockException exception) {
return new CommonResult(4444, "按客戶自定義,global handlerException------1");
}
public static CommonResult handleException2(BlockException exception) {
return new CommonResult(4444, "按客戶自定義,global handlerException------2");
}
}
b. RateLimitController
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定義", new Payment(2021L, "serial003"));
}
c. Sentinel控制台配置
d. 說明
測試后自定義的出來了
3.8.5.更多注解屬性說明
https://github.com/alibaba/Sentinel/wiki/注解支持
所有的代碼都要用try-catch-finally方式進行處理
Sentinel主要有三個核心API
- SphU定義資源
- Tracer定義統計
- ContextUtil定義上下文
3.9.服務熔斷功能⭐
Sentinel整合ribbon+openFeign+fallback
3.9.1.Ribbon系列
a. 啟動nacos和sentinel
b. 提供者9003/9004
b1. 新建cloudalibaba-provider-payment9003/9004
b2. POM
<!-- SpringCloud Aibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.neo.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 日常通用jar包配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
b3. YML
兩個微服務端口分別為9003、9004
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置nacos地址
management:
endpoints:
web:
exposure:
include: '*'
b4. 主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
b5. 業務類
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = Maps.newHashMap();
static {
hashMap.put(1L, new Payment(1L, "3lzwvpo7hu2ty5ab0idr4fmexgsck1"));
hashMap.put(2L, new Payment(2L, "ptc6h3lfz284b0g7j9yrdowsaik5ue"));
hashMap.put(3L, new Payment(3L, "7g1au46qbjzkw2shodptfnlve58309"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
return new CommonResult(200, "from mysql, serverPort: " + serverPort, payment);
}
}
b6. 測試
訪問地址http://localhost:9003/paymentSQL/1
c. 消費者84
c1. 新建cloudalibaba-consumer-nacos-order84
c2. POM
<!-- SpringCloud Aibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.neo.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 日常通用jar包配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
c3. YML
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置nacos地址
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默認8719端口,假如被占用會自動從8719開始依次+1掃描,直到找到未被占用的端口
port: 8718
#消費者將要去訪問的微服務名稱(注冊成功進nacos的微服務提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
c4. 主啟動
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
c5. 業務類
1). ApplicationContextConfig
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
2). CircleBreakerController
@RestController
@Slf4j
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String serverURL;
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException, 非法參數異常...");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException, 該ID沒有對應記錄, 空指針異常");
}
return result;
}
}
2.1). 目的
Ⅰ.fallback處理運行異常
Ⅱ.blockHandler處理Sentinel配置違規
2.2). 測試
訪問http://localhost:84/consumer/fallback/1,結果微服務84帶輪詢負載均衡算法,成功訪問了9003、9004。
由於@SentinelResource(value = "fallback")沒有任何配置,訪問http://localhost:84/consumer/fallback/4
或http://localhost:84/consumer/fallback/5
,返回客戶error頁面,很不友好
2.3). 配置fallback屬性
@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback處理運行異常
@RestController
@Slf4j
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String serverURL;
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback處理運行異常
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException, 非法參數異常...");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException, 該ID沒有對應記錄, 空指針異常");
}
return result;
}
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult(444, "兜底異常handlerFallback, exception內容:" + e.getMessage(), payment);
}
}
運行結果:
2.4). 配置blockHandler屬性
@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只負責Sentinel配置違規
@RestController
@Slf4j
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String serverURL;
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
//@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback處理運行異常
@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只負責Sentinel配置違規
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException, 非法參數異常...");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException, 該ID沒有對應記錄, 空指針異常");
}
return result;
}
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult(444, "兜底異常handlerFallback, exception內容:" + e.getMessage(), payment);
}
public CommonResult blockHandler(@PathVariable Long id, BlockException e) {
Payment payment = new Payment(id, "null");
return new CommonResult(444, "blockHandler-Sentinel限流,無此流水,blockException:" + e.getMessage(), payment);
}
}
Sentinel配置fallback接口熔斷規則:
運行結果:
2.5). 同時配置fallback和blockHandler屬性
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
Sentinel配置fallback接口流控規則:
測試1:每秒請求QPS>1訪問http://localhost:84/consumer/fallback/1
,blockHandler生效
測試2:正常訪問http://localhost:84/consumer/fallback/4
,fallback生效
測試3:每秒請求QPS>1訪問http://localhost:84/consumer/fallback/4
,blockHandler有效
結論:若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出 BlockException 時只會進入 blockHandler 處理邏輯。
2.6). exceptionsToIgnore異常忽略屬性
exceptionsToIgnore = {IllegalArgumentException.class},忽略IllegalArgumentException異常,不走fallback方法
@RequestMapping("/consumer/fallback/{id}")
//@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback處理運行異常
//@SentinelResource(value = "fallback", blockHandler = "blockHandler") //blockHandler只負責Sentinel配置違規
@SentinelResource(value = "fallback",
fallback = "handlerFallback",
blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException, 非法參數異常...");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException, 該ID沒有對應記錄, 空指針異常");
}
return result;
}
3.9.2.Feign系列
修改84模塊
- 84消費者調用提供者9003
- Feign組件一般是消費側
a. POM
增加openfeign依賴:
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
注釋掉熱部署spring-boot-devtools依賴,啟動時openfeign配置類FeignAutoConfiguration$HystrixFeignTargeterConfiguration.class
可能因為熱部署會報錯
b. YML
激活Sentinel對Feign的支持
#激活Sentinel對Feign的支持
feign:
sentinel:
enabled: true
c. 主啟動
啟動類上增加@EnableFeignClients
,啟用feign
d. 業務類
1). feign實現REST遠程調用
feign:接口+注解
增加PaymenService接口:
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
為了避免與業務邏輯耦合,增加全局統一降級回調方法類:
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(4444, "服務降級返回,PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
2). Controller
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
e. 測試
請求地址http://localhost:84/consumer/paymentSQL/2
,可以正常訪問
假設:故意關閉9003、9004微服務提供者
測試:測試84調用9003、9004,84消費側是否自動降級
結論:84消費側會自動降級,不會被耗死
3.9.3.熔斷框架比較
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔離策略 | 信號量隔離(並發線程數限流) | 線程池隔離/信號量隔離 | 信號量隔離 |
熔斷降級策略 | 基於時間響應、異常比率、異常數 | 基於異常比率 | 基於異常比率、響應時間 |
實時統計實現 | 滑動窗口(LeapArray) | 滑動窗口(基於RxJava) | Ring Bit Buffer |
動態規則配置 | 支持多種數據源 | 支持多種數據源 | 有限支持 |
擴展性 | 多個擴展點 | 插件形式 | 接口形式 |
基於注解的支持 | 支持 | 支持 | 支持 |
限流 | 基於QPS、支持基於調用關系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持預熱模式、勻速器模式、預熱排隊模式 | 不支持 | 簡單的Rate Limiter模式 |
系統自適應保護 | 支持 | 不支持 | 不支持 |
控制台 | 提供開箱即用的控制台、可配置規則、查看秒級監控、機器發現等 | 簡單的監控查看 | 不提供控制台,可對接其它監控系統 |
3.10.規則持久化
一旦我們重啟應用,sentinel規則將消失,生產環境需要將配置規則進行持久化
a. 方案
- 將限流配置規則持久化進Nacos保存,只要刷新8401某個REST地址,sentinel控制台的流控規則就能看到,只要Nacos里面的配置不刪除,針對8401上sentinel的流控規則持續有效
b. 步驟
修改cloudalibaba-sentinel-service8401
1). POM
<!-- sentinel-datasource-nacos做持久化使用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2). YML
添加Nacos數據源配置
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
3). 添加Nacos業務規則配置
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- resource: 資源名稱
- limitApp: 來源應用
- grade: 閾值類型,0表示線程數,1表示QPS
- count: 單機閾值
- strategy: 流控模式,0表示直接,1表示關聯,2表示鏈路
- controlBehavior: 流控效果,0表示快速失敗,1表示Warm Up,2表示排隊等待
啟動8401,刷新sentinel,發現業務規則有了
4). 測試
快速訪問接口http://localhost:8401/rateLimit/byUrl
停止8401再看sentinel
重新啟動8401再看sentinel
- 乍一看還是沒有流控規則
- 調用http://localhost:8401/rateLimit/byUrl后刷新sentinel,流控規則出現了,持久化驗證通過
4.SpringCloud Alibaba Seata處理分布式事務
4.1.分布式事務問題
a. 分布式前
- 單機單庫沒有這個問題
- 從1: 1 -> 1: N -> N: N
b. 分布式后
用例
用戶購買商品的業務邏輯。整個業務邏輯由3個微服務提供支持:
倉儲服務:對給定的商品扣除倉儲數量。
訂單服務:根據采購需求創建訂單。
帳戶服務:從用戶帳戶中扣除余額。
架構圖
單體應用被拆分成微服務應用,原來的三個模塊被拆分成三個獨立的應用,分別使用三個獨立的數據源
業務操作需要調用三個服務來完成。此時每個服務內部的數據一致性由本地事務來保證,但是全局的數據一致性問題沒法保證。
c. 結論
一次業務操作需要跨多個數據源或需要跨多個系統進行遠程調用,就會產生分布式事務問題。
4.2.Seata簡介
Seata 是一款開源的分布式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分布式事務服務。
一個典型的分布式事務過程
- 分布式事務處理過程的一ID + 三組件模型
- XID(Transaction ID):全局唯一的事務ID
- 3組件概念
- TC (Transaction Coordinator) - 事務協調者:維護全局事務和分支事務的狀態,驅動全局提交或回滾。
- TM (Transaction Manager) - 事務管理器:定義全局事務的范圍:開始全局事務,提交或回滾全局事務。
- RM (Resource Manager) - 資源管理器:管理正在處理的分支事務的資源,與TC對話以注冊分支事務並報告分支事務的狀態,並驅動分支事務的提交或回滾。
- 處理過程
- 1.TM要求TC開始一項新的全局事務。TC生成代表全局事務的XID。
- 2.XID通過微服務的調用鏈傳播。
- 3.RM將本地事務注冊為XID到TC的相應全局事務的分支。
- 4.TM要求TC提交或回退相應的XID全局事務。
- 5.TC驅動XID的相應全局事務下的所有分支事務以完成分支提交或回滾。
發布說明:https://github.com/seata/seata/releases
怎么使用
- 本地@Transactional
- 全局@GlobalTransactional
- Seata的分布式交易解決方案
- 我們只需要使用一個
@GlobalTransactional
注解在業務方法上
4.3.Seata-Server安裝
4.3.1.官網地址
http://seata.io/zh-cn/
4.3.2.下載版本
https://github.com/seata/seata/releases
4.3.3.seata-server-1.4.2.zip解壓到指定目錄並修改conf目錄下的file.conf配置文件
先備份原始file.conf、registry.conf文件
主要修改:事務日志存儲模式為db + 數據庫連接信息
a. 修改seata解壓目錄中的conf目錄下的file.conf文件
store模塊:
b. 修改seata解壓目錄中的conf目錄下的registry.conf文件
registry和config模塊:
4.3.4.在本地新建一個seata數據庫,導入如下數據庫腳本
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
或者去官網拷貝數據庫腳本,網址:
https://github.com/seata/seata/blob/1.4.2/script/server/db/mysql.sql
4.3.5.去官網復制config.txt
配置和nacos-config.sh
配置
config.txt
網址:https://github.com/seata/seata/blob/1.4.2/script/config-center/config.txt
nacos-config.sh
網址:https://github.com/seata/seata/blob/1.4.2/script/config-center/nacos/nacos-config.sh
將config.txt
配置放在安裝seata的目錄下,與bin目錄同級
將nacos-config.sh
復制下來的文件放在安裝seata目錄下的conf
目錄
4.3.6.修改config.txt
中的部分配置
自定義事務組名稱:
service.vgroupMapping.my_test_tx_group=fsp_tx_group
4.3.7.啟動seata
- 先啟動Nacos
- 再啟動bin目錄下的seata-server.bat 即可啟動seata
4.3.8.運行nacos-config.sh
把seata/config.txt
文件內容配置到nacos
sh nacos-config.sh -h 127.0.0.1
啟動seata-server
服務,打開nacos控制台-服務列表中,能看到seata-server
就算搭建完成
4.4.訂單/庫存/賬戶業務數據庫准備
先啟動Nacos后啟動Seata,否則Seata沒啟動報錯:no available server to connect
4.4.1.分布式事務業務說明
- 業務說明
-
創建三個微服務:訂單服務、庫存服務、賬戶服務。
-
當用戶下單時,會在訂單服務中創建一個訂單,然后通過遠程調用庫存服務來扣減下單商品的庫存;
-
再通過遠程調用賬戶服務來扣減用戶賬戶里面的余額;
-
最后在訂單服務中修改訂單狀態為已完成。
-
該操作跨越三個數據庫,有兩次遠程調用,很明顯會有分布式事務問題。
-
- 下訂單--->扣庫存--->減賬戶(余額)
4.4.2.創建業務數據庫
- seata_order:存儲訂單的數據庫;
- seata_storage:存儲庫存的數據庫;
- seata_account:存儲賬戶信息的數據庫。
- 建庫SQL
create database seata_order;
create database seata_storage;
create database seata_account;
4.4.3.按照上述2庫分別建對應業務表
a. seata_order庫下建t_order表
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` varchar(32) NOT NULL PRIMARY KEY,
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11, 2) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '訂單狀態:0-創建中,1-已完結'
) ENGINE = innodb AUTO_INCREMENT = 7 CHARSET = utf8;
b. seata_storage庫下建t_storage表
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(1) DEFAULT NULL COMMENT '剩余庫存'
) ENGINE = innodb AUTO_INCREMENT = 2 CHARSET = utf8;
INSERT INTO seata_storage.t_storage (`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0', '100');
c. seata_account庫下建t_account表
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(11, 2) DEFAULT NULL COMMENT '總額度',
`used` decimal(11, 2) DEFAULT NULL COMMENT '已用金額',
`residue` decimal(11, 2) DEFAULT '0' COMMENT '剩余可用額度'
) ENGINE = innodb AUTO_INCREMENT = 2 CHARSET = utf8;
INSERT INTO seata_account.t_account (`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');
4.4.4.按照上述3庫分別建對應的回滾日志表
訂單-庫存-賬戶3個庫下都需要建各自的回滾日志表
db_undo_log.sql:
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARSET = utf8;
4.4.5.最終效果
4.4.訂單/庫存/賬戶業務微服務准備
業務需求:下訂單->減庫存->扣余額->改(訂單)狀態
4.4.1.新建訂單Order-Module
a. 新建seata-order-service2001
b. POM
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.2</version>
</dependency>
<!-- feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- web + actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 日常通用jar包配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
c. YML
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定義事務組名稱需要與seata-server的registry.conf中的vgroupMapping一致
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
d. file.conf
復制安裝seata目錄中的conf目錄下的file.conf文件,到項目類路徑下
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
## redis mode: single、sentinel
mode = "single"
## single mode property
single {
host = "127.0.0.1"
port = "6379"
}
## sentinel mode property
sentinel {
masterName = ""
## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
sentinelHosts = ""
}
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
e. registry.conf
復制安裝seata目錄中的conf目錄下的registry.conf文件,到項目類路徑下
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "public"
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "public"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
consul {
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
apollo {
appId = "seata-server"
## apolloConfigService will cover apolloMeta
apolloMeta = "http://192.168.1.204:8801"
apolloConfigService = "http://192.168.1.204:8080"
namespace = "application"
apolloAccesskeySecret = ""
cluster = "seata"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
nodePath = "/seata/seata.properties"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
f. domain
1). CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
2). Order
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private String id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
/*
* 訂單狀態:0-創建中,1-已完結
*
* */
private Integer status;
}
g. Dao接口及實現
1). OrderDao
@Mapper
public interface OrderDao {
//1.新建訂單
int create(Order order);
//2.修改訂單狀態,從0修改為1
int update(@Param("orderId") String orderId, @Param("status") Integer status);
}
2). resources文件夾下新建mapper文件夾
添加OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.neo.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.neo.springcloud.alibaba.domain.Order">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create" parameterType="com.neo.springcloud.alibaba.domain.Order">
insert into t_order(id, user_id, product_id, count, money, status)
values(#{id}, #{userId}, #{productId}, #{count}, #{money}, 0)
</insert>
<update id="update">
update t_order
set status = 1
where id = #{orderId} and status = #{status}
</update>
</mapper>
h. Service接口及實現
1). OrderService
public interface OrderService {
void create(Order order);
}
1.1). OrderServiceImpl
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
@Override
public void create(Order order) {
String bizSeq = UUID.randomUUID().toString().replace("-", "");
log.info("開始新建訂單,bizSeq:" + bizSeq);
//1.新建訂單
orderDao.create(order);
log.info("訂單微服務開始調用庫存,做扣減Count");
//2.扣減庫存
storageService.decrease(order.getProductId(), order.getCount());
log.info("訂單微服務開始調用庫存,做扣減end");
log.info("訂單微服務開始調用賬戶,做扣減Money");
//3.扣減庫存
accountService.decrease(order.getUserId(), order.getMoney());
log.info("訂單微服務開始調用賬戶,做扣減end");
//4.修改訂單狀態,從0到1,1代表已完成
log.info("修改訂單狀態開始");
orderDao.update(order.getId(), 0);
log.info("修改訂單狀態結束");
log.info("下訂單結束");
}
}
2). StorageService
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
3). AccountService
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}
i. Controller
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "訂單創建成功");
}
}
j. Config配置
1). MyBatisConfig
@Configuration
@MapperScan({"com.neo.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
2). DataSourceProxyConfig
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
k. 主啟動
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消數據源的自動創建
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
4.4.2.新建庫存Storage-Module
a. 新建seata-storage-service2002
b. POM
與seata-order-service2001一致
c. YML
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
#自定義事務組名稱需要與seata-server的registry.conf中的vgroupMapping一致
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
d. file.conf
與seata-order-service2001一致
e. registry.conf
與seata-order-service2001一致
f. domain
1). CommonResult
與seata-order-service2001一致
2). Storage
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
/**
* 產品id
*/
private Long productId;
/**
* 總庫存
*/
private Integer total;
/**
* 已用庫存
*/
private Integer used;
/**
* 剩余庫存
*/
private Integer residue;
}
g. Dao接口及實現
1). StorageDao
@Mapper
public interface StorageDao {
//扣減庫存
int decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
2). resources文件夾下新建mapper文件夾
添加StorageMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.neo.springcloud.alibaba.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.neo.springcloud.alibaba.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="INTEGER"/>
<result column="used" property="used" jdbcType="INTEGER"/>
<result column="residue" property="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease">
update t_storage
set residue = residue - #{count},
used = used + #{count}
where product_id = #{productId}
</update>
</mapper>
h. Service接口及實現
1). StorageService
public interface StorageService {
/**
* 扣減庫存
*/
void decrease(Long productId, Integer count);
}
1.1). StorageServiceImpl
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
@Override
public void decrease(Long productId, Integer count) {
log.info("庫存微服務扣減庫存開始");
storageDao.decrease(productId, count);
log.info("庫存微服務扣減庫存結束");
}
}
i. Controller
@RestController
public class StorageController {
@Resource
private StorageService storageService;
/**
* 扣減庫存
*
* @param productId
* @param count
* @return
*/
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200, "扣減庫存成功!");
}
}
j. Config配置
1). MyBatisConfig
與seata-order-service2001一致
2). DataSourceProxyConfig
與seata-order-service2001一致
k. 主啟動
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataStorageMainApp2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMainApp2002.class, args);
}
}
4.4.3.新建賬戶Account-Module
a. 新建seata-account-service2003
b. POM
與seata-order-service2001一致
c. YML
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
#自定義事務組名稱需要與seata-server的registry.conf中的vgroupMapping一致
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
d. file.conf
與seata-order-service2001一致
e. registry.conf
與seata-order-service2001一致
f. domain
1). CommonResult
與seata-order-service2001一致
2). Account
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/**
* 用戶id
*/
private Long userId;
/**
* 總額度
*/
private BigDecimal total;
/**
* 已用額度
*/
private BigDecimal used;
/**
* 剩余額度
*/
private BigDecimal residue;
}
g. Dao接口及實現
1). AccountDao
@Mapper
public interface AccountDao {
//扣減賬戶余額
int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
2). resources文件夾下新建mapper文件夾
添加AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.neo.springcloud.alibaba.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.neo.springcloud.alibaba.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
update t_account
set used = used + ${money},
residue = residue - ${money}
where user_id = ${userId}
</update>
</mapper>
h. Service接口及實現
1). AccountService
public interface AccountService {
/**
* 扣減賬戶余額
*/
void decrease(Long userId, BigDecimal money);
}
1.1). AccountServiceImpl
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("賬戶微服務扣減賬戶余額開始");
accountDao.decrease(userId, money);
log.info("賬戶微服務扣減賬戶余額結束");
}
}
i. Controller
@RestController
public class AccountController {
@Resource
private AccountService accountService;
/**
* 扣減賬戶余額
*
* @param userId
* @param money
* @return
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(Long userId, BigDecimal money) {
accountService.decrease(userId, money);
return new CommonResult(200, "扣減賬戶余額成功!");
}
}
j. Config配置
1). MyBatisConfig
與seata-order-service2001一致
2). DataSourceProxyConfig
與seata-order-service2001一致
k. 主啟動
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
4.5.測試
下訂單->減庫存->扣余額->改(訂單)狀態
4.5.1.數據庫初始情況
4.5.2.正常下單
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
數據庫情況:
4.5.3.模擬超時異常
AccountServiceImpl添加超時:
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("賬戶微服務扣減賬戶余額開始");
//模擬超時異常,全局事務回滾
//暫停幾秒鍾線程
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
log.info("賬戶微服務扣減賬戶余額結束");
}
訪問http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
數據庫情況:
Ⅰ.訂單狀態未完成
Ⅱ.庫存被扣減了
Ⅲ.賬戶被扣減了
故障情況:
當庫存和賬戶金額扣減后,訂單狀態並沒有設置為已經完成,沒有從0改為1,而且由於feign的重試機制,賬戶余額還有可能被多次扣減
4.5.3.處理超時異常,全局分布式事務回滾(@GlobalTransactional)
OrderServiceImpl添加@GlobalTransactional:
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
String bizSeq = UUID.randomUUID().toString().replace("-", "");
log.info("開始新建訂單,bizSeq:" + bizSeq);
order.setId(bizSeq);
//1.新建訂單
orderDao.create(order);
log.info("訂單微服務開始調用庫存,做扣減Count");
//2.扣減庫存
storageService.decrease(order.getProductId(), order.getCount());
log.info("訂單微服務開始調用庫存,做扣減end");
log.info("訂單微服務開始調用賬戶,做扣減Money");
//3.扣減庫存
accountService.decrease(order.getUserId(), order.getMoney());
log.info("訂單微服務開始調用賬戶,做扣減end");
//4.修改訂單狀態,從0到1,1代表已完成
log.info("修改訂單狀態開始");
orderDao.update(order.getId(), 0);
log.info("修改訂單狀態結束");
log.info("下訂單結束");
}
訪問http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
數據庫情況:
下單后,@GlobalTransactional似乎並不能解決超時Feign異常事務回滾問題
4.6.Seata工作原理
4.6.1.Seata
2019年1月份螞蟻金服和阿里巴巴共同開源的分布式事務解決方案
Simple Extensible Autonomous Transaction Architecture,簡單可擴展自治事務框架
2020起始,參加工作后用1.0以后的版本
4.6.2.TC/TM/RM三大組件
TC:seata的服務器
TM:事物的發起者,業務的入口。 @GlobalTransactional(name = “txl-create-order”, rollbackFor = Exception.class)
RM:事務的參與者,一個數據庫就是一個RM。
- 分布式事務的執行流程
- TM 開啟分布式事務(TM 向 TC 注冊全局事務記錄),相當於注解
@GlobalTransaction
注解 - 按業務場景,編排數據庫,服務等事務內部資源(RM 向TC匯報資源准備狀態)
- TM 結束分布式事務,事務一階段結束(TM 通知 TC 提交、回滾分布式事務)
- TC 匯總事務信息,決定分布式事務是提交還是回滾
- TC 通知所有RM提交、回滾資源,事務二階段結束
- TM 開啟分布式事務(TM 向 TC 注冊全局事務記錄),相當於注解
4.6.3.AT模式如何做到對業務的無侵入
Ⅰ.是什么
Ⅱ.一階段加載
在一階段,Seata會攔截"業務SQL",
1.解析SQL語義,找到業務SQL,要更新的業務數據,在業務數據被更新前,將其保存成"before image"(前置鏡像)
2.執行業務SQL更新業務數據,在業務數據更新之后,將其保存成"after image",最后生成行鎖
以上操作全部在一個數據庫事務內完成,這樣保證了一階段操作的原子性
Ⅲ.三階段提交
二階段如果順利提交的話,
因為業務SQL在一階段已經提交至數據庫,所以Seata框架只需將一階段保存的快照和行鎖刪除掉,完成數據清理即可。
Ⅳ.二階段回滾
二階段如果回滾的話,Seata就需要回滾到一階段已經執行的"業務SQL",還原業務數據。
回滾方式便是用"before image"還原業務數據,但是在還原前要首先校驗臟寫,"對比數據庫當前業務數據"和"after image",如果兩份數據完全一致就說明沒有臟寫,可以還原業務數據,如果不一致說明有臟寫,出現臟寫就需要轉人工處理。
4.6.4.總結