分布式事務:從理論到實踐(二)

2020-10-30 小盒子的技術分享

前文我們提到了Seata的AT和TCC模式,本文中我們針對這兩個模式進行深入分析和開發實踐。

AT 模式

原理回顧


根據 官方文檔 及提供的 博客 我們先回顧一下AT模式下分布式事務的原理


AT 模式的一階段、二階段提交和回滾均由 Seata 框架自動生成,用戶只需編寫「業務 SQL」,便能輕鬆接入分布式事務,AT 模式是一種對業務無任何侵入的分布式事務解決方案。



  • 一階段:
    在一階段,Seata 會攔截「業務 SQL」,首先解析 SQL 語義,找到「業務 SQL」要更新的業務數據,在業務數據被更新前,將其保存成「before image」,然後執行「業務 SQL」更新業務數據,在業務數據更新之後,再將其保存成「after image」,最後生成行鎖。以上操作全部在一個資料庫事務內完成,這樣保證了一階段操作的原子性。
  • 二階段提交:
    二階段如果是提交的話,因為「業務 SQL」在一階段已經提交至資料庫, 所以 Seata 框架只需將一階段保存的快照數據和行鎖刪掉,完成數據清理即可。



  • 二階段回滾:
    二階段如果是回滾的話,Seata 就需要回滾一階段已經執行的「業務 SQL」,還原業務數據。回滾方式便是用「before image」還原業務數據;但在還原前要首先要校驗髒寫,對比「資料庫當前業務數據」和 「after image」,如果兩份數據完全一致就說明沒有髒寫,可以還原業務數據,如果不一致就說明有髒寫,出現髒寫就需要轉人工處理。



環境搭建


本文demo使用的環境是基於


  • SpringBoot
  • Spring Cloud Alibaba
  • Nacos
  • Apollo
  • docker compose


首先將 seata-server 在伺服器搭建起來,由於我們使用 nacos作為seata的註冊中心、apollo為註冊中心,所以先將這兩個組件搭建起來,具體的安裝方法請分別參考各自的官方文檔。 nacos apollo


nacos 和 apollo 搭起來以後,我們開始搭建 seata-server 以下是 docker-compose 的配置:


version: "3.1"services: seata-server: image: seataio/seata-server:latest hostname: seata-server ports: - 8091:8091 environment: - SEATA_PORT=8091 - SEATA_IP={你的IP} - SEATA_CONFIG_NAME=file:/seata-server/resources/registry volumes: - ./seata/registry.conf:/seata-server/resources/registry.conf expose: - 8091


修改 registry.conf 配置文件,由於我們使用 nacos 作為註冊中心,apollo 作為配置中心,所以需要修改到以下配置:


registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "你的IP:埠" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" }}config { # file、nacos 、apollo、zk、consul、etcd3 type = "apollo" apollo { appId = "seata-server" apolloMeta = "http://你的IP:埠" namespace = "application" env= "dev" apolloAccesskeySecret = "" } }


注意:seata-server 是可以配置資料庫存儲 seata 所用數據的,我們為了方便利用本地 file 的方式存儲數據,所以沒有再做資料庫的配置。如需修改可以修改配置文件 file.conf


下面是 file.conf 的默認配置:


store { ## store mode: file、db、redis mode = "file" ## file store property file { ## store location dir dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions maxBranchSessionSize = 16384 # globe session size , if exceeded throws exceptions maxGlobalSessionSize = 512 # file buffer size , if exceeded allocate new buffer fileWriteBufferCacheSize = 16384 # when recover batch read size sessionReloadReadSize = 100 # async, sync flushDiskMode = async } ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "mysql" password = "mysql" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } ## redis store property redis { host = "127.0.0.1" port = "6379" password = "" database = "0" minConn = 1 maxConn = 10 maxTotal = 100 queryLimit = 100 }}


啟動 nacos、apollo、seata-server


當顯示以下信息時,代表seata-server啟動了。


這時我們查看 nacos ,也註冊上了


apollo中我們添加一個名為 service.vgroup-mapping.demo-service-seata的key ,value為 default,至於這個的作用,我們後面再說。


我們的 demo 中包含三個服務


  • demo-order
  • demo-storage
  • demo-user


服務間調用使用的是Spring Cloud OpenFeign,除了 SpringBoot 和Spring Cloud 等基礎 bom 要依賴外,還需要加入 seata 的依賴,我的pom,大致如下:


<properties> <spring-boot-dependencies.version>2.3.2.RELEASE</spring-boot-dependencies.version> <spring-cloud-dependencies.version>Hoxton.SR8</spring-cloud-dependencies.version> <spring-cloud-alibaba-dependencies.version>2.2.3.RELEASE</spring-cloud-alibaba-dependencies.version></properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring-boot-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 實現對 Spring MVC 的自動化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 引入 Spring Cloud Alibaba Seata 相關依賴,使用 Seata 實現分布式事務,並實現對其的自動配置 --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!-- 引入 Spring Cloud Alibaba Nacos Discovery 相關依賴,將 Nacos 作為註冊中心,並實現對其的自動配置 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 引入 Spring Cloud OpenFeign 相關依賴,使用 OpenFeign 提供聲明式調用,並實現對其的自動配置 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>


至於項目中所用ORM框架,資料庫連接池等就因人而異了,我用的是mybatis-plus和hikari,資料庫用的是 mysql5.7。


針對上面的三個服務分別創建三個資料庫,order、user、storage,並在每個庫中分別創建一個業務表 t_order、t_user、t_storage 這裡就不貼建庫表的腳本了,大家可以按照自己的設計自己建,需要注意的是每個庫都需要再創建一個 undo_log 表,這是為seata做分布式事務回滾所用。


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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


每個服務中 application.yml 中對應 seata 的配置如下


spring: profiles: active: dev cloud: nacos: discovery: namespace: public password: nacos server-addr: IP:PORT networkInterface: eth1 username: nacos# Seata 配置項,對應 SeataProperties 類seata: application-id: ${spring.application.name} # Seata 應用編號,默認為 ${spring.application.name} tx-service-group: demo-service-seata # Seata 事務組編號,用於 TC 集群名 # Seata 服務配置項,對應 ServiceProperties 類 service: # 虛擬組和分組的映射 vgroup-mapping: demo-service-seata: default # Seata 註冊中心配置項,對應 RegistryProperties 類 registry: type: nacos # 註冊中心類型,默認為 file nacos: cluster: default # 使用的 Seata 分組 namespace: # Nacos 命名空間 serverAddr: 你的IP:埠 # Nacos 服務地址


這裡有幾點需要注意:


  • demo-service-seata 出現了兩次,這兩個地方要寫成一樣
  • demo-service-seata: default
    • 與我們在 apollo 中配置的要一樣
    • 與 seata-server registry.conf 中 nacos 的 cluster 配置一樣。
  • nacos 配置 networkInterface: eth1
    • 這樣寫是因為服務部署在伺服器後用的內網IP註冊到了nacos,想配置它用外網地址就改了下走特定網卡。
    • 解決方案參考:這裡
      例如,使用了Spring cloud alibaba(官方文檔)作為Nacos客戶端,服務默認獲取了內網IP 192.168.1.21,可以通過配置 spring.cloud.inetutils.preferred-networks=10.34.12,使服務獲取內網中前綴為10.34.12的IP
  • 在老版本的 seata 是需要手動設置 DataSourceProxy的 ,參考 官網文檔 新版本的默認是自動代理的,不需要再寫了。


至此我們的環境搭建和準備工作就結束了。


分布式事務具體代碼


我們設計這樣一個同步的業務流程,創建訂單前先扣減庫存,再扣減帳戶餘額,然後再創建訂單,demo設計上參考了 芋道源碼。大致流程如下圖:



通過入口進入orderServicer後,進行上面的三步流程,分別調用兩個微服務,再調自己的訂單服務,這裡注意兩點:


  • 分布式全局事務入口,要添加 @GlobalTransactional
  • 要拋出異常


接下來是扣減庫存微服務部分,簡單做了下扣減,小於10拋出異常


然後是帳戶微服務部分


最後是訂單


代碼都比較簡單,有幾個點需要注意下


  • 全局事務的隔離性和本地事務的不是一個概念。
  • 全局事務的隔離級別一定基礎上依賴本地事務的隔離級別。因此本地事務的隔離級別只要大於等於seata支持的隔離級別就行,所以一般資料庫的默認級別就可以
  • seata的全局事務註解是@GlobalTransactional,@Transactional 是spring的註解,解決本地事務問題,屬於兩種不同粒度的事務範疇。
  • 如果要加全局事務就一定要用 @GlobalTransactional。
  • 在一個事務方法上,是可以疊加兩個註解的,僅意味著功能的疊加,即:有本地事務的處理,也有全局事務的加持。兩者不衝突。


由於在資料庫本地事務隔離級別 已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是 未提交(Read Uncommitted)


所以這種隔離性會帶來問題(注意這裡說的是全局事務):


  • 髒讀: 一個事務讀取到另一個事務未提交的數據
    解決方案:
    • @GlobalLock+@Transactional 註解 + select語句加for update 或
    • GlobalTransactional註解+select語句加for update
  • 髒寫: 一個事務提交的數據覆蓋了另一個事務未提交的數據
    解決方案:必須使用@GlobalTransaction


其實上面這部分,官方文檔也寫的很清楚,尤其對於隔離性的解析:


上圖有些地方理解起來要注意:


  • 這裡說的事務指的是全局的分布式事務,別想成本地事務了,
  • 關於@GlobalLock,場景是一個是全局分布式事務,另一個不是分布式事務,如果你想讓分布式事務不產生「髒讀」,那麼可以在另一個非分布式事務上加@GlobalLock。


我的測試中事務的正常執行和回滾都沒有問題,如果你觀察各資料庫的 undo_log 表,可能會發現沒有數據,但實際情況是數據是插入後又很快清除了,所以你沒看到,如果你觀察主鍵的 auto_increment 可以看到一直在增長。由於我用了阿里雲的RDS,可以通過SQL洞察看到SQL的執行歷史,這裡看到sql確實執行過。


XID是全局事務ID,有時候我們需要獲得並進行一些操作,那麼可以這樣做


String xid = RootContext.getXID();RootContext.unbind();//解綁//中途做一些與事務無關的事。比如日誌服務等等 排除掉,然後RootContext.bind(xid);//再綁回來


@GlobalTransactional也有自己的隔離級別和rollback等,可根據業務情況自行設置


package io.seata.spring.annotation;import io.seata.tm.api.transaction.Propagation;import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})@Inheritedpublic @interface GlobalTransactional { int timeoutMills() default 60000; String name() default ""; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; Propagation propagation() default Propagation.REQUIRED;}


AT 總結


  • 再次強調AT模式是自動的,它自動幫你做回滾和提交,使用時考慮跟自己的實際業務場景是否適合
  • 例子中我對執行事務的方法並沒有做冪等,在實際生產情況下,一定會出現問題的,所以大家在用的時候要注意做接口冪等處理
  • 有關更多seata的參數配置,如超時,重試次數等。請參考 官網 。這裡當然要結合你的feign的重試和超時時間整體考慮。
  • 通過上文的描述我們利用一個例子將AT模式的全局分布式事務模擬了出來,也總結了一些比較難理解和需要注意的點,希望能夠幫助到正在使用seata的小夥伴。


參考資料

[1]

seata官方文檔:http://seata.io/zh-cn/docs/overview/what-is-seata.html

[2]

分布式事務 Seata 及其三種模式詳解:http://seata.io/zh-cn/blog/seata-at-tcc-saga.html

[3]

nacos官方文檔:https://nacos.io/zh-cn/

[4]

apollo的github地址:https://github.com/ctripcorp/apollo

[5]

解決nacos註冊內網地址問題:https://www.cnblogs.com/liboware/p/11973321.html

[6]

官網文檔:http://seata.io/zh-cn/docs/user/configurations.html

[7]

芋道源碼:http://www.iocoder.cn/Spring-Cloud-Alibaba/Seata/

[8]

官網參數配置:http://seata.io/zh-cn/docs/user/configurations.html

相關焦點

  • 分布式事務:從理論到實踐(三)
    注意加@LocalTCC 註解(必要),適用於SpringCloud+Feign模式下的TCC@TwoPhaseBusinessAction(必要) 註解try方法,name 一般寫方法名就行,注意全局唯一,commitMethod對應提交方法名,rollbackMethod對應回滾方法名。
  • TX-LCN—分布式事務框架(一)
    什麼時候使用分布式事務事務的概念最早是在學習資料庫(MySQL、Oracle)中接觸到的,一個事務(本地事務)就是一系列SQL語句的集合,只要在執行過程中一條SQL出錯就會導致整個事務失敗,回滾到原點。而在分布式系統中存在多模塊完成一次業務。那麼就存在一個業務由多模塊操作同一個數據源。
  • 阿里終面:分布式事務原理(上)
    前言單數據源事務 & 多數據源事務常見分布式事務解決方案2.1. 分布式事務模型2.2. 二將軍問題和冪等性2.3. 兩階段提交(2PC) & 三階段提交(3PC)方案2.4. TCC 方案2.5.但是一個資料庫的本地事務機制僅僅對落到自己身上的查詢操作(這裡的查詢是廣義的,包括增刪改查等)起作用,無法幹涉對其他資料庫的查詢操作。所以,資料庫自身提供的本地事務機制無法確保業務對多數據源全局操作的可靠性。基於此,針對多數據源操作提出的分布式事務機制就出現了。分布式事務也可以叫做全局事務。
  • 阿里終面:分布式事務原理(下)
    上一篇我們講了單數據源事務 & 多數據源事務和常見分布式事務解決方案,今天我們繼續3. Seata in AT mode 的實現第 2 章給出了實現實現分布式事務的集中常見的理論模型。本章給出業界開源分布式事務框架 Seata 的實現。
  • 資料庫事務不夠嗎?什麼場景要用到分布式事務?
    原子性(Atomicity)一致性(Consistency)隔離性(Isolation)持久性(Durability)(1) 原子性一個事務要麼全部提交成功,要麼全部失敗回滾,不能只執行其中一部分操作(2) 一致性資料庫系統在運行過程中發生故障,有些事務尚未完成就被迫中斷,如果這些事務有一部分修改已經寫入資料庫
  • 一文搞懂分布式事務-CAP理論
    網際網路系統中,分布式事務是無法避免的,目前多數解決方案是BASE理論,最終一致性,結合事務補償。1.什麼是CAP理論。 CAP理論,又稱為布魯爾定理,是加州大學伯克利分校的計算機科學家埃裡克.布魯爾(Eric Brewer)在2000年的ACM PODC提出的猜想。2年後,麻省理工學院的Seth Gilbert和Nancy Lynch從理論上證明了CAP。Robert Greiner的發表的兩篇博文更容易理解,第一篇已經被自己標識為過時。
  • 一篇文章徹底搞懂「分布式事務」
    01 為什麼需要分布式事務由於近十年網際網路的發展非常迅速,很多網站的訪問越來越大,集中式環境已經不能滿足業務的需要了,只能按照業務為單位進行數據拆分(包含:垂直拆分與水平拆分),以及按照業務為單位提供服務,從早期的集中式轉變為面向服務架構的分布式應用環境。
  • 分布式學習最佳實踐:從分布式系統的特徵開始(附思維導圖)
    )的時候,我逐漸明白,這是因為要滿足這些feature,才設計了很多協議與算法,也提出了一些理論。從分布式系統的特徵出發分布式的世界中涉及到大量的協議(raft、2pc、lease、quorum等)、大量的理論(FLP, CAP等)、大量的系統(GFS、MongoDB
  • 一文看懂分布式事務
    如果這個時候發生非正常的 DB 服務重啟,那麼這些數據還沒在內存,並沒有同步到磁碟文件中(注意,同步到磁碟文件是個隨機 IO),也就是會發生數據丟失。如果這個時候,能夠在有一個文件,當 buffer pool 中的 data page 變更結束後,把相應修改記錄記錄到這個文件(注意,記錄日誌是順序 IO)。
  • 太香了這份架構解密:從分布式到微服務(第二版),神仙進階指南
    有這樣一本神奇的架構書讀者可以在字裡行間見證微服務的發展脈絡大到分布式、微服務、雲原生、K8s、Service Mesh小到網絡、分布式系統、RPC、分布式存儲、分布式計算、全文檢索與消息隊列中間件等這就是《架構解密:從分布式到微服務(第2版)》解密架構發展脈絡和梳理原理之旅就等你啦!
  • 分布式事務和分布式hash
    一致性狀態包含強一致性,任何時刻,所有節點中數據都是一樣的弱一致性,數據更新後,只能訪問到部分節點數據或者是全部訪問不到最終一致性,不保證任何時刻一樣,但隨著時間推移最終會達到一致性狀態因此,存在如下幾種方案:2PC ,二階段提交是一種儘量強一致性設計,引入一個事務協調者來協調和管理各參與者的提交和回滾
  • 後端程式設計師必備:分布式事務基礎篇
    用戶下單購買禮物,禮物資料庫、金幣資料庫、訂單資料庫在不同節點上,用本地事務是不可以的,那麼如何保證不同資料庫(節點)上的數據一致性呢?這就需要分布式事務啦~分庫分表下的分布式事務隨著業務的發展,資料庫的數據日益龐大,超過千萬級別的數據,我們就需要對它分庫分表(以前公司是用mycat分庫分表,後來用sharding-jdbc)。
  • 花了整整三天,我把分布式事務搞完了(優缺點、場景、實現)
    然後再分析一波分布式事務框架 Seata 的具體實現,看看分布式事務究竟是如何落地的,畢竟協議要落地才是有用的。在明確了事務和分布式事務之後,我們就先來看看常見的分布式事務方案:2PC、3PC、TCC、本地消息、事務消息。2PC2PC,Two-phase commit protocol,即兩階段提交協議。它引入了一個事務協調者角色,來管理各個參與者(就是各資料庫資源)。
  • 最全的分布式事務總結
    事務的隔離性是通過資料庫鎖的機制實現的,持久性通過 Redo Log(重做日誌)來實現,原子性和一致性通過 Undo Log 來實現。Undo Log 的原理很簡單,為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到一個地方(這個存儲數據備份的地方稱為 Undo Log)。然後進行數據的修改。
  • 肝完這份MQ+分布式事務套餐,其實阿里P8你也值得
    如果你的簡歷出現Dubbo或Spring cloud的字眼,通常面試官都會問你分布式事務的問題,因為你項目用了Dubbo或Spring cloud分布式框架,就必定要解決分布式事務的難題。如果你想拿到高薪offer,請學好分布式事務技能。
  • Shardingsphere對XA分布式事務的支持
    Apache ShardingSphere 是一套開源的分布式資料庫中間件解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款相互獨立,卻又能夠混合部署配合使用的產品組成。
  • 阿里終面:分布式事務原理
    用戶請求購物微服務商完成下單時,購物微服務一方面調用庫存微服務扣減相應商品的庫存數量,另一方面調用訂單微服務插入訂單記錄(為了後文描述分布式事務解決方案的方便,這裡給出的是一個最簡單的電商系統微服務劃分和最簡單的購物業務流程,後續的支付、物流等業務不在考慮範圍內)。
  • 基於分布式事務徹底解決資料庫數據一致性
    ,要用分布式事務去解決二、分布式事務場景1、電商下單場景① 下單② 發送消息到MQ2、一致性保證① 本地事務(1)下單操作;(2)發送MQ消息操作;(3)放進一個本地事務;三、分布式事務分類1、剛性分布式事務有 強一致性、 XA模型、 CAP中的CP
  • 分布式事務還不理解?這一篇帶你走進它的世界
    3 分布式系統的一致性前面介紹到的分布式事務的難點涉及的問題,最終影響是導致數據出現不一致,下面對分布式系統的一致性問題進行理論分析,後面將基於這些理論進行分布式方案的介紹。CAP理論的定義 在一個分布式系統(指互相連接並共享數據的節點的集合)中,當涉及讀寫操作時,只能保證一致性(Consistence)、可用性(Availability)、分區容錯性(PartitionTolerance)三者中的兩個,另外一個必須被犧牲。
  • 微服務架構下分布式事務處理方案選擇和對比
    分布式事務和本地事務並無太大不同,也需保證事務的四個屬性(原子性、一致性、隔離性、持久性)。對於分布式事務的潛在場景,可以簡單的分為三類:1.跨資源一個完整業務需要操作兩個獨立資料庫,比如需要在A資料庫插入一條訂單記錄,同時更新B資料庫中的庫存狀態,兩個操作跨資料庫,但是需要控制在一個完整事務裡面。