Fescar是阿里18年開源的分布式事務的框架。Fescar的開源對分布式事務框架領域影響很大。作為開源大戶,Fescar來自阿里的GTS,經歷了好幾次雙十一的考驗,一經開源便頗受關注。今天就來看了Fescar的代碼,看看到底是怎么一回事。
Fescar與XA兩階段提交
在XA協議中分為兩階段:
第一階段:事務管理器要求每個涉及到事務的數據庫預提交(precommit)此操作,並反映是否可以提交.
第二階段:事務協調器要求每個數據庫提交數據,或者回滾數據。
優點: 盡量保證了數據的強一致,實現成本較低,在各大主流數據庫都有自己實現,對於MySQL是從5.5開始支持。
缺點
1、同步阻塞問題。執行過程中,所有參與節點都是事務阻塞型的。當參與者占有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。
2、單點故障。由於協調者的重要性,一旦協調者發生故障。參與者會一直阻塞下去。尤其在第二階段,協調者發生故障,那么所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。(如果是協調者掛掉,可以重新選舉一個協調者,但是無法解決因為協調者宕機導致的參與者處於阻塞狀態的問題)
3、數據不一致。在二階段提交的階段二中,當協調者向參與者發送commit請求之后,發生了局部網絡異常或者在發送commit請求過程中協調者發生了故障,這回導致只有一部分參與者接受到了commit請求。而在這部分參與者接到commit請求之后就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交。於是整個分布式系統便出現了數據部一致性的現象。
4、二階段無法解決的問題:協調者再發出commit消息之后宕機,而唯一接收到這條消息的參與者同時也宕機了。那么即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。
Fescar雖然是二階段提交協議的分布式事務,但是其解決了上面XA的一些缺點:
- 單點問題:雖然目前Fescar(0.4.1)還是單server的,但是Fescar官方預計將會在0.5.x中推出HA-Cluster,到時候就可以解決單點問題。
- 同步阻塞:Fescar的二階段,其再第一階段的時候本地事務就已經提交釋放資源了,不會像XA會再兩個prepare和commit階段資源都鎖住,並且Fescar,commit是異步操作,也是提升性能的一大關鍵。
- 數據不一致:如果出現部分commit失敗,那么fescar-server會根據當前的事務模式和分支事務的返回狀態的結果來進行不同的重試策略。並且fescar的本地事務會在一階段的時候進行提交,其實單看數據庫來說在commit的時候數據庫已經是一致的了。
- 只能用於單一數據庫: Fescar提供了兩種模式,AT和MT。在AT模式下事務資源可以是任何支持ACID的數據庫,在MT模式下事務資源沒有限制,可以是緩存,可以是文件,可以是其他的等等。當然這兩個模式也可以混用。
同時Fescar也保留了接近0業務入侵的優點,只需要簡單的配置Fescar的數據代理和加個注解,加一個Undolog表,就可以達到我們想要的目的。
Fescar整合springcloud
這里直接下載 fescar-samples
項目的readme寫的很詳細
准備工作 執行sql/all_in_one.sql 下載0.4.1版本server 客戶端與服務端版本號保持一致 啟動fescar server sh fescar-server.sh 8091 ../data/ 啟動business、storage、account、order 數據庫默認連接127.0.0.1:3306,不同的注意修改 事務成功 GET http://127.0.0.1:8084/purchase/commit 事務回滾 GET http://127.0.0.1:8084/purchase/rollback
看了下項目的依賴和配置文件,和常規的項目相比,多了下面幾個依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-fescar</artifactId> <version>2.1.0.BUILD-SNAPSHOT</version> <exclusions> <exclusion> <groupId>com.alibaba.fescar</groupId> <artifactId>fescar-spring</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba.fescar</groupId> <artifactId>fescar-spring</artifactId> <version>0.4.1</version> </dependency>
啟動相應的項目,觀察相應的數據變化,這個demo算是跑起來
Fescar的實現
Fescar將一個本地事務做為一個分布式事務分支,所以若干個分布在不同微服務中的本地事務共同組成了一個全局事務,結構如下。
事務發起
觀察整個demo項目,唯一不同的就是多了個注解 @GlobalTransactional 所以我先從這個注解入手
在 fescar-spring 包中發現了 com.alibaba.fescar.spring.annotation.GlobalTransactionalInterceptor這個類,這個類實現了org.aopalliance.intercept.MethodInterceptor
已動態代理的方式提供了對GlobalTransactional 注解方法的支持
最終我們的邏輯會到 com.alibaba.fescar.tm.api.TransactionalTemplate#execute方法中
execute方法是事務一階段的核心,主要做了以下幾件事情
1.構造一個全局事務
2.開始事務
通過 TmRpcClient 請求TC server端,注冊一個全局的事物。得到一個全局事務id:XID,將這個XID設置到上下文中
3.執行原始的業務代碼
在springcloud中我們用feign去請求其他服務,fescar對feign進行了重寫,在org.springframework.cloud.alibaba.fescar.feign.FescarFeignClient中fescar對每個feign的請求都會做一次判斷,如果在全局上下文中含有事務id,feign的請求頭會帶上XID
4.事務的回滾或者提交
提交和回滾本質上都是構造一個請求,請求遠程的TC server端
5.清除ThreadLocal中的鈎子函數,我們暫時還沒用到。
事務參與
參與者在收到feign請求時,首先會被org.springframework.cloud.alibaba.fescar.web.FescarHandlerInterceptor攔截,攔截器會把請求中的XID設置到本地的全局上下文中
然后參與者就開始執行本地的業務。fesca在這里做了大量的背后工作。為了能在jdbc連接中添加了自己的邏輯,fesca重寫了Statment之前的邏輯
JdbcTemplate數據源被配置成了Fescar實現DataSourceProxy,進而控制了后續的數據庫連接使用的是Fescar提供的ConnectionProxy,Statment使用的是Fescar實現的StatmentProxy,最終Fescar就順理成章地實現了在本地事務執行前后增加所需要的邏輯
獲取代理數據源
獲取代理連接
獲取代理statement
在PreparedStatementProxy中,真正的執行sql的邏輯被放在了ExecuteTemplate中
根據sqltype,這里有多個executor,感覺和mybatis的代碼風格有點相似
這里我們已UpdateExecutor為例
executor.execute(args);
這行代碼入口在com.alibaba.fescar.rm.datasource.exec.BaseTransactionalExecutor#execute
將ConnectionProxy與Xid(事務ID)進行綁定,這樣后續判斷當前本地事務是否處理全局事務中只需要看ConnectionProxy中Xid是否為空
再執行com.alibaba.fescar.rm.datasource.exec.AbstractDMLBaseExecutor#doExecute
這里會判斷是否是自動提交
如果是自動提交,就會先設置為為非自動提交再執行executeAutoCommitFalse
這里 executeAutoCommitFalse方法是整個邏輯的核心
先查詢Update前對應行記錄的快照beforeImage,再執行Update語句,完成后再查詢Update后對應行記錄的快照afterImage,最后將beforeImage、afterImage生成UndoLog追加到Connection上下文ConnectionContext中
如果一切順利,最終我們會執行本地事務的提交,即執行com.alibaba.fescar.rm.datasource.ConnectionProxy#commit方法,最終邏輯會到com.alibaba.fescar.rm.datasource.ConnectionProxy#processGlobalTransactionCommit
邏輯如下
1.注冊分支事務到TC server端
2再將ConnectionContext中的UndoLog寫入到undo_log表中
3然后調用targetConnection對本地事務進行commit,將UndoLog與業務SQL一起提交
4最后上報分支事務的狀態(成功 or 失敗),並將ConnectionContext上下文重置
事務的參與者便完成了自己的邏輯。二階段中的一階段的邏輯便是上述代碼,這里再引用一張官方的流程圖