第15章 分佈式事務
學習目標
-
理解什麼是事務
-
理解什麼是分佈式事務
-
理解CAP定理
CAP不能3者同時成立
-
能說出相關的分佈式事務解決方案
1.2PC-JTA分佈式事務 2.本地消息-業務庫中添加對應的消息表和業務耦合實現 3.MQ事務消息-RocketMQ 4.Seata
-
理解Seata工作流程
AT模式-表 TCC模式-代碼補償機制
-
能實現Seata案例
Seata使用案例
作業:實現項目中分佈式事務控制-下單->用戶微服務(增加積分)->Goods微服務(庫存遞減)
1 分佈式事務介紹
1.1 什麼是事務
數據庫事務(簡稱:事務,Transaction)是指數據庫執行過程中的一個邏輯單位,由一個有限的數據庫操作序列構成[由當前業務邏輯多個不同操作構成]。
事務擁有以下四個特性,習慣上被稱爲ACID特性:
原子性(Atomicity):事務作爲一個整體被執行,包含在其中的對數據庫的操作要麼全部被執行,要麼都不執行。
一致性(Consistency):事務應確保數據庫的狀態從一個一致狀態轉變爲另一個一致狀態。一致狀態是指數據庫中的數據應滿足完整性約束。除此之外,一致性還有另外一層語義,就是事務的中間狀態不能被觀察到(這層語義也有說應該屬於原子性)。
隔離性(Isolation):多個事務併發執行時,一個事務的執行不應影響其他事務的執行,如同只有這一個操作在被數據庫所執行一樣。
持久性(Durability):已被提交的事務對數據庫的修改應該永久保存在數據庫中。在事務結束時,此操作將不可逆轉。
1.2 本地事務
起初,事務僅限於對單一數據庫資源的訪問控制,架構服務化以後,事務的概念延伸到了服務中。倘若將一個單一的服務操作作爲一個事務,那麼整個服務操作只能涉及一個單一的數據庫資源,這類基於單個服務單一數據庫資源訪問的事務,被稱爲本地事務(Local Transaction)。
1.3 什麼是分佈式事務
分佈式事務指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不同的分佈式系統的不同節點之上,且屬於不同的應用,分佈式事務需要保證這些操作要麼全部成功,要麼全部失敗。本質上來說,分佈式事務就是爲了保證不同數據庫的數據一致性。
1.4 分佈式事務應用架構
本地事務主要限制在單個會話內,不涉及多個數據庫資源。但是在基於SOA(Service-Oriented Architecture,面向服務架構)的分佈式應用環境下,越來越多的應用要求對多個數據庫資源,多個服務的訪問都能納入到同一個事務當中,分佈式事務應運而生。
1.4.1 單一服務分佈式事務
最早的分佈式事務應用架構很簡單,不涉及服務間的訪問調用,僅僅是服務內操作涉及到對多個數據庫資源的訪問。
1.4.2 多服務分佈式事務
當一個服務操作訪問不同的數據庫資源,又希望對它們的訪問具有事務特性時,就需要採用分佈式事務來協調所有的事務參與者。
對於上面介紹的分佈式事務應用架構,儘管一個服務操作會訪問多個數據庫資源,但是畢竟整個事務還是控制在單一服務的內部。如果一個服務操作需要調用另外一個服務,這時的事務就需要跨越多個服務了。在這種情況下,起始於某個服務的事務在調用另外一個服務的時候,需要以某種機制流轉到另外一個服務,從而使被調用的服務訪問的資源也自動加入到該事務當中來。下圖反映了這樣一個跨越多個服務的分佈式事務:
1.4.3 多服務多數據源分佈式事務
如果將上面這兩種場景(一個服務可以調用多個數據庫資源,也可以調用其他服務)結合在一起,對此進行延伸,整個分佈式事務的參與者將會組成如下圖所示的樹形拓撲結構。在一個跨服務的分佈式事務中,事務的發起者和提交均系同一個,它可以是整個調用的客戶端,也可以是客戶端最先調用的那個服務。
較之基於單一數據庫資源訪問的本地事務,分佈式事務的應用架構更爲複雜。在不同的分佈式應用架構下,實現一個分佈式事務要考慮的問題並不完全一樣,比如對多資源的協調、事務的跨服務傳播等,實現機制也是複雜多變。
事務的作用:
保證每個事務的數據一致性。
1.5 CAP定理
CAP 定理,又被叫作布魯爾定理。對於設計分佈式系統(不僅僅是分佈式事務)的架構師來說,CAP 就是你的入門理論。
**C (一致性):**對某個指定的客戶端來說,讀操作能返回最新的寫操作。
對於數據分佈在不同節點上的數據來說,如果在某個節點更新了數據,那麼在其他節點如果都能讀取到這個最新的數據,那麼就稱爲強一致,如果有某個節點沒有讀取到,那就是分佈式不一致。
**A (可用性):**非故障的節點在合理的時間內返回合理的響應(不是錯誤和超時的響應)。可用性的兩個關鍵一個是合理的時間,一個是合理的響應。
合理的時間指的是請求不能無限被阻塞,應該在合理的時間給出返回。合理的響應指的是系統應該明確返回結果並且結果是正確的,這裏的正確指的是比如應該返回 50,而不是返回 40。
**P (分區容錯性):**當出現網絡分區後,系統能夠繼續工作。打個比方,這裏集羣有多臺機器,有臺機器網絡出現了問題,但是這個集羣仍然可以正常工作。
熟悉 CAP 的人都知道,三者不能共有,如果感興趣可以搜索 CAP 的證明,在分佈式系統中,網絡無法 100% 可靠,分區其實是一個必然現象。
如果我們選擇了 CA 而放棄了 P,那麼當發生分區現象時,爲了保證一致性,這個時候必須拒絕請求,但是 A 又不允許,所以分佈式系統理論上不可能選擇 CA 架構,只能選擇 CP 或者 AP 架構。
對於 CP 來說,放棄可用性,追求一致性和分區容錯性,我們的 ZooKeeper 其實就是追求的強一致。
對於 AP 來說,放棄一致性(這裏說的一致性是強一致性),追求分區容錯性和可用性,這是很多分佈式系統設計時的選擇,後面的 BASE 也是根據 AP 來擴展。
順便一提,CAP 理論中是忽略網絡延遲,也就是當事務提交時,從節點 A 複製到節點 B 沒有延遲,但是在現實中這個是明顯不可能的,所以總會有一定的時間是不一致。
同時 CAP 中選擇兩個,比如你選擇了 CP,並不是叫你放棄 A。因爲 P 出現的概率實在是太小了,大部分的時間你仍然需要保證 CA。
就算分區出現了你也要爲後來的 A 做準備,比如通過一些日誌的手段,是其他機器回覆至可用。
2 分佈式事務解決方案
1.XA兩段提交(低效率)-21 XA JTA分佈式事務解決方案
2.TCC三段提交(2段,高效率[不推薦(補償代碼)])
3.本地消息(MQ+Table)
4.事務消息(RocketMQ[alibaba])
5.Seata(alibaba)
2.1 基於XA協議的兩階段提交(2PC)
兩階段提交協議(Two Phase Commitment Protocol)中,涉及到兩種角色
一個事務協調者(coordinator):負責協調多個參與者進行事務投票及提交(回滾)
多個事務參與者(participants):即本地事務執行者
總共處理步驟有兩個
(1)投票階段(voting phase):協調者將通知事務參與者準備提交或取消事務,然後進入表決過程。參與者將告知協調者自己的決策:同意(事務參與者本地事務執行成功,但未提交)或取消(本地事務執行故障);
(2)提交階段(commit phase):收到參與者的通知後,協調者再向參與者發出通知,根據反饋情況決定各參與者是否要提交還是回滾;
如果所示 1-2爲第一階段,2-3爲第二階段
如果任一資源管理器在第一階段返回準備失敗,那麼事務管理器會要求所有資源管理器在第二階段執行回滾操作。通過事務管理器的兩階段協調,最終所有資源管理器要麼全部提交,要麼全部回滾,最終狀態都是一致的
官方解決方案圖例如下:
優點: 儘量保證了數據的強一致,適合對數據強一致要求很高的關鍵領域。
缺點: 犧牲了可用性,對性能影響較大,不適合高併發高性能場景,如果分佈式系統跨接口調用,目前 .NET 界還沒有實現方案。
2.2 補償事務(TCC)
TCC 將事務提交分爲 Try(method1) - Confirm(method2) - Cancel(method3) 3個操作。其和兩階段提交有點類似,Try爲第一階段,Confirm - Cancel爲第二階段,是一種應用層面侵入業務的兩階段提交。
操作方法 | 含義 |
---|---|
Try | 預留業務資源/數據效驗 |
Confirm | 確認執行業務操作,實際提交數據,不做任何業務檢查,try成功,confirm必定成功,需保證冪等 |
Cancel | 取消執行業務操作,實際回滾數據,需保證冪等 |
其核心在於將業務分爲兩個操作步驟完成。不依賴 RM 對分佈式事務的支持,而是通過對業務邏輯的分解來實現分佈式事務。
例如: A要向 B 轉賬,思路大概是:
我們有一個本地方法,裏面依次調用
1、首先在 Try 階段,要先調用遠程接口把 B和 A的錢給凍結起來。
2、在 Confirm 階段,執行遠程調用的轉賬的操作,轉賬成功進行解凍。
3、如果第2步執行成功,那麼轉賬成功,如果第二步執行失敗,則調用遠程凍結接口對應的解凍方法 (Cancel)。
假設用戶user表中有兩個字段:可用餘額(available_money)、凍結餘額(frozen_money)
A扣錢對應服務A(ServiceA)
B加錢對應服務B(ServiceB)
轉賬訂單服務(OrderService)
業務轉賬方法服務(BusinessService)
ServiceA,ServiceB,OrderService都需分別實現try(),confirm(),cancle()方法,方法對應業務邏輯如下
操作方法 | ServiceA | ServiceB | OrderService |
---|---|---|---|
try() | 校驗餘額(併發控制) 凍結餘額+1000 餘額-1000 |
凍結餘額+1000 | 創建轉賬訂單,狀態待轉賬 |
confirm() | 凍結餘額-1000 | 狀態變爲轉賬成功 | |
cancle() | 凍結餘額-1000 餘額+1000 |
狀態變爲轉賬失敗 |
其中業務調用方BusinessService中就需要調用
ServiceA.try()
ServiceB.try()
OrderService.try()
1、當所有try()方法均執行成功時,對全局事物進行提交,即由事物管理器調用每個微服務的confirm()方法
2、 當任意一個方法try()失敗(預留資源不足,抑或網絡異常,代碼異常等任何異常),由事物管理器調用每個微服務的cancle()方法對全局事務進行回滾
優點: 跟2PC比起來,實現以及流程相對簡單了一些,但數據的一致性比2PC也要差一些
缺點: 缺點還是比較明顯的,在2,3步中都有可能失敗。TCC屬於應用層的一種補償方式,所以需要程序員在實現的時候多寫很多補償的代碼,在一些場景中,一些業務流程可能用TCC不太好定義及處理。
2.3 本地消息表(異步確保)
本地消息表這種實現方式應該是業界使用最多的,其核心思想是將分佈式事務拆分成本地事務進行處理,這種思路是來源於ebay。我們可以從下面的流程圖中看出其中的一些細節:
基本思路就是:
消息生產方,需要額外建一個消息表,並記錄消息發送狀態。消息表和業務數據要在一個事務裏提交,也就是說他們要在一個數據庫裏面。然後消息會經過MQ發送到消息的消費方。如果消息發送失敗,會進行重試發送。
消息消費方,需要處理這個消息,並完成自己的業務邏輯。此時如果本地事務處理成功,表明已經處理成功了,如果處理失敗,那麼就會重試執行。如果是業務上面的失敗,可以給生產方發送一個業務補償消息,通知生產方進行回滾等操作。
生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。如果有靠譜的自動對賬補賬邏輯,這種方案還是非常實用的。
這種方案遵循BASE理論,採用的是最終一致性,筆者認爲是這幾種方案裏面比較適合實際業務場景的,即不會出現像2PC那樣複雜的實現(當調用鏈很長的時候,2PC的可用性是非常低的),也不會像TCC那樣可能出現確認或者回滾不了的情況。
優點: 一種非常經典的實現,避免了分佈式事務,實現了最終一致性。在 .NET中 有現成的解決方案。
缺點: 消息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理。
2.4 MQ 事務消息
有一些第三方的MQ是支持事務消息的,比如RocketMQ,他們支持事務消息的方式也是類似於採用的二階段提交,但是市面上一些主流的MQ都是不支持事務消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中間件爲例,其思路大致爲:
第一階段Prepared消息,會拿到消息的地址。
第二階段執行本地事務,第三階段通過第一階段拿到的地址去訪問消息,並修改狀態。
也就是說在業務方法內要想消息隊列提交兩次請求,一次發送消息和一次確認消息。如果確認消息發送失敗了RocketMQ會定期掃描消息集羣中的事務消息,這時候發現了Prepared消息,它會向消息發送者確認,所以生產方需要實現一個check接口,RocketMQ會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。
優點: 實現了最終一致性,不需要依賴本地數據庫事務。
缺點: 目前主流MQ中只有RocketMQ支持事務消息。
2.5 Seata 2PC->改進
2019 年 1 月,阿里巴巴中間件團隊發起了開源項目 Fescar(Fast & EaSy Commit And Rollback),和社區一起共建開源分佈式事務解決方案。Fescar 的願景是讓分佈式事務的使用像本地事務的使用一樣,簡單和高效,並逐步解決開發者們遇到的分佈式事務方面的所有難題。
Fescar 開源後,螞蟻金服加入 Fescar 社區參與共建,並在 Fescar 0.4.0 版本中貢獻了 TCC 模式。
爲了打造更中立、更開放、生態更加豐富的分佈式事務開源社區,經過社區核心成員的投票,大家決定對 Fescar 進行品牌升級,並更名爲 Seata,意爲:Simple Extensible Autonomous Transaction Architecture,是一套一站式分佈式事務解決方案。
Seata 融合了阿里巴巴和螞蟻金服在分佈式事務技術上的積累,並沉澱了新零售、雲計算和新金融等場景下豐富的實踐經驗。
2.5.1 Seata介紹
解決分佈式事務問題,有兩個設計初衷
對業務無侵入:即減少技術架構上的微服務化所帶來的分佈式事務問題對業務的侵入
高性能:減少分佈式事務解決方案所帶來的性能消耗
seata中有兩種分佈式事務實現方案,AT及TCC
-
AT模式主要關注多 DB 訪問的數據一致性,當然也包括多服務下的多 DB 數據訪問一致性問題
-
TCC 模式主要關注業務拆分,在按照業務橫向擴展資源時,解決微服務間調用的一致性問題
2.5.2 AT模式
Seata AT模式是基於XA事務演進而來的一個分佈式事務中間件,XA是一個基於數據庫實現的分佈式事務協議,本質上和兩階段提交一樣,需要數據庫支持,Mysql5.6以上版本支持XA協議,其他數據庫如Oracle,DB2也實現了XA接口
解釋:
Transaction Coordinator (TC): 事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾。
Transaction Manager(TM): 控制全局事務的邊界,負責開啓一個全局事務,並最終發起全局提交或全局回滾的決議。
Resource Manager (RM): 控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾。
協調執行流程如下:
Branch就是指的分佈式事務中每個獨立的本地局部事務。
第一階段
Seata 的 JDBC 數據源代理通過對業務 SQL 的解析,把業務數據在更新前後的數據鏡像組織成回滾日誌,利用 本地事務 的 ACID 特性,將業務數據的更新和回滾日誌的寫入在同一個 本地事務 中提交。
這樣,可以保證:任何提交的業務數據的更新一定有相應的回滾日誌存在
基於這樣的機制,分支的本地事務便可以在全局事務的第一階段提交,並馬上釋放本地事務鎖定的資源
這也是Seata和XA事務的不同之處,兩階段提交往往對資源的鎖定需要持續到第二階段實際的提交或者回滾操作,而有了回滾日誌之後,可以在第一階段釋放對資源的鎖定,降低了鎖範圍,提高效率,即使第二階段發生異常需要回滾,只需找對undolog中對應數據並反解析成sql來達到回滾目的
同時Seata通過代理數據源將業務sql的執行解析成undolog來與業務數據的更新同時入庫,達到了對業務無侵入的效果。
第二階段
如果決議是全局提交,此時分支事務此時已經完成提交,不需要同步協調處理(只需要異步清理回滾日誌),Phase2 可以非常快速地完成.
如果決議是全局回滾,RM 收到協調器發來的回滾請求,通過 XID 和 Branch ID 找到相應的回滾日誌記錄,通過回滾記錄生成反向的更新 SQL 並執行,以完成分支的回滾
2.5.3 TCC模式
seata也針對TCC做了適配兼容,支持TCC事務方案,原理前面已經介紹過,基本思路就是使用侵入業務上的補償及事務管理器的協調來達到全局事務的一起提交及回滾。
3 Seata案例
3.1 需求分析
完成一個案例,用戶下單的時候記錄下單日誌,完成訂單添加,完成用戶賬戶扣款,完成商品庫存削減功能,一會在任何一個微服務中製造異常,測試分佈式事務。
先將day15\seata\案例SQL腳本
數據庫腳本導入到數據庫中。
3.2 案例實現
fescar:fast easy commit and rollback
3.2.1 父工程
搭建fescar-parent,爲了適應我們暢購工程的分佈式事務,我們這裏的父工程引入和暢購工程一樣的依賴包。
pom.xml依賴如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>fescar-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<packaging>pom</packaging>
<!--跳過測試-->
<properties>
<skipTests>true</skipTests>
</properties>
<!--依賴包-->
<dependencies>
<!--測試包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!--鑑權-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--web起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis 使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<!--httpclient支持,微信支付-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--通用mapper起步依賴,MyBatis通用Mapper封裝,基於MyBatis動態SQL實現,可以實現對數據庫的操作,不需要編寫SQL語句-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
<!--MySQL數據庫驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis分頁插件,用於解決數據庫分頁實現 PageHelper.start(當前頁,每頁顯示的條數)-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
3.2.2 公共工程
將所有數據庫對應的Pojo/Feign抽取出一個公共工程fescar-api
,在該工程中導入依賴:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fescar-parent</artifactId>
<groupId>com.itheima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>
API:Model和Feign
</description>
<artifactId>fescar-api</artifactId>
</project>
將Pojo導入到工程中
3.2.3 商品微服務
創建fescar-item
微服務,在該工程中實現庫存削減。
(1)pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fescar-parent</artifactId>
<groupId>com.itheima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fescar-item</artifactId>
<dependencies>
<dependency>
<groupId>com.itheima</groupId>
<artifactId>fescar-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(2)Dao
創建com.itheima.dao.ItemInfoMapper
,代碼如下:
public interface ItemInfoMapper extends Mapper<ItemInfo> {
}
(3)Service
創建com.itheima.service.ItemInfoService
接口,並創建庫存遞減方法,代碼如下:
public interface ItemInfoService {
/**
* 庫存遞減
* @param id
* @param count
*/
void decrCount(int id, int count);
}
創建com.itheima.service.impl.ItemInfoServiceImpl
實現庫存遞減操作,代碼如下:
@Service
public class ItemInfoServiceImpl implements ItemInfoService {
@Autowired
private ItemInfoMapper itemInfoMapper;
/***
* 庫存遞減
* @param id
* @param count
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void decrCount(int id, int count) {
//查詢商品信息
ItemInfo itemInfo = itemInfoMapper.selectByPrimaryKey(id);
itemInfo.setCount(itemInfo.getCount()-count);
int dcount = itemInfoMapper.updateByPrimaryKeySelective(itemInfo);
System.out.println("庫存遞減受影響行數:"+dcount);
//異常準備回滾
//int q=10/0;
}
}
(4)Controller
創建com.itheima.controller.ItemInfoController
,代碼如下:
@RestController
@RequestMapping("/itemInfo")
@CrossOrigin
public class ItemInfoController {
@Autowired
private ItemInfoService itemInfoService;
/**
* 庫存遞減
* @param id
* @param count
* @return
*/
@PostMapping(value = "/decrCount")
public String decrCount(@RequestParam(value = "id") int id, @RequestParam(value = "count") int count){
//庫存遞減
itemInfoService.decrCount(id,count);
return "success";
}
}
(5)啓動類
創建com.itheima.ItemApplication
代碼如下:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.itheima.feign"})
@MapperScan(basePackages = {"com.itheima.dao"})
public class ItemApplication {
public static void main(String[] args) {
SpringApplication.run(ItemApplication.class,args);
}
}
(6)application.yml
創建applicatin.yml,配置如下:
server:
port: 18082
spring:
application:
name: item
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/fescar-item?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
3.2.4 用戶微服務
創建fescar-user
微服務,並引入公共工程依賴。
(1)pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fescar-parent</artifactId>
<groupId>com.itheima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fescar-user</artifactId>
<dependencies>
<dependency>
<groupId>com.itheima</groupId>
<artifactId>fescar-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(2)Dao
創建com.itheima.dao.UserInfoMapper
,代碼如下:
public interface UserInfoMapper extends Mapper<UserInfo> {
}
(3)Service
創建com.itheima.service.UserInfoService
接口,代碼如下:
public interface UserInfoService {
/***
* 賬戶金額遞減
* @param username
* @param money
*/
void decrMoney(String username, int money);
}
創建com.itheima.service.impl.UserInfoServiceImpl
實現用戶賬戶扣款,代碼如下:
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
/***
* 賬戶金額遞減
* @param username
* @param money
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void decrMoney(String username, int money) {
UserInfo userInfo = userInfoMapper.selectByPrimaryKey(username);
userInfo.setMoney(userInfo.getMoney()-money);
int count = userInfoMapper.updateByPrimaryKeySelective(userInfo);
System.out.println("添加用戶受影響行數:"+count);
//int q=10/0;
}
}
(4)Controller
創建com.itheima.controller.UserInfoController
代碼如下:
@RestController
@RequestMapping("/userInfo")
@CrossOrigin
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
/***
* 賬戶餘額遞減
* @param username
* @param money
*/
@PostMapping(value = "/add")
public String decrMoney(@RequestParam(value = "username") String username, @RequestParam(value = "money") int money){
userInfoService.decrMoney(username,money);
return "success";
}
}
(5)啓動類
創建com.itheima.UserApplication
,代碼如下:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.itheima.feign"})
@MapperScan(basePackages = {"com.itheima.dao"})
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class,args);
}
}
(6)application.yml
創建application.yml配置如下:
server:
port: 18084
spring:
application:
name: user
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/fescar-user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
3.2.5 訂單微服務
在訂單微服務中實現調用商品微服務遞減庫存。
(1)pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fescar-parent</artifactId>
<groupId>com.itheima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fescar-order</artifactId>
<dependencies>
<dependency>
<groupId>com.itheima</groupId>
<artifactId>fescar-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(2)Dao
創建com.itheima.dao.OrderInfoMapper
,代碼如下:
public interface OrderInfoMapper extends Mapper<OrderInfo> {
}
(3)Service
創建com.itheima.service.OrderInfoService
實現添加訂單操作,代碼如下:
public interface OrderInfoService {
/***
* 添加訂單
* @param username
* @param id
* @param count
*/
void add(String username, int id, int count);
}
創建com.itheima.service.impl.OrderInfoServiceImpl
,代碼如下:
@Service
public class OrderInfoServiceImpl implements OrderInfoService {
@Autowired
private OrderInfoMapper orderInfoMapper;
@Autowired
private ItemInfoFeign itemInfoFeign;
/***
* 添加訂單
* @param username
* @param id
* @param count
*/
@Override
public void add(String username, int id, int count) {
//添加訂單
OrderInfo orderInfo = new OrderInfo();
orderInfo.setMessage("生成訂單");
orderInfo.setMoney(10);
int icount = orderInfoMapper.insertSelective(orderInfo);
System.out.println("添加訂單受影響函數:"+icount);
//遞減庫存
itemInfoFeign.decrCount(id,count);
}
}
(3)Controller
創建com.itheima.controller.OrderInfoController
調用下單操作,代碼如下:
@RestController
@RequestMapping("/orderInfo")
@CrossOrigin
public class OrderInfoController {
@Autowired
private OrderInfoService orderInfoService;
/**
* 增加訂單
* @param username
* @param id
* @param count
*/
@PostMapping(value = "/add")
public String add(@RequestParam(value = "name") String username, @RequestParam(value = "id") int id, @RequestParam(value = "count") int count){
//添加訂單
orderInfoService.add(username,id,count);
return "success";
}
}
(4)啓動類
創建com.itheima.OrderApplication
啓動類,代碼如下:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.itheima.feign"})
@MapperScan(basePackages = {"com.itheima.dao"})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
(5)application.yml配置
server:
port: 18083
spring:
application:
name: order
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/fescar-order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
3.2.6 業務微服務
創建fescar-business
業務微服務,在該微服務中實現分佈式事務控制,下單入口從這裏開始。
(1)pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fescar-parent</artifactId>
<groupId>com.itheima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>分佈式事務業務控制</description>
<artifactId>fescar-business</artifactId>
<dependencies>
<dependency>
<groupId>com.itheima</groupId>
<artifactId>fescar-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(2)Dao
創建com.itheima.dao.LogInfoMapper
代碼如下:
public interface LogInfoMapper extends Mapper<LogInfo> {
}
(3)Service
創建com.itheima.service.BusinessService
接口,代碼如下:
public interface BusinessService {
/**
* 下單
* @param username
* @param id
* @param count
*/
void add(String username, int id, int count);
}
創建com.itheima.service.impl.BusinessServiceImpl
,代碼如下:
@Service
public class BusinessServiceImpl implements BusinessService {
@Autowired
private OrderInfoFeign orderInfoFeign;
@Autowired
private UserInfoFeign userInfoFeign;
@Autowired
private LogInfoMapper logInfoMapper;
/***
* 下單
* @param username
* @param id
* @param count
*/
@Override
public void add(String username, int id, int count) {
//添加訂單日誌
LogInfo logInfo = new LogInfo();
logInfo.setContent("添加訂單數據---"+new Date());
logInfo.setCreatetime(new Date());
int logcount = logInfoMapper.insertSelective(logInfo);
System.out.println("添加日誌受影響行數:"+logcount);
//添加訂單
orderInfoFeign.add(username,id,count);
//用戶賬戶餘額遞減
userInfoFeign.decrMoney(username,10);
}
}
(4)Controller
創建com.itheima.controller.BusinessController
,代碼如下:
@RestController
@RequestMapping(value = "/business")
public class BusinessController {
@Autowired
private BusinessService businessService;
/***
* 購買商品分佈式事務測試
* @return
*/
@RequestMapping(value = "/addorder")
public String order(){
String username="zhangsan";
int id=1;
int count=5;
//下單
businessService.add(username,id,count);
return "success";
}
}
(5)啓動類
創建啓動類com.itheima.BusinessApplication
,代碼如下:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.itheima.feign"})
@MapperScan(basePackages = {"com.itheima.dao"})
public class BusinessApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessApplication.class,args);
}
}
(6)application.yml配置
server:
port: 18081
spring:
application:
name: business
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/fescar-business?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#讀取超時設置
ribbon:
ReadTimeout: 30000
#hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
3.3 分佈式事務抽取
上面案例,並沒有實現分佈式事務,在我們以後工作中,也並非每個服務都需要實現分佈式事務,我們可以將分佈式事務抽取出來。
3.3.1 分佈式事務工程抽取搭建
創建fescar-transaction
微服務工程,在該工程中實現分佈式事務控制。
(1)pom.xml依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fescar-parent</artifactId>
<groupId>com.itheima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>fescar分佈式事務微服務</description>
<artifactId>fescar-transaction</artifactId>
<properties>
<fescar.version>0.4.2</fescar.version>
</properties>
<dependencies>
<!--fescar依賴包-->
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-tm</artifactId>
<version>${fescar.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-spring</artifactId>
<version>${fescar.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
</dependencies>
</project>
(2)資源文件導入
將seata\資源包
中的文件導入到該工程中。
相關概念講解
XID:全局事務的唯一標識,由 ip:port:sequence 組成;
Transaction Coordinator (TC):事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾;
Transaction Manager (TM ):控制全局事務的邊界,負責開啓一個全局事務,並最終發起全局提交或全局回滾的決議;
Resource Manager (RM):控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾;
Fescar 使用 XID 表示一個分佈式事務,XID 需要在一次分佈式事務請求所涉的系統中進行傳遞,從而向 feacar-server 發送分支事務的處理情況,以及接收 feacar-server 的 commit、rollback 指令。
3.3.2 配置講解
fescar 的配置入口文件是 registry.conf, 查看代碼 ConfigurationFactory 得知目前還不能指定該配置文件,所以配置文件名稱只能爲 registry.conf。
在 registry
中可以指定具體配置的形式,默認使用 file 類型,在 file.conf 中有 3 部分配置內容:
transport transport :用於定義 Netty 相關的參數,TM、RM 與 fescar-server 之間使用 Netty 進行通信。
service:
service {
#vgroup->rgroup
vgroup_mapping.my_test_tx_group = "default"
#only support single node 配置Client連接TC的地址
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#是否啓用Seata分佈式事務
disableGlobalTransaction = false
}
client:
client {
#RM接收TC的commit通知緩衝上限
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
fescar 在 AT 模式下需要創建數據庫代理.在com.itheima.fescar.FescarAutoConfiguration
中代碼如下:
/***
* 創建代理數據庫
* 會將und_log綁定到本地事務中
* @param environment
* @return
*/
@Bean
public DataSource dataSource(Environment environment){
//創建數據源對象
DruidDataSource dataSource = new DruidDataSource();
//獲取數據源鏈接地址
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
try {
//設置數據庫驅動
dataSource.setDriver(DriverManager.getDriver(environment.getProperty("spring.datasource.url")));
} catch (SQLException e) {
throw new RuntimeException("無法識別驅動類型");
}
//獲取數據庫名字
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
//獲取數據庫密碼
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
//將數據庫封裝成一個代理數據庫
return new DataSourceProxy(dataSource);
}
/***
* 全局事務掃描器
* 用來解析帶有@GlobalTransactional註解的方法,然後採用AOP的機制控制事務
* @param environment
* @return
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(Environment environment){
//事務分組名稱
String applicationName = environment.getProperty("spring.application.name");
String groupName = environment.getProperty("fescar.group.name");
if(applicationName == null){
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName);
}else{
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName);
}
}
使用 DataSourceProxy
的目的是爲了引入 ConnectionProxy
,fescar 無侵入的一方面就體現在 ConnectionProxy
的實現上,即分支事務加入全局事務的切入點是在本地事務的 commit
階段,這樣設計可以保證業務數據與 undo_log
是在一個本地事務中。
undo_log
是需要在業務庫上創建的一個表,fescar 依賴該表記錄每筆分支事務的狀態及二階段 rollback
的回放數據。不用擔心該表的數據量過大形成單點問題,在全局事務 commit
的場景下事務對應的 undo_log
會異步刪除。
所以在每個微服務對應的數據庫中需要創建一張undo_log表。
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) 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`),
KEY `idx_unionkey` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8;
3.3.3 測試分佈式事務
(1)添加依賴
將上面所有工程都添加fescar-transaction
的依賴。
<!--分佈式事務模塊-->
<dependency>
<groupId>com.itheima</groupId>
<artifactId>fescar-transaction</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(2)啓動
解壓seata\fescar-server-0.4.2.zip
文件包,並點擊bin\fescar-server.bat
啓動Seata的事務協調器。
(3)重新啓動
在業務微服務上添加@GlobalTransactional(name = "add")
註解,並重新啓動其他微服務,測試測試數據正確。
4 分佈式事務實戰(學員完成)
4.1 undolog表結構導入
核心在於對業務sql進行解析,轉換成undolog,所以只要支持Fescar分佈式事務的微服務數據都需要導入該表結構,我們在每個微服務的數據庫中都導入下面表結構:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) 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`),
KEY `idx_unionkey` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8;
4.2 Fescar工程搭建
如上圖,在所有微服務工程中,不一定所有工程都需要使用分佈式事務,我們可以創建一個獨立的分佈式事務工程,指定微服務需要支持分佈式事務的時候,直接依賴獨立的分佈式工程即可。
搭建一個changgou-transaction-fescar提供fescar分佈式事務支持。
4.2.1 pom.xml依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>changgou_parent</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou_transaction_fescar</artifactId>
<properties>
<fescar.version>0.4.2</fescar.version>
</properties>
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-tm</artifactId>
<version>${fescar.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-spring</artifactId>
<version>${fescar.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
</dependencies>
</project>
4.2.2 引入配置
將fescar配置文件
文件夾中的所有配置文件拷貝到resources工程下,如下圖:
其中file.conf有2個配置
service.vgroup_mapping.my_test_tx_group 映射到相應的 Fescar-Server 集羣名稱,然後再根據集羣名稱.grouplist 獲取到可用服務列表。
4.3 TM和ProxyDataSource
核心在於對業務sql進行解析,轉換成undolog,並同時入庫,此時需要創建一個代理數據源,用代理數據源來實現。
要想實現全局事務管理器,需要添加一個@GlobalTransactional註解
,該註解需要創建一個解析器,GlobalTransactionScanner
,它是一個全局事務掃描器,用來解析帶有@GlobalTransactional註解的方法,然後採用AOP的機制控制事務。
每次微服務和微服務之間相互調用,要想控制全局事務,每次TM都會請求TC生成一個XID,每次執行下一個事務,也就是調用其他微服務的時候都需要將該XID傳遞過去,所以我們可以每次請求的時候,都獲取頭中的XID,並將XID傳遞到下一個微服務。
4.3.1 TM和ProxyDataSource實現
創建FescarAutoConfiguration類,代碼如下:
@Configuration
public class FescarAutoConfiguration {
public static final String FESCAR_XID = "fescarXID";
/***
* 創建代理數據庫
* @param environment
* @return
*/
@Bean
public DataSource dataSource(Environment environment){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
try {
dataSource.setDriver(DriverManager.getDriver(environment.getProperty("spring.datasource.url")));
} catch (SQLException e) {
throw new RuntimeException("can't recognize dataSource Driver");
}
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
return new DataSourceProxy(dataSource);
}
/***
* 全局事務掃描器
* 用來解析帶有@GlobalTransactional註解的方法,然後採用AOP的機制控制事務
* @param environment
* @return
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(Environment environment){
String applicationName = environment.getProperty("spring.application.name");
String groupName = environment.getProperty("fescar.group.name");
if(applicationName == null){
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName);
}else{
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName);
}
}
/***
* 每次微服務和微服務之間相互調用
* 要想控制全局事務,每次TM都會請求TC生成一個XID,每次執行下一個事務,也就是調用其他微服務的時候都需要將該XID傳遞過去
* 所以我們可以每次請求的時候,都獲取頭中的XID,並將XID傳遞到下一個微服務
* @param restTemplates
* @return
*/
@ConditionalOnBean({RestTemplate.class})
@Bean
public Object addFescarInterceptor(Collection<RestTemplate> restTemplates){
restTemplates.stream()
.forEach(restTemplate -> {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if(interceptors != null){
interceptors.add(fescarRestInterceptor());
}
});
return new Object();
}
@Bean
public FescarRMRequestFilter fescarRMRequestFilter(){
return new FescarRMRequestFilter();
}
@Bean
public FescarRestInterceptor fescarRestInterceptor(){
return new FescarRestInterceptor();
}
}
注意:如果自定義fescar.group.name需要和file.conf中的名字保持一致。
創建FescarRMRequestFilter,給每個線程綁定一個XID,代碼如下;
public class FescarRMRequestFilter extends OncePerRequestFilter {
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(FescarRMRequestFilter.class);
/**
* 給每次線程請求綁定一個XID
* @param request
* @param response
* @param filterChain
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String currentXID = request.getHeader(FescarAutoConfiguration.FESCAR_XID);
if(!StringUtils.isEmpty(currentXID)){
RootContext.bind(currentXID);
LOGGER.info("當前線程綁定的XID :" + currentXID);
}
try{
filterChain.doFilter(request, response);
} finally {
String unbindXID = RootContext.unbind();
if(unbindXID != null){
LOGGER.info("當前線程從指定XID中解綁 XID :" + unbindXID);
if(!currentXID.equals(unbindXID)){
LOGGER.info("當前線程的XID發生變更");
}
}
if(currentXID != null){
LOGGER.info("當前線程的XID發生變更");
}
}
}
}
創建FescarRestInterceptor過濾器,每次請求其他微服務的時候,都將XID攜帶過去。
public class FescarRestInterceptor implements RequestInterceptor, ClientHttpRequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String xid = RootContext.getXID();
if(!StringUtils.isEmpty(xid)){
requestTemplate.header(FescarAutoConfiguration.FESCAR_XID, xid);
}
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String xid = RootContext.getXID();
if(!StringUtils.isEmpty(xid)){
HttpHeaders headers = request.getHeaders();
headers.put(FescarAutoConfiguration.FESCAR_XID, Collections.singletonList(xid));
}
return execution.execute(request, body);
}
}
4.4 分佈式事務測試
4.4.1 微服務添加依賴
因爲所有微服務都有可能使用分佈式事務,所以我們可以在每個微服務工程中添加fescar的依賴,當然,搜索工程排除,因爲它不需要依賴數據庫,代碼如下:
<!--fescar依賴-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-transaction-fescar</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
4.4.2 測試
在訂單微服務的OrderServiceImpl的add方法上增加@GlobalTransactional(name = “add”)註解,代碼如下:
這裏涉及到幾個微服務的調用,我們先查詢下數據庫數據,然後再測試一次,如果輸出添加訂單完成
和庫存減少完畢
則表明訂單微服務和商品微服務的事務已經完成,這時候我們在添加積分的方法中製造一個異常,如果積分添加異常,而商品微服務中數據沒發生變化,則表明分佈式事務控制成功。
修改用戶微服務,在添加用戶積分的地方製造異常,代碼如下:
啓動Fescar-server,打開seata包/fescar-server-0.4.2/bin,雙擊fescar-server.bat啓動fescar-server,如下:
測試前後結果一致