Spring Cloud Netflix項目進入到維護模式html
什麼是維護模式?=> 將模塊置於維護模式,意味着Spring Cloud團隊將不會再向模塊添加新功能(咱們將修復block級別的bug以及安全問題,咱們也會考慮並審查社區的小型pull request)。java
進入維護模式意味着?=> SpringCloud Netflix將再也不開發新的組件。新組件功能將以其餘替代品代替的方法實現。mysql
官網 誕生於2018.10.31,Spring Clooud Alibaba正式入駐Spring Cloud官方孵化器,並在Maven中央庫發佈了第一個版本。linux
能幹什麼?nginx
功能 | 具體說明 |
---|---|
服務限流與降級 | 默認支持Servlet,Feign,RestTemplate,Dubbo和RocketMQ限流降級功能的接入,能夠在運行時經過控制檯實時修改限流降級規則,還支持查看限流降級Metrics監控。 |
服務註冊與發現 | 適配Spring Cloud服務註冊與發現標準,默認集成了Ribbon的支持。 |
分佈式配置管理 | 支持分佈式系統中的外部化配置,配置更改時自動刷新。 |
消息驅動能力 | 基於SpringCloud Stream 爲微服務應用構建消息驅動能力。 |
阿里雲對象儲存 | 阿里雲提供的海量,安全,底成本,高可靠的雲存儲服務。支持在任什麼時候間,任何地點儲存和訪問任意類型的數據。 |
分佈式任務的調度 | 提供秒級,精準,高可靠,高可用的定時(基於Cron表達式)任務調度服務。同時提供分佈式的任務執行模型,如網格任務。網格任務支持海量子任務均勻分配到全部Worker(schedulerx-client)上執行。 |
使用SpringCloud Alibaba須要在POM文件中引入其依賴:git
<dependencyManagement> <dependencies> <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> </dependencies> </dependencyManagement>
官方文檔github
「Nacos」,前四個字母分別問Naming和Configuration的前兩個字母,最後的s爲Service,它是一個更易於構建雲原生應用的動態服務發現、配置管理和服務管理平臺,說白了就是 註冊中心 + 配置中心 的組合,等價於SpringCloud以前的 Eureka + Config + Bus,因此Nacos能夠替代Eureka作服務中心,能夠替代Config作配置中心。web
在Nacos官網下載地址中下載Nacos,在安裝Nacos前須要本地Java8和Maven的環境已經準備好,而後下載其壓縮包:算法
解壓該壓縮文件後直接運行在bin目錄下的startup.cmd啓動Nacos:spring
而後訪問 http://localhost:8848/nacos/ 進入Nacos,其默認的帳號密碼都是nacos,登陸成功後便可看到以下界面:
在以前學習服務註冊中心的時候講過CAP理論,即
C | A | P |
---|---|---|
Consistency | Available | Partition tolerance |
強一致性 | 可用性 | 分區容錯性 |
CAP原則又稱CAP定理,指的是在一個分佈式系統中,一致性、可用性、分區容錯性。
CAP 原則指的是,這三個要素最多隻能同時實現兩點,不可能三者兼顧。在分佈式架構中,P永遠要求被保證,因此當前的分佈式架構只有AP和CP兩種。Nacos屬於AP模型,再次對比這些服務註冊中心:
服務註冊與發現框架 | CAP模型 | 控制檯管理 | 社區活躍度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x版本歷史問題) |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP(事實上也能夠支持CP) | 支持 | 高 |
聽說Nacos在阿里巴巴內部有超過10萬的實例運行,已通過了相似雙十一等各類大型流量的考驗。
基於Nacos的服務提供者
新建Module:cloudalibaba-provider-payment9002做爲服務提供方微服務。固然爲了實現Nacos的負載均衡,使nacos-payment-provider服務集羣化,咱們同事也仿照9002搭建一個9003微服務。
而後在工程的父POM中引入依賴(Spring Cloud Alibaba簡介中引入的依賴)的前提下,在該模塊的POM引入依賴Nacos服務註冊發現的依賴:
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
而後添加其配置文件application.yml:
server: port: 9002 spring: application: name: nacos-payment-provider # 微服務名稱 cloud: nacos: discovery: server-addr: localhost:8848 # 配置Nacos地址 management: endpoints: web: exposure: include: '*' # 監控端點所有打開
編寫其主啓動類並在主啓動類上添加 @EnableDiscoveryClient
註解,使9002微服務可以被註冊中心發現:
@EnableDiscoveryClient @SpringBootApplication public class PaymentMain9002 { public static void main(String[] args) { SpringApplication.run(PaymentMain9002.class); } }
而後編寫一個簡單的業務類:
@RestController public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/payment/nacos/{id}") public String getPayment(@PathVariable("id") Integer id) { return "Nacos服務註冊,端口:" + serverPort + ";id:" + id; } }
啓動900二、9003微服務模塊,在Nacos的服務列表中咱們能夠看到這兩個微服務已經入駐服務註冊中心:
點開nacos-payment-provider服務的詳情頁面,能夠看到服務的實例詳情:
基於Nacos的服務消費者
新建Module:cloudalibaba-consumer-nacos-order83做爲服務消費方微服務,在該模塊的POM中一樣引入令Nacos服務註冊中心發現本身的依賴,配置其配置文件:
server: port: 83 spring: application: name: nacos-order-cosumer cloud: nacos: discovery: server-addr: localhost:8848 # 消費者將要去訪問的微服務名稱(註冊成功進Nacos的微服務提供者) service-url: nacos-user-service: http://nacos-payment-provider
而後編寫服務消費方的主啓動類,Nacos自己就具備負載均衡功能,由於引入Nacos依賴的同時,Nacos內部集成了Ribbon,如圖所示:
而咱們在學習Ribbon時知道,用了Ribbon就須要使用 RestTemplate
,全部咱們編寫配置類,向Spring容器中注入 RestTemplate
:
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced //負載均衡 public RestTemplate getRestTemplate() { return new RestTemplate(); } }
注意 注入 RestTemplate
時必定要添加 @LoadBalanced
註解,不然不會開啓負載均衡,在服務提供方集羣的狀況下,因爲沒有開啓負載均衡,消費方會沒法選擇具體調用哪一個微服務實例,也就是服務提供方實例有不少個,而消費方服務不知道要調用哪一個具體的服務提供方實例,不加該註解會產生 UnknownHostException
的異常。
而後編寫其業務類:
@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); } }
啓動83服務消費方,在Nacos服務註冊中心中咱們能夠看到入駐了兩個服務提供方實例和一個服務消費方實例。
測試
屢次訪問 http://localhost:83/consumer/payment/nacos/1 ,咱們發現服務消費方微服務83能夠完成對服務提供方微服務9002/9003的輪詢負載均衡調用,也就是 Nacos內部就整合了Ribbon,實現了負載均衡,默認負載均衡算法採用輪詢。
事實上,Nacos能夠在AP模式和CP模式中進行切換,也就是說Nacos不只僅支持AP(可用性和分區容錯性),它一樣支持CP(一致性和分區容錯性)。當 採起入駐到Nacos服務註冊中心的微服務對本身的健康狀態進行上報時,也就是對入駐到註冊中心的微服務進行非持久化的保存,一旦客戶端上報不健康信息,就將不健康的實例摘除掉,這相似於Eureka(固然Eureka能夠開啓自我保護模式);當 採起由Nacos服務註冊中心本身探測入駐到中心的微服務是否健康時,也就是對入駐到註冊中心的微服務進行持久化保存,即便服務註冊中心發現微服務已經不健康了,也不會刪除到微服務,這相似於Consul。以下圖(CoreDNS也是一種服務註冊中心):
Nacos與其餘服務註冊中心特性的對比:
Nacos | Eureka | Consul | CoreDNS | Zookeeper | |
---|---|---|---|---|---|
一致性協議 | CP/AP | AP | CP | / | CP |
健康檢查 | TCP/HTTP/MySQL/Client Beat(客戶端心跳) | Clent Beat | TCP/HTTP/gRPC/cmd | / | Client Beat |
負載均衡 | 權重/DSL/元數據/CMDB | Ribbon | Fabio | RR | / |
雪崩保護 | 支持 | 支持 | 不支持 | 不支持 | 不支持 |
自動註銷實例 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
訪問協議 | HTTP/DNS/UDP | HTTP | HTTP/DNS | DNS | TCP |
監聽支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
多數據中心 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
跨註冊中心 | 支持 | 不支持 | 支持 | 不支持 | 不支持 |
SpringCloud集成 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
K8s集成 | 支持 | 不支持 | 支持 | 支持 | 不支持 |
從理論知識中咱們知道Nacos服務註冊中心能夠在AP和CP模式中進行切換,C是全部節點在同一時間看到的數據是一致的,而A是全部的請求都會受到響應(A能夠近似理解爲高可用)。
通常來講,若是不須要存儲服務級別的信息且服務實例是經過Nacos-Client註冊的,而且服務能保持心跳上報,那麼就能夠選擇AP模式,當前主流的微服務框架如SpringCloud和Dubbo,都適用於AP模式,AP模式爲了服務的可能性而減弱了一致性,所以 AP模式下只支持註冊臨時實例。
而若是須要在服務級別編輯或存儲配置信息,那麼CP是必須的,K8s服務和DNS服務使用於CP模式,CP模式下則支持註冊持久化實例,此時以Raft協議爲集羣運行模式,該模式下注冊實例以前必須先註冊服務,若是服務不存在,則會返回錯誤。
若是須要讓Nacos服務從AP模式切換到CP模式的話,只須要向服務註冊中心發送POST請求便可:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
在用SpringCloud Config結合SpringCloud Bus時,咱們能夠把配置信息託管到遠端如GitHub上,而如今咱們應用了Nacos後,能夠直接在Nacos上託管配置信息。
新建Module:cloudalibaba-config-nacos-client3377做爲服務配置中心微服務,在其POM文件中引入必要的依賴(如Nachos服務註冊中心的依賴)爲,引入配置中心的依賴:
<!--nacos-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>
Nacos同SpringCloud Config同樣,在項目初始化時,要保證先從配置中心(直接以Nacos做爲配置中心)進行配置拉取,拉取配置後,才能保證項目的正常啓動,而咱們知道SpringBoot配置文件的加載順序是存在優先級的,bootstrap的優先級要高於application,因此咱們建立兩個配置文件 bootstrap.yml
(從配置中心拉取配置)和 application.yml
(寫本身的配置),咱們配置 bootstrap.yml
以使3377服務從Nacos上拉取配置信息:
# nacos配置 server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: #Nacos服務註冊中心地址 server-addr: localhost:8848 config: #Nacos做爲配置中心地址 server-addr: localhost:8848 # 指定yaml格式的配置,也就是說從Nacos上讀yaml格式的配置文件 file-extension: yaml # 配置匹配規則 # ${spring.application.name}-${spring.profile.active}.${file-extension}
而後用 application.yml
定製本身的配置信息,將環境定義爲開發環境:
spring: profiles: active: dev # 表示開發環境
編寫其主啓動類後,而後編寫其業務類,在業務來上添加SpringCloud的原生註解@RefreshScope
以使服務能夠支持從Nacos配置中心動態刷新配置信息:
@RestController @RefreshScope //支持Nacos的動態刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } }
在上面的業務層中咱們從配置中心讀取了配置信息
而咱們在Nacos配置中心中添加配置信息,在Nacos配置中心中添加配置文件要遵循必定的匹配規則。 ↓
Nacos中的 dataId
的組成格式及與SpringBoot配置文件中的匹配規則,更詳細的信息能夠參考Nacos官方文檔。
在Nacos SpringCloud中,dataId
的完整格式爲:
${prefix}-${spring.profile.active}.${file-extension}
prefix
默認爲 spring.application.name
(服務名)的值,也能夠經過配置項 spring.cloud.nacos.config.prefix
來配置。spring.profile.active
即爲當前環境對應的 profile(上面咱們在 application.yml
中配置的屬性),詳情能夠參考 Spring Boot文檔。 注意:當 spring.profile.active
爲空時,對應的鏈接符 -
也將不存在,dataId 的拼接格式變成 ${prefix}.${file-extension}
, 建議 不要使 spring.profile.active
爲空。file-exetension
爲配置內容的數據格式,能夠經過配置項 spring.cloud.nacos.config.file-extension
來配置。目前只支持 properties
和 yaml
類型。因爲prefix
默認爲 spring.application.name
(服務名)的值,因此 dataId
的完整格式能夠替換爲:
${spring.application.name}-${spring.profile.active}.${file-extension}
結合咱們在 boostrap.yml
配置文件和 application.yml
配置文件的配置,咱們能夠獲得在當前實例中的 dataId
應該爲:
nacos-config-client-dev.yaml
根據上述公式獲得 dataId
應爲的值後,咱們就能夠在Nacos配置中心的配置列表中新建配置文件。在新建配置文件時,Data ID
中填入咱們獲得的 dataId
,組名先選擇默認的便可,配置格式選擇咱們在 file-extension
中設置的YAML格式,而後編寫配置文件,編寫後點擊 發佈便可:
注意,在Nacos中的
dataId
中的後綴名必須用yaml
而不能用yml
。
添加配置文件後再點開Nacos配置中心的配置列表,就能發現已經存在剛纔建立的配置文件:
而後咱們啓動3377微服務,訪問 http://localhost:3377/config/info 查看可否訪問都Nacos配置中心的配置信息:
Nacos配置中心直接就 支持動態刷新,再更改了Nacos配置中心的配置信息後,再經過3377微服務訪問,就能夠獲得更新後的配置信息。目前Nacos已經實現了Eureka + Config + Bus的功能,可是Nacos之因此如此優秀是由於這些框架有的功能它都有,而這些框架沒有的功能它還有!下面看Nacos的一些高級功能。
問題:多環境多項目管理
在多環境多項目管理時,也就是實際開發中,一般一個系統會準備不一樣的開發環境,如dev開發環境、test測試環境、prod生產環境,如何保證指定環境啓動時服務能正確讀取到Nacos上相應環境的配置文件呢?一個大型分佈式微服務系統會有不少微服務子項目,每一個微服務項目又都會有相應的開發環境、測試環境、預發環境、正式環境等,那對這些微服務配置該如何管理呢?=> 在Nacos配置中心中就能夠進行分類配置,一個配置文件的具體所屬由 namespace
+ Group
+ dataId
所構成:
爲何如此設計?
這種設計就相似於Java中的包名和類名,最外層的 namespace
是能夠用於區分部署環境的,主要用來實現隔離,好比有三個環境:開發、測試、生產,那就能夠建立三個 namespace
。
Group
和 dataId
邏輯上區分兩個目標對象。Group能夠把不一樣的微服務劃分到同一個分組裏面去。Service就是微服務,一個Service能夠包含多個Cluster(集羣),Nacos默認Cluster是DEFALUT,Cluster是對指定微服務的一個虛擬劃分。
舉例:好比說爲了容災,將Service微服務分別部署在了杭州機房和廣州機房,這時就能夠給杭州機房的Service微服務起一個集羣名稱HZ,給廣州機房的Service微服務起一個集羣名字GZ,還能夠儘可能讓同一個機房的微服務互相調用,以提高性能。
最後就是Instance
,就是微服務的實例。
默認狀況下 namespace
的值爲 public
, Group
的值爲 DEFAULT_GROUP
,默認Cluster是DEFAULT。
下面對這三個屬性進行詳細展開。
dataId
方案
指定 spring.profile.active
配置屬性和配置文件的 dataId
來使不一樣環境下讀取不一樣的配置,也就是說咱們如今用默認的命名空間 namespace
(即 public
),默認的分組 Group
(即 DEFAULT_GROUP
),而後在Nacos配置中心中創建兩個 dataId
分別爲 dev
環境和test
環境。此時在同一命名空間同一組中就有了兩個應用於開發、測試不一樣環境的配置文件:
而後咱們經過修改微服務本身定製配置信息 application.yml
配置文件中的spring.profile.active
屬性便可進行多環境下的配置文件的讀取,配置什麼就加載什麼,在以前咱們的3377微服務獲取的是配置中心中 nacos-config-client-dev.yaml
配置文件的配置信息,如今咱們將 application.yml
修改成:
spring: profiles: # active: dev # 表示開發環境 active: test # 表示開發環境
而後重啓3377微服務,再次進行測試發現訪問到的配置信息爲測試環境的配置信息:
Group
方案
不只經過 dataId
能夠實現環境分區,應用 Group
也能夠實現環境分區,首先咱們在Nacos配置中心新建兩個相同 dataId
,不一樣 Group
的配置文件,一個用於開發環境,一個用於測試環境:
因爲這兩個配置文件的 dataId
均爲 nacos-config-client-info.yaml
,根據以前解釋的 dataId
的命名規則,咱們應該將3377微服務的自定義配置文件 application.yml
中的 spring.profile.active
屬性更改成 info
:
spring: profiles: active: info
在確認了讀取的配置文件的 dataId
後,咱們在其 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.group
屬性,設置爲相應的組名便可,好比咱們如今設置爲測試環境組:
spring: cloud: nacos: config: group: TEST_GROUP
重啓3377微服務,再次訪問能夠發現獲取到的配置信息即爲TEST_GROUP
組的配置信息。
namespace
方案
根據namespace一樣能夠進行環境區分。
新建兩個命名空間
服務列表查看
在 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.namespace
屬性,設置爲相應的命名空間流水號便可,好比咱們如今設置爲開發環境命名空間:
spring: cloud: nacos: config: namespace: 5e4fbf07-10f3-4216-86c5-e40391667310
在dev命名空間下新建不一樣group的配置文件
重啓3377微服務,再次訪問能夠發現獲取到的配置信息即爲 dev
命名空間下TEST_GROUP
組的配置信息。
官網集羣部署架構圖
開源的時候推薦用戶把全部服務列表放到一個vip下面,而後掛到一個域名下面
http://ip1:port/openAPI 直連ip模式,機器掛則須要修改ip纔可使用。
http://SLB:port/openAPI 掛載SLB模式(內網SLB,不可暴露到公網,以避免帶來安全風險),直連SLB便可,下面掛server真實ip,可讀性很差。
http://nacos.com:port/openAPI 域名 + SLB模式(內網SLB,不可暴露到公網,以避免帶來安全風險),可讀性好,並且換ip方便,推薦模式
VIP:即虛擬IP,即Nginx
上圖翻譯一下就是
默認Nacos使用嵌入式數據庫實現數據的存儲,因此,若是啓動多個默認配置下的Nacos節點,數據儲存是存在一致性問題的。Nacos默認自帶的嵌入式數據庫爲derby。
爲了解決這個問題,Nacos採用了 集中式存儲的方式來支持集羣化部署,目前只支持MySQL的存儲。
單機模式支持MySQL,在0.7版本以前,在單機模式時Nacos使用嵌入式數據庫實現數據的儲存,不方便觀察數據的基本狀況。0.7版本增長了支持MySQL數據源能力,具體的操做步驟以下。
derby到mysql切換配置步驟:
# 若是是Mysql5.+ spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://mpolaris.top:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC db.user=root db.password=123456 # 若是是Mysql8.+ spring.datasource.platform=mysql jdbc.DriverClassName=com.mysql.cj.jdbc.Driver db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user= root db.password= 123456
啓動Nacos,能夠看到是個全新的空記錄界面,之前是記錄進derby。
須要一個Nginx,三個Nacos註冊中心,一個Mysql
Nacos下載Linux版,下載地址,nacos-server-1.1.4.tar.gz,解壓後安裝
集羣配置步驟
Linux服務器上MySQL數據庫配置(按照4.3配置)
application.properties配置(按照4.3配置)
Linux服務器上Nacos的集羣配置cluster.conf
cd /opt/nacos/conf cp cluster.conf.example cluster.conf
注意:這個IP不能寫127.0.0.1,必須是Linux命令hostname -i 可以識別的IP
# it is ip # example # 這裏我填的是本身的私網ip 172.20.xxx.xxx:3333 172.20.xxx.xxx:4444 172.20.xxx.xxx:5555
編輯Nacos的啓動腳本startup.sh(先備份),使它可以接受不一樣的啓動端口
# 平時單機版的啓動,都是./startup.sh便可 # 可是集羣啓動,咱們但願能夠相似其餘軟件的shell命令,傳遞不一樣的端口號啓動不一樣的nacos實例。 # 例如:命令 ./startup.sh -p 3333 表示啓動端口號爲3333的nacos服務器實例 vim ./startup.sh # 修改第57行開始增長p export SERVER="nacos-server" export MODE="cluster" export FUNCTION_MODE="all" while getopts ":m:f:s:p:" opt do case $opt in m) MODE=$OPTARG;; f) FUNCTION_MODE=$OPTARG;; s) SERVER=$OPTARG;; p) PORT=$OPTARG;; ?) echo "Unknown parameter" exit 1;; esac done # 修改第134行,增長 - Dserver.port=${PORT} nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1
./startup.sh -p 3333 ./startup.sh -p 4444 ./startup.sh -p 5555 #注意:提示服務器內存不夠,能夠把nacos啓動腳本里-Xms和-Xmx jvm內存調整小一點 # 同時經過window瀏覽器訪問: http://192.168.42.82:3333/nacos/#/login http://192.168.42.82:4444/nacos/#/login http://192.168.42.82:5555/nacos/#/login
Nginx的配置,由它做爲負載均衡器
#user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; upstream cluster { server 127.0.0.1:3333; server 127.0.0.1:4444; server 127.0.0.1:5555; } server { listen 1111; server_name localhost; location / { # root html; # index index.html index.htm; proxy_pass http://cluster; } } }
cd /usr/local/nginx/sbin ./nginx -c /opt/nginx/conf/nginx.conf
開始測試
微服務測試
微服務springalibaba-provider-payment9002啓動註冊進nacos集羣
server: port: 9002 spring: application: name: nacos-payment-provider # 微服務名稱 cloud: nacos: discovery: # server-addr: localhost:8848 # 配置Nacos地址 # 換成Nginx的1111端口,作集羣 server-addr: 192.168.42.1:1111 management: endpoints: web: exposure: include: '*' # 監控端點所有打開
高可用總結
主要特性
與Hystrix對比
Sentiel能夠解決的問題
Sentinel分爲兩部分:
運行命令
# 前提:Java8環境,8080端口不被佔用 java -jar sentinel-dashboard-1.7.0.jar
訪問Sentinel管理界面
啓動Nacos8848,http://localhost:8848/nacos/#/login
新建Moudle:cloudalibaba-sentinel-service8401
pom.xml
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel-datasource-nacos 後續作持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--其餘基礎包-->
yml配置文件
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 # Nacos服務註冊中心地址 sentinel: transport: dashboard: localhost:8080 # 配置Sentinel dashboard地址 # 默認8719端口,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的端口 port: 8719 datasource: ds1: nacos: server-addr: localhost:8848 dataId: cloudalibaba-sentinel-service groupId: DEFAULT_GROUP data-type: json rule-type: flow management: endpoints: web: exposure: include: '*' feign: sentinel: enabled: true # 激活Sentinel對Feign的支持
主啓動
@EnableDiscoveryClient @SpringBootApplication public class MainApp8401 { public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } }
業務類FlowLimitController
@RestController @Slf4j public class FlowLimitController { @GetMapping("/testA") public String testA() { return "==> testA"; } @GetMapping("/testB") public String testB() { log.info(Thread.currentThread().getName() + "\t" + "==> testB"); return "==> testB"; } }
啓動Sentinel8080,啓動微服務8401
啓動8401微服務後臺查看sentinel控制,啥也沒有?=> Sentinel採用懶加載,執行兩次訪問http:localhost8401/testA,http:localhost8401/testB,效果以下:
名詞說明:
流控模式
直接(默認)
測試:快速點擊訪問 http://localhost:8401/testA => Blocked by Sentinel(flow limiting)
思考:直接調用了默認報錯信息,技術方面是沒問題的,可是 是否因該有咱們本身的後續處理呢?(相似有一個fallback的兜底方法),後面會講到如何配置
關聯
鏈路
流控效果
快速失敗
Warm Up(預熱)
排隊等待
名詞說明:
RT(平均響應時間,秒級)
-Dcsp.sentinel.statistic.max.rt=XXXX
才能生效)異常比例(秒級)
注意:Sentinel的斷路器是沒有半開狀態的
半開狀態:半開的狀態,系統自動去檢測是否請求有異常,沒有異常就關閉斷路器恢復使用,有異常則繼續打開斷路器不可用,具體能夠參考Hystrix。
Sentinel熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其餘的資源而致使級聯錯誤。
當資源被降級後,在接下來的降級時間窗口以內,對該資源的調用都自動熔斷(默認行爲是拋出DegradException )。
降級策略演示
RT(平均響應時間(DEGRADE_GRADE_RT
))
count
,以ms爲單位),那麼在接下來的時間窗口(DegradeRule
中的timeWindow
,以 s 爲單位)以內,對這個方法的調用都會自動的熔斷(拋出DegradeException
)。注意 Sentinel 莫仍統計的 RT 上限是 4900 ms,超出次閾值的都會算做 4900 ms,若須要變動此上限能夠經過啓動配置項 -Dcsp.sentinel.statistic.max.rt=xxx
來配置。測試
@GetMapping("/testD") public String testD() { //暫停幾秒線程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testD 測試RT"); return "==> testD"; }
異常比例(DEGRADE_GRADE_EXCEPTION_RATIO)
DegradeRule
中的count
)以後,資源進入降級狀態,即在接下來的時間窗口(DegradeRule
中的timeWindow
,以 s 爲單位)以內,對這個方法的調用都會自動的返回。異常比率的閾值範圍是 [0.0,1.0]
,表明 0% - 100%。測試
@GetMapping("/testD") public String testD() { log.info("testD 測試RT"); int age = 10 / 0; return "==> testD"; }
異常數(DEGRADE_GRADE_EXCEPTION_COUNT):
timeWindow
小於 60s,則結束熔斷狀態後仍可能再進入熔斷狀態。即 時間窗口必定要 >= 60 秒。測試
源碼
com.alibaba.csp.sentinel.slots.block.BlockException
名詞說明
何爲熱點?熱點即常常訪問的數據。不少時候咱們但願統計某個熱點數據中訪問頻次最高的 Top K 數據,並對其訪問進行限制。好比
熱點參數限流會統計傳入的熱點參數,並根據配置的限流閾值與模式,對包含熱點參數的資源調用進行限流。熱點參數限流能夠看做是一種特殊的流量控制,僅對包含熱點參數的資源調用生效。
Sentinel 利用 LRU 策略統計最近最常訪問的熱點參數,結合令牌桶算法來進行參數級別的流控。熱點參數限流支持集羣模式。
回顧 - 兜底方法
兜底方法分爲 系統默認 和 客戶自定義 兩種,以前限流出現問題後,都是用sentinel 系統默認的提示:Blocked by Sentinel (flow limiting)。
咱們能不能自定義?相似hystrix,某個方法出問題了,就找對應的兜底降級方法!
=> 在sentinel中經過 @SentinelResource
註解可以實現 (相似於hystrix的HystrixCommand
)。
/** * value: 惟一標識,對應熱點規則資源名 * blockHandler:違背了熱點規則後指向的兜底方法,若是不配置,異常就會直接打到前臺去(不友好) */ @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 e) { return "==> deal_testHotKey"; }
配置
方法testHotKey裏面第一個參數只要QPS超過每秒1次,立刻降級處理。且用了咱們自定義的兜底方法。
測試
# QPS超過每秒1次會限流 http://localhost:8401/testHotKey?p1=0 # QPS超過每秒1次會限流 http://localhost:8401/testHotKey?p1=0&p2=1 # 無限制,不會限流 http://localhost:8401/testHotKey?p2=1
參數例外項
上述案例演示了第一個參數p1,當QPS超過每秒1次馬上就會被限流。
特殊狀況:咱們指望 p1 參數當它是某個特殊值時,它的限流和平時不同,例如當 p1 的值等於 5 時,它的閾值能夠達到 200。怎樣實現呢?=> 配置參數例外項。
前提條件:熱點參數必須是 基本類型 或者 String
其餘注意點
@SentineResource 處理的是Sentinel控制檯配置的違規狀況,有blockHandler方法配置的兜底方法處理。假如程序代碼出錯如 int age = 10/0,這個是Java運行時爆出的運行時異常RunTimeException,@SentineResource 無論。
總結:@SentineResource 主管配置出錯,運行出錯該走異常仍是會走異常!
各項配置說明
系統保護規則是從應用級別的入口流量進行控制,從單臺機器的 load,CPU使用率,平均RT,入口QPS 和併發線程數等幾個維度監控應用指標,讓系統儘量跑在最大吞吐量的同時保證系統總體的穩定性。
系統保護規則是 應用總體維度的,而不是資源維度的,而且 僅對入口流量生效。入口流量指的是進入應用的流量(EntryType.IN
),好比 Web 服務器 或Dubbo 服務端接受的請求,都屬於入口流量。
系統規則支持如下的模式:
Load 自適應
(僅對 Linux/Unix-like 機器生效):系統的 load1做爲啓發指標,進行自適應系統保護。當系統 load1 超過設定的啓發值,且系統當前的併發線程數超過估算的系統容量時纔會觸發系統保護(BBR階段)。系統容量由系統的 maxQps * minRt
估算得出。設定參考值通常是 CPU cores * 2.5
。CPU usage
(1.5.0+ 版本):當系統CPU 使用率超過閾值即觸發系統保護(取值範圍 0.0 ~ 1.0),比較靈敏。平均RT
:當單臺機器上全部入口流量的平均RT達到閾值即觸發系統保護,單位是毫秒。併發線程數
:當單臺機器上全部入口流量的併發線程數達到閾值即觸發系統保護。入口QPS
:當單臺機器上全部入口流量的 QPS 達到閾值即觸發系統保護。配置全局QPS爲例:
系統規則通常少用,使用危險,一竹竿打死一船人!
按資源名稱限流 + 後續處理
啓動nacos,sentinel
修改8401pom.xml,將公共模塊也引入
<!-- 公共模塊 --> <dependency> <groupId>com.polaris</groupId> <artifactId>cloud-api-common</artifactId> <version>${project.version}</version> </dependency>
新增業務類RateLimitController,便於演示
@RestController public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource", blockHandler = "handleException") public CommonResult byResource() { return new CommonResult(200, "按資源名稱限流測試OK", new Payment(2020L, "serial001")); } public CommonResult handleException(BlockException exception) { return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服務不可用"); } }
配置流控規則:表示1秒鐘內查詢次數大於1,就跑到咱們自定義的限流處進行限流
測試:出現了咱們自定義的限流兜底方案!
發現問題:此時若是關閉服務8401會怎樣?=> Sentinel控制檯流控規則消失了?臨時存儲?
按照Url地址限流 + 後續處理
經過訪問URL來限流,會返回Sentinel自帶默認的限流處理信息
業務類增長
@GetMapping("/rateLimit/byUrl") @SentinelResource(value = "byUrl") public CommonResult byUrl() { return new CommonResult(200, "按url限流測試OK", new Payment(2020L, "serial002")); }
配置流控規則
測試:會返回Seninel自帶的限流處理結果:Blocked by Sentinel (flow limiting)
上述兜底方案面臨的問題
系統默認的方案,沒有體現咱們本身的業務需求
依照現有條件,咱們自定義的處理方法又和業務代碼耦合在一塊兒,不直觀
每一個業務方法都添加一個兜底方法,會致使代碼膨脹加重
全局統一的處理方法沒有體現
客戶自定義限流處理邏輯
建立CustomerBlockHandler類用於自定義限流處理邏輯
public class CustomerBlockHandler { public static CommonResult handlerException(BlockException exception) { return new CommonResult(4444, "按客戶自定義,global handlerException => 1"); } public static CommonResult handlerException2(BlockException exception) { return new CommonResult(4444, "按客戶自定義,global handlerException => 2"); } }
RateLimitController增長方法
@GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2") public CommonResult customerBlockHandler() { return new CommonResult(200, "按客戶自定義", new Payment(2020L, "serial003")); }
測試:出現了咱們自定義的限流兜底方案!
@SentinelResource的其餘屬性
屬性 | 做用 | 是否必須 |
---|---|---|
value | 資源名稱 | 是 |
entryType | entry類型,標記流量的方向,取值IN/OUT,默認是OUT | 否 |
blockHandler | 處理BlockException的函數名稱。函數要求: 1. 必須是 public 2.返回類型與原方法一致 3. 參數類型須要和原方法相匹配,並在最後加 BlockException 類型的參數。4. 默認需和原方法在同一個類中。若但願使用其餘類的函數,可配置 blockHandlerClass ,並指定blockHandlerClass裏面的方法。 |
否 |
blockHanderClass | 存放blockHandler的類。對應的處理函數必須static修飾,不然沒法解析,其餘要求:同blockHandler。 | 否 |
fallback | 用於在拋出異常的時候提供fallback處理邏輯。fallback函數能夠針對全部類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。函數要求:1. 返回類型與原方法一致 2. 參數類型須要和原方法相匹配,Sentinel 1.6開始,也可在方法最後加 Throwable 類型的參數。3.默認需和原方法在同一個類中。若但願使用其餘類的函數,可配置 fallbackClass ,並指定fallbackClass裏面的方法。 |
否 |
fallbackClass (1.6) | 存放fallback的類。對應的處理函數必須static修飾,不然沒法解析,其餘要求:同fallback。 | 否 |
defaultFallback | 用於通用的 fallback 邏輯。默認fallback函數能夠針對全部類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,以fallback爲準。函數要求:\1. 返回類型與原方法一致 \2. 方法參數列表爲空,或者有一個 Throwable 類型的參數。\3. 默認須要和原方法在同一個類中。若但願使用其餘類的函數,可配置 fallbackClass ,並指定 fallbackClass 裏面的方法。 |
否 |
exceptionTolgnore (1.6) | 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出。 | 否 |
exceptionsToTrace | 須要trace的異常 | Throwable |
注意:
1.6.0 以前的版本 fallback 函數只針對降級異常(
DegradeException
)進行處理,不能針對業務異常進行處理。若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出
BlockException
時只會進入blockHandler
處理邏輯。若未配置blockHandler
、fallback
和defaultFallback
,則被限流降級時會將BlockException
直接拋出。從 1.4.0 版本開始,註解方式定義資源支持自動統計業務異常,無需手動調用
Tracer.trace(ex)
來記錄業務異常。Sentinel 1.4.0 之前的版本須要自行調用Tracer.trace(ex)
來記錄業務異常。
sentinel整合Ribbon + openFeign + fallback
啓動nacos和sentinel
新建服務提供者9004/9005
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
server: port: 9004 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 # 配置Nacos地址 management: endpoints: web: exposure: include: '*'
業務類
@RestController public class PaymentController { @Value("${server.port}") private String serverPort; public static HashMap<Long, Payment> hashMap = new HashMap<>(); static { hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181")); hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182")); hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183")); } @GetMapping(value = "/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { Payment payment = hashMap.get(id); CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, payment); return result; } }
新建服務消費者84
<!--SpringCloud openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
server: port: 84 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默認8719端口,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的端口 port: 8719 #消費者將要去訪問的微服務名稱(註冊成功進nacos的微服務提供者) service-url: nacos-user-service: http://nacos-payment-provider # 激活Sentinel對Feign的支持 feign: sentinel: enabled: true
@EnableDiscoveryClient @SpringBootApplication @EnableFeignClients public class OrderNacosMain84 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain84.class, args); } }
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
@Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult<Payment> paymentSQL(Long id) { return new CommonResult<>(44444, "服務降級返回,=> PaymentFallbackService", new Payment(id, "errorSerial")); } }
@RestController @Slf4j public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; //① 沒有配置 // @SentinelResource(value = "fallback") //② fallback只負責業務異常 // @SentinelResource(value = "fallback",fallback = "handlerFallback") //③ blockHandler只負責sentinel控制檯配置違規 // @SentinelResource(value = "fallback",blockHandler = "blockHandler") @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class}) public CommonResult<Payment> fallback(@PathVariable Long id) { CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id); if (id == 4) { throw new IllegalArgumentException( "IllegalArgumentException,非法參數異常!"); } else if (result.getData() == null) { throw new NullPointerException( "NullPointerException,該ID沒有對應記錄,空指針異常"); } return result; } //本例是fallback public CommonResult handlerFallback(@PathVariable Long id, Throwable e) { Payment payment = new Payment(id, "null"); return new CommonResult<>(444, "兜底異常handlerFallback,exception內容 " + e.getMessage(), payment); } //本例是blockHandler public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) { Payment payment = new Payment(id, "null"); return new CommonResult<>(445, "blockHandler-sentinel限流,無此流水: blockException " + blockException.getMessage(), payment); } //==> OpenFeign @Resource private PaymentService paymentService; @GetMapping(value = "/consumer/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { return paymentService.paymentSQL(id); } }
測試
是什麼
一旦咱們重啓應用,Sentinel規則消失,生產環境須要將配置規則進行持久化
怎麼作
將限流規則持久進Nacos保存,只要刷新8401某個rest地址,Sentinel控制檯的流控規則就能看的到,只要Nacos裏面的配置不刪除,針對8401上的流控規則持續有效。
步驟演示
現象:咱們先在Sentinel配置一條流控規則,當重啓8401後,該流控規則就會失效消失了!
修改8401
<!--SpringCloud ailibaba sentinel-datasource-nacos 作持久化用 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
添加Nacos業務規則配置(規則便可以保存在Sentinel中,也能夠保存在Nacos中等,只要是一個持久化媒介就行)
[ { "resource":"/rateLimit/byUrl", //資源名稱 "limitApp":"default", //來源應用 "grade":1, //閾值類型,0表示線程數,1表示QPS "count":1, //單機閾值 "strategy":0, //流控模式,0表示直接,1表示關聯,2表示鏈路 "controlBehavior":0, //流控效果,0表示快速失敗,1表示Warm Up,2表示排隊等候 "clusterMode":false //是否集羣 } ]
啓動8401 刷新Sentinel發現業務規則有了
中止8401,再看sentinel,發現停機後流控規則消失了!
重啓8401,再看sentinel,屢次調用接口,屢次刷新sentinel後,發現流控規則從新出現了,持久化驗證成功!
分佈式以前
單機庫存沒有這個問題
從1:1 => 1:N => N:N,開始出現分佈式事務問題
分佈式以後
單機應用被拆分紅微服務應用,原來的三個模塊被拆分紅 三個獨立的應用,分別使用 三個獨立的數據源 ,業務操做須要調用 三個服務來完成。
此時 每一個服務內部的數據一致性由 本地 事務來保證,可是 全局 的數據一致性問題無法保證。
一句話:一次業務操做須要跨多個數據源或須要跨多個系統進行遠程調用,就會產生分佈式事務問題。
是什麼
Seata是一款開源的分佈式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分佈式事務服務。
做用
一個典型的分佈式事務過程:ID + 三組件模型
處理過程(咱們能夠用這種方式幫助理解:RM-學生,TM-班主任,TC-授課老師)
怎樣用
本地 @Transational
全局 @GlobalTransational
下載版本
修改conf目錄下的file.conf配置文件
先備份原始file.conf文件
主要修改:自定義事務組名稱 + 事務日誌存儲模式爲db + 數據庫鏈接
mysql5.7數據庫新建庫seata,建表db_store.sql在seata-server-0.9.0\seata\conf目錄裏面
修改seata-server-0.9.0\seata\conf目錄下的registry.conf目錄下的registry.conf配置文件
目的是指明註冊中心爲nacos,及修改nacos鏈接信息。
先啓動Nacos,端口號爲8848
再啓動seata-server:bin目錄下seata-server.bat
分佈式事務業務說明
這裏咱們建立三個服務,一個訂單服務,一個庫存服務,一個帳戶服務。
當用戶下單時,會在訂單服務中建立一個訂單,而後經過遠程調用庫存服務來扣減下單商品的庫存,再經過遠程調用帳戶服務來扣減用戶帳戶裏面的餘額,最後在訂單服務中修改訂單狀態爲已完成。
該操做跨越三個數據庫,有兩次遠程調用,很明顯會有分佈式事務問題。
建立業務數據庫
create database seata_order; create database seata_storage; create database seata_account;
# seata_order庫下新建t_order表 DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `int` bigint(11) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL COMMENT '用戶id', `product_id` bigint(11) DEFAULT NULL COMMENT '產品id', `count` int(11) DEFAULT NULL COMMENT '數量', `money` decimal(11, 0) DEFAULT NULL COMMENT '金額', `status` int(1) DEFAULT NULL COMMENT '訂單狀態: 0:建立中 1:已完結', PRIMARY KEY (`int`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '訂單表' ROW_FORMAT = Dynamic; # seata_storage庫下新建t_storage表 DROP TABLE IF EXISTS `t_storage`; CREATE TABLE `t_storage` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `product_id` bigint(11) DEFAULT NULL COMMENT '產品id', `total` int(11) DEFAULT NULL COMMENT '總庫存', `used` int(11) DEFAULT NULL COMMENT '已用庫存', `residue` int(11) DEFAULT NULL COMMENT '剩餘庫存', PRIMARY KEY (`int`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '庫存' ROW_FORMAT = Dynamic; INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100); # seata_account庫下新建t_account表 CREATE TABLE `t_account` ( `id` bigint(11) NOT NULL COMMENT 'id', `user_id` bigint(11) DEFAULT NULL COMMENT '用戶id', `total` decimal(10, 0) DEFAULT NULL COMMENT '總額度', `used` decimal(10, 0) DEFAULT NULL COMMENT '已用餘額', `residue` decimal(10, 0) DEFAULT NULL COMMENT '剩餘可用額度', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '帳戶表' ROW_FORMAT = Dynamic; INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
# 該sql在seata\conf目錄下的db_undo_log.sql中 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 KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
業務需求
下訂單 => 減庫存 => 扣餘額 => 改(訂單)狀態
新建訂單Order-Module seata-order-service2001
pom
<dependencies> <!--SpringCloud ailibaba 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> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency> <!--feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 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> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--mysql-connector-java--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--平常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
yml
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: #自定義事務組名稱須要與seata-server中的對應 tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://mpolaris.top:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 1234321 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
domain
//CommonResult @Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T>{ private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code,message,null); } }
//order @Data @AllArgsConstructor @NoArgsConstructor public class Order{ private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status; //訂單狀態:0:建立中;1:已完結 }
Dao
@Mapper public interface OrderDao{ //1 新建訂單 void create(Order order); //2 修改訂單狀態,從零改成1 void update(@Param("userId") Long userId, @Param("status") Integer status); }
service
public interface OrderService{ void create(Order order); }
@FeignClient(value = "seata-storage-service") public interface StorageService{ @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
@FeignClient(value = "seata-account-service") public interface AccountService{ @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
@Service @Slf4j public class OrderServiceImpl implements OrderService{ @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * 建立訂單->調用庫存服務扣減庫存->調用帳戶服務扣減帳戶餘額->修改訂單狀態 * 簡單說:下訂單->扣庫存->減餘額->改狀態 */ @Override @GlobalTransactional(name="fsp-create-order",rollbackFor=Exception.class) public void create(Order order) { log.info("==> 開始新建訂單"); //1 新建訂單 orderDao.create(order); //2 扣減庫存 log.info("==> 訂單微服務開始調用庫存,作扣減Count"); storageService.decrease(order.getProductId(),order.getCount()); log.info("==> 訂單微服務開始調用庫存,作扣減end"); //3 扣減帳戶 log.info("==> 訂單微服務開始調用帳戶,作扣減Money"); accountService.decrease(order.getUserId(),order.getMoney()); log.info("==> 訂單微服務開始調用帳戶,作扣減end"); //4 修改訂單狀態,從零到1,1表明已經完成 log.info("==> 修改訂單狀態開始"); orderDao.update(order.getUserId(),0); log.info("==> 修改訂單狀態結束"); log.info("==> 下訂單結束了,O(∩_∩)O哈哈~"); } }
controller
@RestController public class OrderController{ @Resource private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order) { orderService.create(order); return new CommonResult(200,"訂單建立成功"); } }
config
@Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean( DataSourceProxy dataSourceProxy)throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory( new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
@Configuration @MapperScan({"com.polaris.springcloud.dao"}) public class MyBatisConfig { }
主啓動
@EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消數據源的自動建立 public class SeataOrderMainApp2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class, args); } }
mapper
<mapper namespace="com.polaris.springcloud.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.polaris.springcloud.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="product_id" property="productId" jdbcType="BIGINT"/> <result column="count" property="count" jdbcType="INTEGER"/> <result column="money" property="money" jdbcType="DECIMAL"/> <result column="status" property="status" jdbcType="INTEGER"/> </resultMap> <insert id="create"> insert into t_order (id,user_id,product_id,count,money,status) values (null,#{userId},#{productId},#{count},#{money},0); </insert> <update id="update"> update t_order set status = 1 where user_id=#{userId} and status = #{status}; </update> </mapper>
將seata-server/conf目錄下的file.conf和registry.conf拷貝到項目模塊下的resource中(seata-server/conf下的是總控)
新建庫存Storage-Module seata-storage-service2002
新建帳戶Account-Module seata-account-service2003
參考訂單Module,源碼參考我的GitHub
數據庫初始狀況
正常下單
超時異常,沒加@GlobalTransactional
@Override public void decrease(Long userId, BigDecimal money) { LOGGER.info("==> account-service中扣減帳戶餘額開始"); //模擬超時異常,全局事務回滾 //暫停幾秒鐘線程 try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } accountDao.decrease(userId, money); LOGGER.info("==> account-service中扣減帳戶餘額結束"); }
超時異常,添加了@GlobalTransactional
再次理解TC/TM/RM三個組件 - 分佈式事務的執行流程:
AT模式如何作到對業務的無侵入: