Netflix網飛公司 和 Spring Cloud 和 Spring Cloud Alibaba之間的愛恨情仇這里我們就不多BB了,今天總結一下Spring Cloud Alibaba各大組件的使用,做一個學習總結
創建Maven父工程,貼入以下版本約束
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</dependency>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Alibaba-->
<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>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement> <build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement> <plugins>
<!--熱部署配置, 后面沒用,這個可以不用配-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
Nacos = Eureka + Config + Bus
思想還是借鑒的Eureka的思想,大家可以當成Eureka來用,但功能只增不減
基礎環境:JDK8 + Maven3.2+ (開發缺這兩個環境 ?)
如果大家想在服務器上裝Nacos的話,上面兩個環境必須備好
Nacos windows下載安裝:https://github.com/alibaba/nacos(版本隨意)
下載后解壓進入bin目錄,雙擊startup.cmd即可啟動服務,可以發現Nacos是Java開發的
-
可以發現Nacos是單獨部署的,而不是想Eureka、Config那樣起服務
啟動之后訪問localhost:8848/nacos即可看到web頁面
鍵入賬號和密碼,默認均為:nacos,登陸監控頁面
此時一個注冊中心和一個配置中心服務就算搭建成功,后面我們要使用的時候,對號入座即可
Nacos的服務注冊
基於我要寫完幾大組件,不想參雜無關的冗余模塊,所以這里我們演示一下注冊中心的功能,寫一個后面會用到的模塊,讓他注冊進Nacos,后面被其他模塊調用或者演示服務限流、降級、熔斷等通用的服務,下面我們就創建一個支付模塊:payment_8801
創建一個maven項目,改pom,創yml,寫入口函數,寫接口,一步到位
-
改pom
<dependencies> <!-- nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--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> <!--監控 以json格式輸出信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> </dependencies>
-
創yml
server: port: 8801 spring: application: name: payment-server cloud: nacos: discovery: #配置注冊中心的地址 server-addr: localhost:8848 #監控 management: endpoints: web: exposure: include: '*'
-
寫入口函數
@SpringBootApplication @EnableDiscoveryClient //開啟注冊中心 public class PaymentApplication8801 { public static void main(String[] args) { SpringApplication.run(PaymentApplication8801.class); } }
-
接口我們就不寫了,我們這里只是演示一下注冊中心
然后我們訪問我們的Nacos的頁面。如下所示,我們的服務已經在Nacos的服務列表上了,服務名和Eureka一樣,都是默認使用項目名稱
Nacos的配置中心
Config 配置中心 + Bus消息總線實現配置的實時刷新,Nacao打包一套帶走
繼續創建一個Maven項目,改pom,創yml,寫入口函數,寫接口,一步到位
-
改pom
<dependencies> <!-- SpringCloud ailibaba config--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- SpringCloud ailibaba nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--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> <!--監控 以json格式輸出信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> </dependencies>
-
創建yml
-
這里會創建兩個配置文件,bootstrap.yml 和 application.yml,這兩者不做說明,大家應該懂吧
bootstrap.yml 如下
-
server: port: 8888 spring: application: name: nacos-config cloud: nacos: discovery: server-addr: localhost:8848 #作為注冊中心的地址 config: server-addr: localhost:8848 #作為配置中心的地址 file-extension: yaml #指定yaml格式的配置 #監控 management: endpoints: web: exposure: include: '*'
application.yml配置如下
spring: profiles: active: dev #表示拉取配置中心開發環境的配置文件
穿插一個特別說明:大家仔細看一下我配置的配置中心的屬性
首先配置了配置中心的地址,就是Nacos的地址
然后我指定了一個讀取配置文件的后綴為:yaml
我選擇了運行環境為dev 開發環境
就這三個配置,已經足夠我去配置中心讀取配置了,他是這么定位文件的呢?
Nacos關於自己的配置中心鎖定文件有一套自己的定位方法
項目名稱 + “-” + 運行環境 + 后綴
上面的配置中
項目名稱為: nacos-config
運行環境我們選擇的:dev
指定了配置文件的后綴為:yaml (不能是yml,Nacos不能創建這個格式的文件)
所以我們的配置文件應該為:nacos-config-dev.yaml
如果這里已經了解了的話,Nacos的web頁面還可以配置一個命名空間和分組的功能,目的就是將各個配置文件分開來,一個命名空間有多個分組,每個分組里有多個配置文件,如果你設置了命名空間和分組,就得在上面的配置中加加兩個配置,用來定位命名空間和分組的屬性這個,如果我們不設置命名空間和分組,就是默認的,命名空間為public,分組為default,這個我們不用明文配置,默認讀取。
一般我們在使用時,都會使用命名空間和分組,主要是隔離不同的配置,新加配置
namespace: 165e1f54-5249-521f-20d0-890d225974a #命名空間編號,頁面會顯示 group: DEV_GROUP # 分組信息
一般對於命名空間,我們會創建三個:開發,測試,生產,實現配置隔離
而對於分組,可以把不同的微服務划分到一個組,統一管理
既然配置已經完成了,那我們就去配置中心創建一個配置文件吧
然后當然就是發布啦,發布后返回配置列表可以發現多了一個配置
配置文件就緒,讀取配置就緒,下面我們就寫一個接口測一測我們是否可以讀取到配置文件中的數據吧,讀取的就是我們剛剛配置的數據,注意那個頭上的注解,他代替了Bus
@RestController @RefreshScope //支持nacos作為config配置中心的動態刷新,一個注解干掉Bus public class NacosConfigController { //讀取配置中心的數據 @Value("${config.info}") private String info; //將配置中心讀取到的配置暴露出去測試一下 @GetMapping("/config/info") public String test1(){ return info; } }
啟動服務,測試我們的接口,如下所示,成功讀取到了配置中心配置的屬性
都到這一步了,不測測動態刷新就虧了,我們修改配置的version為2,保存后再次訪問該接口,發現配置已經實現動態刷新,我們再也不需要通過Bus來打太極了
Nacos數據持久化
生產環境中必配,目前只支持持久化到mysql,官方建議一主一從
在Nacos中,內嵌了一個數據庫derby,用作持久化數據,但是如果我們開啟了多個Nacos節點,各內部的數據的一致性是不可保證的,所以集群部署的前提就是數據的一致性,而數據的一致性要求多節點使用同一個數據庫。
因為我們現在是windows環境,Linux上操作一致
在我們的解壓文件中下conf目錄下有一個nacos-mysql.sql文件,這就是我們需要初始化數據庫的建表語句,導入到mysql中,可能會有版本導致導入失敗,這里各人解決
其次在conf下還有一個application.properties文件,在這里我們需要配置開啟持久化數據到mysql,並配置mysql相關的連接信息,注釋不用去打開,直接新增吧
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=nacos_devtest #更改用戶名 db.password=youdontknow #更改密碼
數據庫名要根據的數據庫來定,數據庫名無要求
然后重啟Nacos即可,發現之前的配置已經小時了,且控制台沒報錯就已經切換,如果還想確定是否已經切換了持久化源,可以新增一個配置,然后去數據庫看數據是否已經持久化到數據庫
Nacos的集群部署
在生產環境中,這種服務都是集群部署的,防止單點故障,保證高可用
講道理,搭這個集群搞崩了我三台虛擬機,現在我本地虛擬機都還沒配制好,淦
也不能怪Nacos,只能怪VMWare太脆弱,莫名其妙就崩了,后面再補上集群實操,這里就說說操着手法吧,有基礎的我建議使用Docker搭建,簡單方便
根據官方示意圖Nacos的集群搭建需要三個Nacos + 一個Nginx + 一個mysql(玩玩不用達主從),三個Nacos服務節點才能搭起集群,Nginx做最外層的訪問入口和負載均衡
准備工作:
三台服務器,每台3G內存,不然你就要走我的后路,內存不足,服務多的那台給4G
每台服務器一個Nacos服務節點,Nginx和Mysql隨便選一個節點部署
三台服務器需要JDK環境 + Maven環境
三台服務器均關閉防火牆,提醒一下
第一步:三台同步
修改Nacos持久化源為數據庫,上面已經說明過,三台統一使用一個數據庫
第二步:三台同步
修改conf目錄下cluster.config.example 重命名為 cluster.config
在其中鍵入參與集群的三台Nacos節點的ip:port,如下所示
192.168.0.140:8848
192.168.0.141:8848
192.168.0.142:8848
依次啟動三台服務,這其中可能會出現錯誤,在啟動的控制台他會告訴你日志在哪,看看日志有沒有拋出什么異常,目前我遇到過數據庫連接不上,內存不夠異常,均已修復
至於其他的異常,百度吧,后浪
第三步:配置Nginx
無非就是配一個反向代理,proxy_pass upstream指向三台服務
實現入口統一和負載均衡
配置完,Nginx配置文件檢查一把 ./nginx -t
配置文件沒錯再重啟 ./nginx -s reload
最后,訪問Nginx監聽的端口,成功訪問Nacos的頁面,搭建完成
Sentinel = Hystrix
說到Hystrix,我們就必須清清楚楚的明白什么是降級,什么是熔斷,復制了一段以前學習Spring Cloud 時寫的筆記,希望大家能夠明白
服務熔斷
服務雪崩:是一種因服務提供者的不可用導致服務調用者的不可用,並將不可用逐漸放大的過程。
雪崩效應:服務提供者因為不可用或者延遲高造成的服務調用者的請求線程阻塞,阻塞的請求會占用系統的固有線程數、IO等資源,當這樣的被阻塞的線程越來越多的時候,系統瓶頸造成業務系統癱瘓崩潰,這種現象成為雪崩效應
熔斷機制:熔斷機制是服務雪崩的一種有效解決方案,當服務消費者請求的服務提供者因為宕機或者網絡延遲高等原因造成暫時不能提供服務時,采用熔斷機制,當我們的請求在設定的最常等待響應閥值最大時仍然沒有得到服務提供者的響應的時候,系統將通過斷路器直接將吃請求鏈路斷開,這種解決方案稱為熔斷機制
服務降級
理解了上面所說的服務熔斷相關的知識,想想在服務熔斷發生時,該請求線程仍然占用着系統的資源,為了解決這個問題,在編寫消費者[重點:消費者]端代碼時就設置了預案,當服務熔斷發生時,直接響應有服務消費者自己給出的一種默認的,臨時的處理方案,再次注意"這是由服務的消費者提供的",服務的質量就相對降級了,這就是服務降級,當然服務降級可以發生在系統自動因為服務提供者斷供造成的服務熔斷,也可運用在為了保證核心業務正常運行,將一些不重要的服務暫時停用,不重要的服務的響應都由消費者給出,將更多的系統資源用作去支撐核心服務的正常運行,比如雙11期間,收貨地址服務全部采用默認,不能再修改,就是收貨地址服務停了,把更多的系統資源用作去支撐購物車、下單、支付等服務去了。
我們再簡單歸納一下:簡單來說就是服務提供者斷供了,其一為了保證系統可以正常運行,其二為了增加用戶的體驗,由服務的消費者調用自己的方法作為返回,暫時給用戶響應結果的一種解決方案;
Sentinel相對於Hystix:
和Nacos一樣,需要自己獨立部署一個服務
一套web界面可以進行更加細粒度的配置,流控,速率控制,服務熔斷,服務降級。
摘抄一句官網的話:Sentinel 是面向分布式服務架構的流量控制組件,主要以流量為切入點,從限流、流量整形、熔斷降級、系統負載保護、熱點防護等多個維度來幫助開發者保障微服務的穩定性。
更多詳細詳見官網: Go Sentinel
下載地址:https://github.com/alibaba/Sentinel/releases
下載的是一個jar,下載后直接 java -jar 執行即可部署服務,可見也是Java開發的,啟動之后默認端口為8080,我們訪問localhost:8080即可,賬號和密碼默認均為:sentinel
-
我們繼續創建一個maven項目,改pom,創yml,寫啟動函數,一步到位
<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> <version>2.2.0.RELEASE</version> </dependency> <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> <!--監控 以json格式輸出信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> </dependencies>
-
創yml
server: port: 8804 spring: application: name: sentinel-server cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置Sentinel的服務地址 dashboard: localhost:8080 #默認8719端口,本地啟動 HTTP API Server 的端口號 port: 8719 #監控 management: endpoints: web: exposure: include: '*'
-
創啟動函數
@EnableDiscoveryClient @SpringBootApplication public class SentinelApplication { public static void main(String[] args) { SpringApplication.run(SentinelApplication.class); } }
-
寫測試接口
@RestController public class SentinelController { @GetMapping("/test1") public String test1(){ return "This is test1"; } @GetMapping("/test2") public String test2(){ return "This is test2"; } }
-
然后我們登入Sentinel監控頁面,發現什么也沒有,Sentinel是懶加載的,在監控接口之前,需要我們手動跑一下接口,即可激活監控 : localhost:8804/test1
-
看見左邊Tab欄的功能嘛,接下來我們把常用的說道說道,首先第一個實時監控就是一個監控展示頁面,這個無需多講,下面開工
簇點鏈路
在該Tab欄中,主要顯示監控的的接口(需要有流量激活),右邊的配置才是我們的學習重點也就流控規則、降級規則、熱點規則、授權規則,在Tab欄中都有單獨的模塊進行設置,也可以在簇點鏈路中點擊相應圖標為某一個接口設置相關規則
流控規則
我們看的第一個就是流控規則,我們對test1這個接口設置流控,來到下面這一個頁面
我們為test1接口,設置如下的流量控制規則閥值為1,其余的按照上面默認的來,
也就是:
閥值類型:QPS
閥值:1
流控模式:直接
流控效果:快速失敗
接下來我們訪問我們的test1接口,在一秒內訪問一次一點問題都沒有,如果在一秒內連續的訪問超過閥值呢,我們來看看
也就是說當我們的流量超過閥值后,根據流控模式直接限流不讓訪問,且根據流控效果快速失敗直接返回Sentinel定義的提示信息
下面我們再來說一下流控模式的另一個模式:關聯流控
-
需要說明的是:這個閥值對test1 、test2兩個接口同時生效
效果是什么呢?
當我們對test2接口進行流量跑通的時候,超過閥值1秒/次,就會對test1接口進行限流以及根據快速失敗返回Sentins定義的提示信息
場景如下:
訂單服務調用支付服務完成下單操作,但是當支付服務已經快撐不住的時候,需要告訴訂單服務,我吃不消了,你限流從而減輕我的壓力,我們應該對訂單服務限流,從而緩解支付服務的壓力 ,就是這么個意思!
下面我們再來說一下流控模式的最后一個模式:鏈路流控
這里,我還沒有涉及到服務之間的相互調用,但這個也很簡單,我就口頭闡述,鏈路流控什么意思?
首先對直接訪問接口進行流控,也就是test1這個接口閥值只允許在1秒/次
其次服務之間的相互調用,形成很多的調用鏈路下面我們用兩個鏈路來說明
鏈路1 :A -> Z
鏈路2 :X -> Z
Z作為一個兩個調用鏈路中的一個公共服務服務,被大家調用
如果我配置了如下流控規則:沒寫的默認上述配置
資源名 A
流控模式:鏈路
入口資源:Z
這個時候 A -> Z 這條鏈路就被流控了,在該鏈路中無論A或者Z 超過閥值就會觸發流控,而X -> Z這條鏈路我可沒給他配置流控,他還是隨便訪問的,就是這么個意思
上面我們已經將流控效果解釋完了,下面我們來說道說道流控效果:快速失敗、Warn Up、排隊等候
-
第一個說明的也就是最簡單的流控效果:快速失敗(直接拒絕訪問)
每次我們流控生效的時候,看到的那個頁面就是默認的快速失敗產生的頁面(默認)
-
然后就是第二個流控效果:Warm Up (預熱)
這個比較適用與秒殺系統,平時流量得心應手的處理着,某個瞬間突然高並發的流量大炮接踵而至,直接把系統拉升到高水位可能瞬間把系統壓垮,通過"冷啟動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。warm up預熱主要用於啟動需要額外開銷的場景,例如建立數據庫連接等。
我們來詳細說一下這個預熱流控效果到底是怎么回事
官方說的是根據冷加載因子(默認3),閥值我們設置的為6,那兒會得到一個計算公式
閥值 / 冷加載因子(3)會得到一個值,這個值就是我們系統初開始提供的qps,
然后我們設置了一個預熱時長為3
意思就是test1這個接口最開始的QPS閥值為6 / 3 = 2 QPS,然后再3S之內,慢慢的將QPS提高,直到達到6為止
我設置的是個數據就很方便測試,剛開始,我們一秒按三次F5不過分吧,這點手速都沒有不配單身,最開始我們的QPS只有2,我們請求三次試一試發現被流控了,然后我們保持2次/秒繼續發着請求,三秒之后發現QPS提高到了6
然后我們就還剩最后一個流控效果了: 排隊等候
顧名思義,就是排隊嘛,當我們的QPS閥值為3的時候,加入來了10個並發怎么辦,直接拒絕訪問並提示流控信息?排隊等候不會不讓他們訪問,而是會讓他們等候,我們只有允許三個請求進來,其余的在外面等到,依次處理,當等待時長過長,無非就是超時重試嘛,有點類似於消息隊列的異步削峰效果
上面我們為test1接口配置了流控,QPS為1,超時時長為1秒,為了方便測試,我們需要在test1接口中打印一下相關信息,看一看
@GetMapping("/test1") public String test1() { System.out.println( LocalTime.now() + Thread.currentThread().getName() + "正在處理請求"); return "This is test1"; }
重啟后開始測試,這次我們不能再手點了,我們使用postman壓測,開10個線程,每個線程執行一次該請求,請注意觀察我打印的時間,就是一秒只處理一個請求
流控相關的就到這里結束吧,下面我們開始另一個篇章,降級規則
降級規則
熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。 ==Hystrix有半開狀態測試接口是否恢復,Sentinel通過設置降級時間來重新測試==
一共有三個降級策略,我們簡單說一下
-
時間窗口是固有的,通過時間窗口設置的時限來恢復關閉降級
RT(平均響應時間):當 1s 內持續進入 N 個請求,對應的平均響應時間均超過閾值,且在時間窗口時間以內的請求 >= 5,那么就會觸發降級,那么在接下的時間窗口期之內,對這個方法的調用都會自動地熔斷。窗口期過后,降級失效,恢復調用,注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。
異常比例 :當資源的每秒請求量 >=5(默認,可配置),並且每秒異常總數占通過量的比值超過閾值之后,資源進入降級狀態,即在接下的時間窗口之內,對這個方法的調用都會自動地返回。異常比率的閾值范圍是 [0.0, 1.0],代表 0% - 100%。
異常數 :當資源近 1 分鍾的異常數目超過閾值之后會進行熔斷。注意由於統計時間窗口是分鍾級別的,若 timeWindow 小於 60s,則結束熔斷狀態后仍可能再進入熔斷狀態。
-
首先我們來測試一下平均響應時間:RT
-
接口中,我們睡眠1秒,然后平均響應時間也設置為1秒,這樣一個請求的響應肯定會超過閥值,用作測試RT,這個 test2接口 我們就單獨針對降級,流控就不設置了
-
@GetMapping("/test2") public String test2() throws InterruptedException { Thread.sleep(1000); System.out.println("測試RT"); return "This is test2"; }
我們在時間窗口之內再通過瀏覽器的方式訪問接口,發現降級已經生效,等過了時間窗口設置的時間后,降級關閉,可正常訪問
下面我們開始說第二個降級策略:異常比例
@GetMapping("/test3") public String test3() throws InterruptedException { System.out.println("測試異常比例"); int i = 10 / 0; return "This is test2"; }
壓測10個請求,代碼中,我們在拋出異常之前進行了控制台輸出,因為拋出了異常,不好截圖,我就直說了吧,只處理了五個請求,所以得出一個結論:
當單秒請求並發超過5
且出現異常的請求占總的請求的比列超過了我們配置異常比例時就會發生降級
-
至於最后一個異常數,我相信大家看都能看明白,這里就不做代碼測試了,知道有這么個東西就好了,那么我們就翻篇進入下一個知識點吧
熱點規則
[來自官方] 何為熱點?熱點即經常訪問的數據。很多時候我們希望統計某個熱點數據中訪問頻次最高的 Top K 數據,並對其訪問進行限制。比如:
商品 ID 為參數,統計一段時間內最常購買的商品 ID 並進行限制
用戶 ID 為參數,針對一段時間內頻繁訪問的用戶 ID 進行限制
熱點參數限流會統計傳入參數中的熱點參數,並根據配置的限流閾值與模式,對包含熱點參數的資源調用進行限流。熱點參數限流可以看做是一種特殊的流量控制,僅對包含熱點參數的資源調用生效。
@GetMapping("/test4") @SentinelResource(value = "test4",blockHandler = "duty_test4") public String test4(@RequestParam(value = "p1" ,required = false)String p1, @RequestParam(value = "p2" ,required = false)String p2) { System.out.println("測試熱點參數" + "--" +p1 + "--" + p2); return "This is test4"; } //test4de 兜底方法,現在我們不用Sentinel默認的那個提示了,用自定義的 public String duty_test4(String p1, String p2, BlockException e){ System.out.println( p1 + p2); return "好像出了點問題"; }
添加了該熱點規則后,如果我們的請求中的只要帶有p1這個字段,在達到閥值的時候就會降級,因為自定義了降級后處理函數:duty_test4,就會執行這個兜底函數
為了更加明白的解釋他的意思,我門訪問一下接口試試:
http://localhost:8804/test4?p1=1 [ 降級生效 ]
http://localhost:8804/test4?p2=2 [ 降級沒有生效 ]
明白了嗎?也就是說,在我們配置參數索引為0的時候,就綁定了p1參數,一旦請求中有這個參數,且QPS達到閥值,就會啟用降級
下面還有個高級選項,一起來看看吧
首先參數類型那一欄是個下拉框,包含了8大基本類型和一個String類型,選擇String,參數值我們設定為 “窩淦”,限流閥值為3,添加即可出現和我上面這圖一樣的效果,當然這些高級選項都是基於我們前面配置的參數下標為0的p1的,效果就是,如果沒有這些設置,我們的p1在熱點規則的制約下只能達到1QPS,但是當我們給p1賦值為 “我淦” 的時候,QPS可以調到3,不會觸發降級效果,這就是參數列外形,顧名思義 列外,列外...,就是不走尋常路
系統規則
系統保護規則是從應用級別的入口流量進行控制,從單台機器的 load、CPU 使用率、平均 RT、入口 QPS 和並發線程數等幾個維度監控應用指標,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。
系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量生效。入口流量指的是進入應用的流量(
EntryType.IN
),比如 Web 服務或 Dubbo 服務端接收的請求,都屬於入口流量。
效果就是:對整個系統添加限流但是,我們不建議使用,這個跳過
SentinelResource注解
要說的就是配置兜底函數的實現
-
自定義方法,就拿我們剛剛測試代碼做說明,原封不動
@GetMapping("/test4") @SentinelResource(value = "test4",blockHandler = "duty_test4") public String test4(@RequestParam(value = "p1" ,required = false)String p1, @RequestParam(value = "p2" ,required = false)String p2) { System.out.println("測試熱點參數" + "--" +p1 + "--" + p2); return "This is test4"; } //test4de 兜底方法,現在我們不用Sentinel默認的那個提示了,用自定義的 public String duty_test4(String p1, String p2, BlockException e){ System.out.println( p1 + p2); return "好像出了點問題"; }
在接口函數的上方的注解:@SentinelResource(value = "test4",blockHandler = "duty_test4")
value:作為綁定接口的唯一標識,在Sentinel控制台中國添加熱點規則或者其他規則使用,用於定位
blockHandler :指定另一個函數,用作替換當降級發生時執行的默認函數,以后不會出現系統定義的那個降級提示了,替換的是我們自定義的函數,我們可以在這里返回一些我們應該返回的錯誤信息和狀態碼等
對比Hystrix,他也可以實現將自定義返回封裝到一個專門的類中,實現復用和代碼防融合,另外搭配一個屬性:blockHandlerClass指定該類即可,blockHandler 找的就是該類中的兜底函數了。
-
fallback 和 blockHandler
上一個列子我們記錄了 blockHandler 屬性的作用是當發生降級時,blockHandler指定的函數會替換掉 Sentinel自定義函數用作返回
而fallback屬性的作用在於:未觸發降級的情況下,接口函數拋出異常就會執行fallback指定的兜底函數
-
忽略異常
@SentinelResource注解還可以配置異常忽略,屬性為:exceptionsToIgnore ,下面我們開啟Openfein的時候統一做演示,目前我們常用的屬性就以下幾個:
@SentinelResource( value = "flag", fallback = "fallback_method", blockHandler = "block_method", exceptionsToIgnore = RuntimeException.class)
OpenFeign服務熔斷
好了,來到我們熟悉的領域,遠程調用OpenFeign,在上一章的開頭,我詳細再次咬文嚼字的閱讀了熔斷和降級的意思后,很明顯Sentinel返回的那些默認信息就是一種降級措施,而服務熔斷,接下來我們配合着Nacos + Sentinel再次學習一把。
首先提供兩個消息提供者,復用之前的模塊,我們修修補補就能揭竿為旗,草木皆兵
創建兩個除了端口不一致,其余全部一致的兩個模塊,用作服務提供者,待會實現負載均衡
再創建服務的消費者,通過OpenFeign遠程調用兩個服務提供者的服務
服務提供者 payment_8801改造
-
pom
<dependencies> <!-- 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> <!--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> <!--監控 以json格式輸出信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> </dependencies>
-
yml
server: port: 8801 spring: application: name: payment-server cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置地址 dashboard: localhost:8080 #默認8719端口,本地啟動 HTTP API Server 的端口號 port: 8719 #監控 management: endpoints: web: exposure: include: '*'
-
主啟動函數
@SpringBootApplication @EnableDiscoveryClient public class PaymentApplication8801 { public static void main(String[] args) { SpringApplication.run(PaymentApplication8801.class); } }
-
業務接口,主要返回自己服務的端口,便於區分負載均很是否生效
@RestController @RequestMapping("/payment") public class PaymentController { @Value("${server.port}") private String port; @GetMapping("/test1/{id}") public String test1(@PathVariable("id") Integer id){ return "This is Payment server,Port is " + port + "\t id為" + id; } }
然后當然是直接啟動這兩個服務提供者啦,注意項目名稱要一致,這樣形成集群,我們去Nacos看看呢,之前config那個項目被我停掉了,他所以顯示為紅色,表示服務不可用
服務消費者order_8803 改造
-
pom
<dependencies> <!-- 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> <version>2.2.1.RELEASE</version> </dependency> <!--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> <!--監控 以json格式輸出信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> </dependencies>
-
yml
server: port: 8803 spring: application: name: order-server cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置地址 dashboard: localhost:8080 #默認8719端口,本地啟動 HTTP API Server 的端口號 port: 8719 #feign集成hystrix需要配置開啟,實現降級 feign: sentinel: enabled: true #監控 management: endpoints: web: exposure: include: '*'
-
主啟動函數
@EnableDiscoveryClient @SpringBootApplication @EnableFeignClients(basePackages = "com.nacos.order.clients") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class); } }
-
遠程調用Feign接口
@FeignClient(value = "payment-server",fallback = PaymentClientDuty.class) public interface PaymentClient { @GetMapping("/payment/test1/{id}") public String test1(@PathVariable("id") Integer id); }
-
遠程調用兜底方法實現
@Component public class PaymentClientDuty implements PaymentClient { @Override public String test1(Integer id) { return "OpenFeign遠程調用失敗調用" + id; } }
-
Controller
@RestController public class OrderController { @Autowired private PaymentClient paymentClient; @GetMapping("/order/{id}") public String openfeignTest(@PathVariable("id") Integer id){ return paymentClient.test1(id); } }
-
好,起服務,看一下我們的訂單服務是否可以實現遠程負載均很的調用兩個支付模塊
-
再測試一下降級,我們將兩個支付模塊都給停掉,再訪問該接口,如我們預測一般無二
持久化規則
在上面的測試之中,你有沒有發現一個問題,當我們配置了相關規則后,只要服務重啟,這些規則就會不在,需要從新配置規則,那是因為他們都隨着服務的不可用而從Sentinel的內存中銷毀了,下面我們來配置將其持久化到Nacos之中,Nacos持久化到哪呢?之前已經說到過,當然是持久化到Mysql啦。
這次我們就是用訂單服務做一個演示吧,我們對其進行配置規則
相關的依賴,之前我已經將其添加到pom中,就是
<!-- 后續做持久化用到 -->
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
-
在Nacos中配置信息,如下所示,然后發布即可
[
{
"resource":"/order/1",//資源名稱,這個根據Sentinel給定的為准,定位的
"limitApp":"default",//來源應用
"grade":1,//閾值類型,0線程數,1QPS
"count":10,//單機閾值
"strategy":0,//流控模式,0表示直接,1表示關聯,2表示鏈路
"controlBehavior":0,//流控效果 ,0表示快速失敗,1表示warm up,2表示排隊等待
"clusterMode":false //是否集群
}
]
-
我們重啟訂單服務,訪問一下接口,讓Sentinel監控到,發現配置還在,並沒有消失。
Seata(最新版本巨坑)
寫在最前面:這個1.2為目前最前最新版,異常錯誤層出不窮,目前網上相關的博客也少的可憐,只有自己慢慢的測,大多是版本依賴問題,官方文檔又糙的很,自定義配置的載入可以為配在用yml中,也可以配在Seata默認讀取的文件,這個是我折騰兩天才發現的,所以得出那個道理,坐在二排看戲永遠是最穩當的,下面的所有配置和依賴都是我排錯最后留下的
分布式事務簡單介紹
設想在分布式項目中,一個微服務項目照顧一個到多個數據庫,數據源也是一個到多個,當一個業務需要服務調用服務的方式才能完成的時候,如何保證兩個服務在的統一事務控制,舉個列子,下單服務調用支付服務和配送服務,完成一個商品的購買,當下單后,支付服務調用失敗怎么辦,本地事務肯定是管控不了這個業務線的,不可能下單不付款就給配送吧,宇宙條都經不起這種虧損。
Seata的出現就是應對分布式事務而生的一個組件,下面我們的總結分為兩個大的部分進行說明
第一大模塊就是建立業務線模塊,創建三個模塊,分別是訂單模塊、庫存模塊、賬戶模塊 實現用戶下單減庫存且扣除用戶金額的業務線,我們使用Seata控制整條業務線的分布式事務
第二大模塊就是對Seata的執行原理進行debug解讀,Seata的使用時超級簡單的,但執行原地是有點復雜的,這個是我們今天總結的重點所在
准備工作
先統籌一下全局,讓大家心里有個譜,三個模塊,每個模塊對應一個數據庫訂單模塊對應訂單數據庫,庫存模塊對應庫存數據庫、賬戶模塊對應賬戶數據庫
Seata下載,這里我們使用最新的版本1.2,注意0.9之前(含)的版本解壓文件結構有變化
注意下載的版本和引入的seata依賴版本要一致,因為我們使用1.2學習,所以在引入依賴時也得為1.2版本,下載解壓開來,修改以下配置文件
conf / file.conf :這個文件會配置在每個參與分布式事務的服務的resource目錄下面
conf / registry.conf :這個文件會配置在每個參與分布式事務的服務的resource目錄下面
0.9之前Seata需要的數據庫sql都是放在conf下面的,到了1.0往后在conf目錄下給了一個README-zh.md的腳本說明文件,所有配置都去他指定的網站上自己取舍,下面我們各個去取並配置在我們本地
然后我們訪問 : https://github.com/seata/seata/tree/develop/script/config-center
將其下的config.txt拷貝到本地seata安裝目錄下,修改見下面
將其下nacos目錄下的nacos-config.sh 拷貝至本地seata下的conf目錄下
將其下的config的內容拷貝下來,寫在Seata目錄下,我將無關或者多余的配置去掉了留下了下面的內容,我們用的就是數據庫做為我們的配置
至於拷貝在conf目錄下的nacos-config.sh,我們需要這樣操作:
先將Nacos啟動,下面會初始化一些數據庫的配置配置到Nacos中,以后改數據庫相關的屬性直接就在Nacos上去改就可以,參與分布式事務業務線的服務都會連接Nacos
然后進入到conf目錄,敲開Git的Git Bash Here (右鍵啊)然后執行下面命令 :
sh nacos-config.sh
然后我們去我們的Nacos就會看到多了很多的關於數據庫連接信息的配置,如下
數據庫准備,因為三個模塊,三個庫,我們建庫建表開始(業務表)
建庫就自己建吧,這里我就只貼了相關的sql表
-- 在訂單數據庫 - 創建訂單表 CREATE TABLE `order`.`Untitled` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `user_id` bigint(11) NOT NULL COMMENT '用戶id', `product_id` bigint(11) NOT NULL COMMENT '產品id', `ount` int(11) NOT NULL COMMENT '購買數量', `money` decimal NOT NULL COMMENT '金額', `tatus` int(11) NOT NULL COMMENT '訂單狀態:0:創建中,1:已完成', PRIMARY KEY (`id`) ); --在庫存數據庫 - 創建庫存表 CREATE TABLE `storage` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `product_id` bigint(11) NOT NULL COMMENT '產品id', `total` int(11) NOT NULL COMMENT '總庫存', `residue` int(11) NOT NULL COMMENT '剩余庫存', PRIMARY KEY (`id`) ) ; --在賬戶數據庫 - 創建賬戶表 CREATE TABLE `account` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `user_id` bigint(11) NOT NULL COMMENT '用戶id', `total` decimal NOT NULL COMMENT '總額度', `residue` decimal NOT NULL COMMENT '剩余可用額度', PRIMARY KEY (`id`) );
還沒完,要想被Seata監控全局事務,我們得在參與事務的數據庫中多建一個表
也就是 seata/conf 下的 README-zh.md中的 client,那是一個連接,我們點擊進去
-
當然你可以可以點擊我准備好的: Client
-
將sql語句copy下來,在我們的 訂單數據庫 和 庫存數據庫 均執行,
-
這個表是Seata掌控全局事務的基礎
CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME NOT NULL COMMENT 'create datetime', `log_modified` DATETIME NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
然后就是最后一個sql腳本了,里面還有Steta自身需要的sql腳本
也就是 seata/conf 下的 README-zh.md中的 server,那是一個連接,我們點擊進去
-
當然你也可以直接點擊我准備好的 : server
-
進入db目錄下,可以發現目前只支持三種數據庫,分別是mysql、oracle、pgsql,我們使用的是mysql,那么我們就mysql的內容拷貝下來,在剛剛我們准備的seata數據庫中創建seata運行時需要的表,這里我也提供一下,一共三張表,隨着版本有點小變化,注意版本問題哦,我是1.2
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;
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, `gmt_modified` DATETIME, PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(96), `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;
數據庫相關的准備工作到這里就算告一段落。現在我們的數據如下所示
訂單模塊創建
訂單模塊 : seata_order_9000
-
pom
<dependencies> <!-- 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> <!--Seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入與自己版本相同的 --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.2.0</version> </dependency> <!--遠程調用 OpenFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.1.RELEASE</version> </dependency> <!--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> <!--監控 以json格式輸出信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <!--lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> <scope>provided</scope> </dependency> <!-- Mybatis整合SpringBoot --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency> <!-- jdbc --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.3.0.RELEASE</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.22</version> </dependency> </dependencies>
-
ymlserver:
port: 9000 spring: application: name: seata-order-server cloud: nacos: discovery: #Nacos注冊中心地址 server-addr: localhost:8848 datasource: type: com.alibaba.druid.pool.DruidDataSource #數據源類型 driver-class-name: com.mysql.cj.jdbc.Driver #mysql驅動包 url: jdbc:mysql://localhost:3306/test_order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root feign: hystrix: enabled: true logging: level: io: seata: info mybatis: mapper-locations: classpath:mapper/*.xml #這種是采用yml配置Seata的注冊中心和配置中心的方式,我們采用特定文件方式 #seata: # enabled: true # application-id: order # 應用 id 為唯一便於區分 # tx-service-group: my_test_tx_group # 事務分組,這個是默認分組,配置文件中修改 # config: # type: nacos # nacos: # namespace: # serverAddr: 127.0.0.1:8848 # group: SEATA_GROUP # userName: "nacos" # password: "nacos" # registry: # type: nacos # nacos: # application: seata-server # server-addr: 127.0.0.1:8848 # namespace: # userName: "nacos" # password: "nacos"
# enable-auto-data-source-proxy: false
#這邊自動代理關掉是因為,, seata源碼中SeataDataSourceBeanPostProcessor的初始化要比我的datasource初始話晚,導致datasoure不會被包裝為代理類,此處我自己代碼做了處理
-
入口啟動函數
//排除Spring Boot的自動數據源裝配,采用我們自己的 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableFeignClients(basePackages = "com.ninja.seata.client") @EnableDiscoveryClient @MapperScan(value = "com.ninja.seata.mapper") public class OrderApplication9000 { public static void main(String[] args) { SpringApplication.run(OrderApplication9000.class); } }
-
實體類
@Data public class Order implements Serializable { private long id; private long userId; private long productId; private long count; private double money; private long status; }
@Data @NoArgsConstructor @AllArgsConstructor public class CommonResult <T> implements Serializable { private Integer statu; private String Messgae; private T data; public CommonResult(Integer statu, String messgae) { this.statu = statu; this.Messgae = messgae; } }
-
mapper以及映射文件
@Mapper public interface OrderMapper { //添加一個訂單,並返回訂單id void addOrder(Order order); //修改訂單的狀態為 1 void updateStatus(@Param("id") long id); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ninja.seata.mapper.OrderMapper"> <insert id="addOrder" parameterType="com.ninja.seata.domain.Order"> <selectKey resultType="java.lang.Long" order="AFTER" keyProperty="id"> SELECT LAST_INSERT_ID() </selectKey> INSERT INTO `order` values (null,#{userId},#{productId},#{count},#{money},0) </insert> <update id="updateStatus" parameterType="long"> UPDATE `order` set status = 1 where id = #{id} </update> </mapper>
-
庫存、賬戶的遠程調用接口
@FeignClient(value = "seata-storage-server") public interface StorageClient { //減庫存 @GetMapping("/storage/reduce/{id}/{num}") public void reduce(@PathVariable("id") Long id, @PathVariable("num") Long num); }
@FeignClient(value = "seata-account-server") public interface AccountClient { //減余額 @GetMapping("/account/reduce/{userId}/{money}") public void reduce(@PathVariable("userId") Long userId, @PathVariable("money") Double money); }
-
service
@Service @Slf4j public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private StorageClient storageClient; @Autowired private AccountClient accountClient; //下面是我們發生分布式事務問題的函數 //Seata的使用就是這一個注解 + 一堆環境配置,哈哈哈哈哈 //seata開啟分布式事務,異常時回滾,name保證唯一即可,也可以不填 //rollbackFor:指定發生什么異常時執行回滾,也可以設置指定什么異常不回歸 @GlobalTransactional(rollbackFor = Exception.class) public Long createOrder(Order order){ log.info("----->餓了么來訂單啦"); //1: 首先模擬業務線創建一個訂單數據 orderMapper.addOrder(order); //2: 然后我們調用庫存服務,減庫存 storageClient.reduce(order.getProductId(),order.getCount()); //3: 調用賬戶服務,扣取該用戶的錢錢 accountClient.reduce(order.getUserId(),order.getMoney()); //最后這個訂單算完完成,我們修改訂單狀態為已完成:1 orderMapper.updateStatus(order.getId()); log.info("----->恭喜xx選手成功接單,訂單號為:" + order.getId()); return order.getId(); } }
-
controller
@RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; //便於測試,我們只傳數量和金額就可以了,使用get @GetMapping("/create/{count}/{money}") public CommonResult createOrder(@PathVariable("count") Long count, @PathVariable("money") Double money) { //自定義訂單,模擬下單數據 Order order = new Order(); order.setCount(count); order.setMoney(money); order.setProductId(1); //商品表准備好這個商品 order.setUserId(1); //賬戶表也准備好這個賬戶 Long resultCode = orderService.createOrder(order); if (resultCode == null){ return new CommonResult<>(444, "新增訂單失敗"); } return new CommonResult<>(200, "新增訂單成功",resultCode); } }
-
config :前面我們排除了SpringBoot自帶的,這里使用我們的,將數據源交給Seata代理
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } //DataSourceProxy 這個是Seata的東西,初始化數據庫代理,且只使用這個 @Bean @Primary public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }
-
Seata關於注冊中心和注冊中心的配置文件
將上面的file.conf 和 registry.conf 拷貝到Resource目錄下,整個工程就是這樣的
庫存模塊創建
-
pom
和訂單依賴一樣
-
yml
修改端口 、 項目名稱、數據庫連接信息,其余和訂單模塊一致
-
入口啟動函數
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableDiscoveryClient public class StorageApplication9004 { public static void main(String[] args) { SpringApplication.run(StorageApplication9004.class); } }
-
mapper和映射文件
@Mapper public interface StorageMapper { void redecu(@Param("id")long id, @Param("num")long num); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ninja.seata.mapper.StorageMapper"> <update id="redecu" parameterType="long"> update `storage` set residue = residue - #{num} where id = #{id} </update> </mapper>
-
controller直接調mapper
@RestController @RequestMapping("/storage") @Slf4j public class StorageController { @Autowired private StorageMapper storageMapper; @GetMapping("/reduce/{id}/{num}") public void reduce(@PathVariable("id")Long id,@PathVariable("num")Long num){ storageMapper.redecu(id,num); log.info(id + "商品,扣取庫存為:" + num); } }
-
config
配置數據源和訂單模塊一致
-
Seata關於注冊中心和注冊中心的配置文件
和訂單模塊一致
賬戶模塊創建
-
pom
和訂單模塊一致
-
yml
修改端口 、 項目名稱、數據庫連接信息,其余和訂單模塊一致
-
啟動入口函數
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableDiscoveryClient public class AccountApplication { public static void main(String[] args) { SpringApplication.run(AccountApplication.class); } }
-
mapper和映射文件
@Mapper public interface AccountMapper { void reduce(@Param("userId") long userId, @Param("money") double money); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ninja.account.mapper.AccountMapper"> <update id="reduce"> update account set residue = residue - #{money} where user_id = #{userId} </update> </mapper>
-
controller直接調用mapper
@RequestMapping("/account") @RestController @Slf4j public class AccountController { @Autowired private AccountMapper accountMapper; @GetMapping("/reduce/{userId}/{money}") public void reduce(@PathVariable("userId")Long userId, @PathVariable("money")Double money){ accountMapper.reduce(userId,money); log.info(userId + "用戶,扣除" + money + "余額"); } }
-
Seata關於注冊中心和注冊中心的配置文件
和訂單模塊一致,還是那兩個配置文件
環境測試(沒有Seata)
上面我們已經將三個模塊整裝待發,下面是我在沒有引入Seata相關配置和依賴下做的測試,先把他跑通,你們可以不用跳過這一步
首先商品數據和賬戶信息在數據庫定義好,如下所示
下面我們開測:
模擬異常環境
因為我們的業務是下訂單 -> 減庫存 ->扣賬戶 —>修改狀態的業務線,
這里我們就對扣賬戶這一個服務動點手腳,拋點異常或者線程隨眠都可以,讓訂單模塊無法調用賬戶模塊即可,我們就會發訂單已經創建了,庫存也扣了,但是沒有扣錢,怎么辦,分布式事務問題就這么如意料般的出現了,下面我們來解決
Seata登場
上面三個模塊的創建,已經創建好,下面我們啟動三個服務,發起請求之前,容我先把我的業務數據庫初始化一下,以便待會測試方便觀察
下面我們跑接口創建訂單,發現拋出了異常,我們再去數據庫觀察一把,發現三張表還是如我們初始化那般,沒有任何變化,到這里,Seata管控全局事務生效,Demo到此結束,下一篇我將會分析Seata的管控全局事務的執行原理(淺到邏輯,不涉及源碼)
Seata執行原理分析(AT模式)
在說原理之前,我們先回顧一下我們上面的所作所為,首先我們修改了Seata服務的配置,並將數據庫相關的屬性推送到了nacos上,其次為我們三個項目添加了Seata相關的依賴,並分別配置了兩個Seata的注冊中心為Nacos和配置中心為Nacos的文件,最后我們將我們自定義的數據源(Seata代理)加入了容器,並排除了SpringBoot自動封裝的數據源,讓Seata代理了數據庫的超控,然后我們在我們涉及分布式事務的業務類上加了一個Seata的注解,使的全局事務生效,理明白了這些后,下面我們來講述原理,這個執行原理,主要是通過數據庫產生的數據為依據
引入官方的套話:主要實現是靠:一個id + 三組件
-
id : 全局唯一的事務Id,在我們的seata數據庫中,每啟用一個全局事務,就會產生一個事務ID
TC組件 : 事務協調者 維護全局和分支事務的狀態,驅動全局事務提交或回滾。
管控我們三個微服務事務的老大,他來決定一起提交還是一起回滾
TM組件 :事務管理器 定義全局事務的范圍:開始全局事務、提交或回滾全局事務。
加了注解的那個服務發起了一個全局事務,在經過老大TC同意后上線一個全局事務
RM組件 :資源管理器 管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
相當於就是寄生在各服務上的一個監控者,監控本地事務的執行是否順利,及時向老大報告各服務的本地事務的情況,
處理過程
-
TM向TC申請開啟一個全局事務,全局事務創建成功並生成一個全局唯一的XID
-
XID在微服務調用鏈路的上下文中傳播
-
RM向TC注冊分支事務,將其納入XID對應全局事務的管轄
-
TM 向 TC 發起針對 XID 的全局提交或回滾請求
-
TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求
數據庫分析
-
我們將賬戶表的那個人為異常消除掉,現在屬於正常業務線,不會出現異常,在賬戶服務返回之前我們打一個斷點,觀察seata數據庫中的系統三張表的變化如下所示
看圖說話:
glbal_table:全局事務表 ,是TM向TC申請上線的一個全局事務,這個事務的信息存在在這張表中,關於全局事務的一些信息也在這個表中,特別留意我們上面說到的XID,可以看到,在后面的兩張表中,他是貫徹始終沒有變的
branch_table :分支表,我們一共三個微服務,每個微服務都是一個分支,他們都在以XID為基准的某個全局事務的管理下
lock_table:鎖表,這里應該看row_key這個屬性,也是在XID的管轄下,將三張表鎖了起來,有了上鎖就會有解鎖,下面我們通過我們來分析我們的業務在Seata的動向,來解釋他們的意思
-
一階段(RM操盤)
全局事務開啟,Seata代理了我們的數據源,他會得到我們的sql,並解析sql,得到其中的關鍵信息,比如是修改庫存:update storage set residue = 100 where id = 1,Seata就會從這條sql中得到操作信息(CRUD)、表信息、修改信息、條件信息,然后會根據這些解析信息先去查詢 select residue from storage where id = 1,得到一個我們還沒修改前的一條數據,並將這條數據存起來,放在我們的undo_log表(參與分布式事務的庫都應該有的表)中,這個操作叫生成“前置鏡像”,然后再執行我們的update語句,對該條數據進行修改(並未提交),修改完之后還沒結束,Seata會從前鏡像中得到此次修改的數據的主鍵,並根據該主鍵再去查詢修改后的數據,繼續存在undo_log表(和前置鏡像混合為一個json,在一條數據中)中,看下圖的rollback_info,里面就存了前置鏡像和后置鏡像一些XID等信息
我在官網找到了一張圖來驗證這個文件,rollback.info的內容如下所示
當生成了日志后,就會想TC注冊一個本服務的分支,(上面的圖中,三個服務注冊了三個分支),注冊分支后,還會根據主鍵id申請表中修改的數據的全局鎖(該鎖存於鎖表中),得到全局鎖之后,然后提交本地事務,釋放本地鎖,最后向TC匯報本地事務的提交情況
一階段本地事務提交前,需要確保先拿到 全局鎖 。
拿不到 全局鎖 ,不能提交本地事務。
拿 全局鎖 的嘗試被限制在一定范圍內,超出范圍將放棄,並回滾本地事務,釋放本地鎖。
舉一個官方給的列子:
兩個全局事務 tx1 和 tx2,分別對 a 表的 m 字段進行更新操作,m 的初始值 1000。
tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務提交前,先拿到該記錄的 全局鎖 ,本地提交釋放本地鎖。 tx2 后開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務提交前,嘗試拿該記錄的 全局鎖 ,tx1 全局提交前,該記錄的全局鎖被 tx1 持有,tx2 需要重試等待 全局鎖 。
二階段之回滾(RM操盤)
在全局事務的管理下,當某一個服務出現異常時,TC就會給RM發布回滾請求,這個時候RM就會開啟一個本地事務,進行下面的操作
通過 XID 和 Branch ID 查找到相應的 UNDO LOG 記錄。
數據校驗:拿 UNDO LOG 中的后置鏡像與當前數據進行比較,如果有不同,說明數據被當前全局事務之外的動作做了修改。這種情況,需要根據配置策略來做處理,
根據 UNDO LOG 中的前置鏡像和業務 SQL 的相關信息生成並執行回滾的語句:
提交本地事務。並把本地事務的執行結果(即分支事務回滾的結果)上報給 TC
二階段之提交
收到 TC 的分支提交請求,把請求放入一個異步任務的隊列中,馬上返回提交成功的結果給 TC。
異步任務階段的分支提交請求將異步和批量地刪除相應 UNDO LOG 記錄
提交后會釋放全局鎖,其他全局事務才能繼續對這條數據進行操作
-
總結一下,我們使用的是Seata的AT模式,該模式的提交一共有兩個階段
-
一階段 prepare 行為:在本地事務中,一並提交業務數據更新和相應回滾日志記錄。
-
二階段 commit 行為:馬上成功結束,自動 異步批量清理回滾日志。
-
二階段 rollback 行為:通過回滾日志,自動 生成補償操作,完成數據回滾。
-
-
這兩個階段中的讀寫問題,我們就用官方文檔給的列子和說明來理解
-
寫隔離
-
一階段本地事務提交前,需要確保先拿到 全局鎖 。
-
拿不到 全局鎖 ,不能提交本地事務。
-
拿 全局鎖 的嘗試被限制在一定范圍內,超出范圍將放棄,並回滾本地事務,釋放本地鎖。
比如 :兩個全局事務 tx1 和 tx2,分別對 a 表的 m 字段進行更新操作,m 的初始值 1000。tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務提交前,先拿到該記錄的 全局鎖 ,本地提交釋放本地鎖。 tx2 后開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務提交前,嘗試拿該記錄的 全局鎖 ,tx1 全局提交前,該記錄的全局鎖被 tx1 持有,tx2 需要重試等待 全局鎖 。
如果 tx1 的二階段全局提交:全局提交,則tx1釋放 全局鎖 。tx2 拿到 全局鎖 提交本地事務。
如果 tx1 的二階段全局回滾,則 tx1 需要重新獲取該數據的本地鎖,進行反向補償的更新操作,實現分支的回滾。此時,如果 tx2 仍在等待該數據的 全局鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗。分支的回滾會一直重試,直到 tx2 的 全局鎖 等鎖超時,放棄 全局鎖 並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功。
因為整個過程 全局鎖 在 tx1 結束前一直是被 tx1 持有的,所以不會發生 臟寫 的問題。
-
-
讀隔離
-
在數據庫本地事務隔離級別 讀已提交 或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是 讀未提交 。
如果應用在特定場景下,必需要求全局的 讀已提交 ,目前 Seata 的方式是通過 SELECT FOR UPDATE 語句的代理。
SELECT FOR UPDATE 語句的執行會申請 全局鎖 ,如果 全局鎖 被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 語句的本地執行)並重試。這個過程中,查詢是被 block 住的,直到 全局鎖 拿到,即讀取的相關數據是 已提交 的,才返回。
出於總體性能上的考慮,Seata 目前的方案並沒有對所有 SELECT 語句都進行代理,僅針對 FOR UPDATE 的 SELECT 語句。
-