- 一、本地事務
- 二、分散式事務
- 2.1、典型的分散式事務應用場景
- 2.2、跨庫事務
- 2.3、分庫分表
- 2.4、微服務架構
- 三、兩階段提交協議(2PC)
- 2PC存在的問題
- 四、Seata
- 4.1、Seata的三大角色
- 4.2、Seata AT模式的設計思路
- 五、Seata快速開始
- Seata Server(TC)環境搭建
- 步驟一:下載安裝包
- 步驟二:建表(db模式)
- 步驟三:配置Nacos註冊中心
- 步驟四:配置Nacos配置中心
- 步驟五:啟動Seata Server
- Seata Client快速開始
- 1) 環境準備
- 2) 微服務匯入seata依賴
- 3)微服務對應資料庫中新增undo_log表(僅AT模式)
- 4) 微服務application.yml中新增seata配置
- 5) 在全域性事務發起者中新增@GlobalTransactional註解
- 6)測試分散式事務是否生效
- Seata Server(TC)環境搭建
一、本地事務
大多數場景下,我們的應用都只需要操作單一的資料庫,這種情況下的事務稱之為本地事務(Local Transaction)。本地事務的ACID特性是資料庫直接提供支援。本地事務應用架構如下所示:
在JDBC程式設計中,我們透過java.sql.Connection物件來開啟、關閉或者提交事務。程式碼如下所示:
Connection conn = ... //獲取資料庫連線
conn.setAutoCommit(false); //開啟事務
try{
//...執行增刪改查sql
conn.commit(); //提交事務
}catch (Exception e) {
conn.rollback();//事務回滾
}finally{
conn.close();//關閉連結
}
二、分散式事務
在微服務架構中,完成某一個業務功能可能需要橫跨多個服務,操作多個資料庫。這就涉及到到了分散式事務,需要操作的資源位於多個資源伺服器上,而應用需要保證對於多個資源伺服器的資料操作,要麼全部成功,要麼全部失敗。本質上來說,分散式事務就是為了保證不同資源伺服器的資料一致性。
2.1、典型的分散式事務應用場景
2.2、跨庫事務
跨庫事務指的是,一個應用某個功能需要操作多個庫,不同的庫中儲存不同的業務資料。下圖演示了一個服務同時操作2個庫的情況:
2.3、分庫分表
通常一個庫資料量比較大或者預期未來的資料量比較大,都會進行分庫分表。如下圖,將資料庫B拆分成了2個庫:
對於分庫分表的情況,一般開發人員都會使用一些資料庫中介軟體來降低sql操作的複雜性。如,對於sql:insert into user(id,name) values (1,"張三"),(2,"李四")。這條sql是操作單庫的語法,單庫情況下,可以保證事務的一致性。 但是由於現在進行了分庫分表,開發人員希望將1號記錄插入分庫1,2號記錄插入分庫2。所以資料庫中介軟體要將其改寫為2條sql,分別插入兩個不同的分庫,此時要保證兩個庫要不都成功,要不都失敗,因此基本上所有的資料庫中介軟體都面臨著分散式事務的問題。
2.4、微服務架構
下圖演示了一個3個服務之間彼此呼叫的微服務架構:
Service A完成某個功能需要直接運算元據庫,同時需要呼叫Service B和Service C,而Service B又同時操作了2個資料庫,Service C也操作了一個庫。需要保證這些跨服務呼叫對多個資料庫的操作要麼都成功,要麼都失敗,實際上這可能是最典型的分散式事務場景。
小結:上述討論的分散式事務場景中,無一例外的都直接或者間接的操作了多個資料庫。如何保證事務的ACID特性,對於分散式事務實現方案而言,是非常大的挑戰。同時,分散式事務實現方案還必須要考慮效能的問題,如果為了嚴格保證ACID特性,導致效能嚴重下降,那麼對於一些要求快速響應的業務,是無法接受的。
三、兩階段提交協議(2PC)
兩階段提交(Two Phase Commit),就是將提交(commit)過程劃分為2個階段(Phase):
階段1:
TM通知各個RM準備提交它們的事務分支。如果RM判斷自己進行的工作可以被提交,那就對工作內容進行持久化,再給TM肯定答覆;要是發生了其他情況,那給TM的都是否定答覆。
以mysql資料庫為例,在第一階段,事務管理器向所有涉及到的資料庫伺服器發出prepare"準備提交"請求,資料庫收到請求後執行資料修改和日誌記錄等處理,處理完成後只是把事務的狀態改成"可以提交",然後把結果返回給事務管理器。
階段2
TM根據階段1各個RM prepare的結果,決定是提交還是回滾事務。如果所有的RM都prepare成功,那麼TM通知所有的RM進行提交;如果有RM prepare失敗的話,則TM通知所有RM回滾自己的事務分支。
以mysql資料庫為例,如果第一階段中所有資料庫都prepare成功,那麼事務管理器向資料庫伺服器發出"確認提交"請求,資料庫伺服器把事務的"可以提交"狀態改為"提交完成"狀態,然後返回應答。如果在第一階段內有任何一個資料庫的操作發生了錯誤,或者事務管理器收不到某個資料庫的回應,則認為事務失敗,回撤所有資料庫的事務。資料庫伺服器收不到第二階段的確認提交請求,也會把"可以提交"的事務回撤。
2PC存在的問題
- 同步阻塞問題
2PC 中的參與者是阻塞的。在第一階段收到請求後就會預先鎖定資源,一直到 commit 後才會釋放。
- 單點故障
由於協調者的重要性,一旦協調者TM發生故障,參與者RM會一直阻塞下去。尤其在第二階段,協調者發生故障,那麼所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。
- 資料不一致
若協調者第二階段傳送提交請求時崩潰,可能部分參與者收到commit請求提交了事務,而另一部分參與者未收到commit請求而放棄事務,從而造成資料不一致的問題。
四、Seata
Seata 是一款開源的分散式事務解決方案,致力於提供高效能和簡單易用的分散式事務服務。Seata 將為使用者提供了 AT、TCC、SAGA 和 XA 事務模式,為使用者打造一站式的分散式解決方案。AT模式是阿里首推的模式,阿里雲上有商用版本的GTS(Global Transaction Service 全域性事務服務)
官網:https://seata.io/zh-cn/index.html
4.1、Seata的三大角色
在 Seata 的架構中,一共有三個角色:
- TC (Transaction Coordinator) - 事務協調者
維護全域性和分支事務的狀態,驅動全域性事務提交或回滾。
- TM (Transaction Manager) - 事務管理器
定義全域性事務的範圍:開始全域性事務、提交或回滾全域性事務。
- RM (Resource Manager) - 資源管理器
管理分支事務處理的資源,與TC交談以註冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
其中,TC 為單獨部署的 Server 服務端,TM 和 RM 為嵌入到應用中的 Client 客戶端。
在 Seata 中,一個分散式事務的生命週期如下:
- TM 請求 TC 開啟一個全域性事務。TC 會生成一個 XID 作為該全域性事務的編號。XID會在微服務的呼叫鏈路中傳播,保證將多個微服務的子事務關聯在一起。
- RM 請求 TC 將本地事務註冊為全域性事務的分支事務,透過全域性事務的 XID 進行關聯。
- TM 請求 TC 告訴 XID 對應的全域性事務是進行提交還是回滾。
- TC 驅動 RM 們將 XID 對應的自己的本地事務進行提交還是回滾。
思考
1、為什麼是TM告知TC需要回滾或者是提交?
2、TC如何告知RM進行回滾或者是提交?
3、RM如何進行提交或者是回滾?
在下面將會對這些問題進行一一分析和解決,但不會深入到原始碼級別。
4.2、Seata AT模式的設計思路
Seata AT模式的核心是對業務無侵入,是一種改進後的兩階段提交,其設計思路如下:
-
一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
-
二階段:
-
提交非同步化,非常快速地完成。
-
回滾透過一階段的回滾日誌進行反向補償。
-
一階段
業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
核心在於對業務sql進行解析,轉換成undolog,並同時入庫,這是怎麼做的呢?
二階段
- 分散式事務操作成功,則TC通知RM非同步刪除undolog
- 分散式事務操作失敗,TM向TC傳送回滾請求,RM 收到協調器TC發來的回滾請求,透過 XID 和 Branch ID 找到相應的回滾日誌記錄,透過回滾記錄生成反向的更新 SQL 並執行,以完成分支的回滾。
五、Seata快速開始
Seata分TC、TM和RM三個角色,TC(Server端)為單獨服務端部署,TM和RM(Client端)由業務系統整合。
Seata Server(TC)環境搭建
Server端儲存模式(store.mode)支援三種:
- file:單機模式,全域性事務會話資訊記憶體中讀寫並持久化本地檔案root.data,效能較高
- db:高可用模式,全域性事務會話資訊透過db共享,相應效能差些
- redis:1.3及以上版本支援,效能較高,存在事務資訊丟失風險,請提前配置適合當前場景的redis持久化配置
資源目錄:
-
https://github.com/seata/seata/tree/v1.5.1/script
-
client
- 存放client端sql指令碼,引數配置
-
config-center
- 各個配置中心引數匯入指令碼,config.txt(包含server和client)為通用引數檔案
-
server
- server端資料庫指令碼及各個容器配置
db儲存模式+Nacos(註冊&配置中心)方式部署
步驟一:下載安裝包
https://github.com/seata/seata/releases
步驟二:建表(db模式)
建立資料庫seata,執行sql指令碼,https://github.com/seata/seata/tree/v1.5.1/script/server/db
步驟三:配置Nacos註冊中心
註冊中心可以說是微服務架構中的”通訊錄“,它記錄了服務和服務地址的對映關係。在分散式架構中,服務會註冊到註冊中心,當服務需要呼叫其它服務時,就到註冊中心找到服務的地址,進行呼叫。比如Seata Client端(TM,RM),發現Seata Server(TC)叢集的地址,彼此通訊。
注意:Seata的註冊中心是作用於Seata自身的,和Spring Cloud的註冊中心無關
Seata支援哪些註冊中心?
- eureka
- consul
- nacos
- etcd
- zookeeper
- sofa
- redis
- file (直連)
配置將Seata Server註冊到Nacos,修改conf/application.yml檔案
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
cluster: default
username: nacos
password: nacos
注意:請確保client與server的註冊處於同一個namespace和group,不然會找不到服務。
啟動 Seata-Server 後,會發現Server端的服務出現在 Nacos 控制檯中的註冊中心列表中。
步驟四:配置Nacos配置中心
配置中心可以說是一個"大貨倉",內部放置著各種配置檔案,你可以透過自己所需進行獲取配置載入到對應的客戶端。比如Seata Client端(TM,RM),Seata Server(TC),會去讀取全域性事務開關,事務會話儲存模式等資訊。
注意:Seata的配置中心是作用於Seata自身的,和Spring Cloud的配置中心無關
Seata支援哪些配置中心?
- nacos
- consul
- apollo
- etcd
- zookeeper
- file (讀本地檔案, 包含conf、properties、yml配置檔案的支援)
1)配置Nacos配置中心地址,修改conf/application.yml檔案
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
group: SEATA_GROUP
data-id: seataServer.properties
username: nacos
password: nacos
2)上傳配置至Nacos配置中心
https://github.com/seata/seata/tree/v1.5.1/script/config-center
a) 獲取/seata/script/config-center/config.txt,修改為db儲存模式,並修改mysql連線配置
store.mode=db
store.lock.mode=db
store.session.mode=db
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
在store.mode=db,由於seata是透過jdbc的executeBatch來批次插入全域性鎖的,根據MySQL官網的說明,連線引數中的rewriteBatchedStatements為true時,在執行executeBatch,並且操作型別為insert時,jdbc驅動會把對應的SQL最佳化成insert into () values (), ()
的形式來提升批次插入的效能。 根據實際的測試,該引數設定為true後,對應的批次插入效能為原來的10倍多,因此在資料來源為MySQL時,建議把該引數設定為true。
b) 配置事務分組, 要與client配置的事務分組一致
- 事務分組:seata的資源邏輯,可以按微服務的需要,在應用程式(客戶端)對自行定義事務分組,每組取一個名字。
- 叢集:seata-server服務端一個或多個節點組成的叢集cluster。 應用程式(客戶端)使用時需要指定事務邏輯分組與Seata服務端叢集的對映關係。
事務分組如何找到後端Seata叢集(TC)?
- 首先應用程式(客戶端)中配置了事務分組(GlobalTransactionScanner 構造方法的txServiceGroup引數)。若應用程式是SpringBoot則透過seata.tx-service-group 配置。
- 應用程式(客戶端)會透過使用者配置的配置中心去尋找service.vgroupMapping .[事務分組配置項],取得配置項的值就是TC叢集的名稱。若應用程式是SpringBoot則透過seata.service.vgroup-mapping.事務分組名=叢集名稱 配置
- 拿到叢集名稱程式透過一定的前字尾+叢集名稱去構造服務名,各配置中心的服務名實現不同(前提是Seata-Server已經完成服務註冊,且Seata-Server向註冊中心報告cluster名與應用程式(客戶端)配置的叢集名稱一致)
- 拿到服務名去相應的註冊中心去拉取相應服務名的服務列表,獲得後端真實的TC服務列表(即Seata-Server叢集節點列表)
c) 在nacos配置中心中新建配置,dataId為seataServer.properties,配置內容為上面修改後的config.txt中的配置資訊
從v1.4.2版本開始,seata已支援從一個Nacos dataId中獲取所有配置資訊,你只需要額外新增一個dataId配置項。
新增後檢視:
步驟五:啟動Seata Server
啟動命令:
bin/seata-server.sh
啟動成功,檢視控制檯,賬號密碼都是seata。http://localhost:7091/#/login
在Nacos註冊中心中可以檢視到seata-server註冊成功
支援的啟動引數
引數 | 全寫 | 作用 | 備註 |
---|---|---|---|
-h | --host | 指定在註冊中心註冊的 IP | 不指定時獲取當前的 IP,外部訪問部署在雲環境和容器中的 server 建議指定 |
-p | --port | 指定 server 啟動的埠 | 預設為 8091 |
-m | --storeMode | 事務日誌儲存方式 | 支援file,db,redis,預設為 file 注:redis需seata-server 1.3版本及以上 |
-n | --serverNode | 用於指定seata-server節點ID | 如 1,2,3..., 預設為 1 |
-e | --seataEnv | 指定 seata-server 執行環境 | 如 dev, test 等, 服務啟動時會使用 registry-dev.conf 這樣的配置 |
比如:
bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db
Seata Client快速開始
Spring Cloud Alibaba整合Seata AT模式實戰
業務場景
使用者下單,整個業務邏輯由三個微服務構成:
- 庫存服務:對給定的商品扣除庫存數量。
- 訂單服務:根據採購需求建立訂單。
- 帳戶服務:從使用者帳戶中扣除餘額。
1) 環境準備
- 父pom指定微服務版本
Spring Cloud Alibaba Version | Spring Cloud Version | Spring Boot Version | Seata Version |
---|---|---|---|
2.2.8.RELEASE | Spring Cloud Hoxton.SR12 | 2.3.12.RELEASE | 1.5.1 |
- 啟動Seata Server(TC)端,Seata Server使用nacos作為配置中心和註冊中心
- 啟動nacos服務
2) 微服務匯入seata依賴
spring-cloud-starter-alibaba-seata內部整合了seata,並實現了xid傳遞
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3)微服務對應資料庫中新增undo_log表(僅AT模式)
https://github.com/seata/seata/blob/v1.5.1/script/client/at/db/mysql.sql
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) 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(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
4) 微服務application.yml中新增seata配置
seata:
application-id: ${spring.application.name}
# seata 服務分組,要與服務端配置service.vgroup_mapping的字尾對應
tx-service-group: default_tx_group
registry:
# 指定nacos作為註冊中心
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace: public
group: SEATA_GROUP
config:
# 指定nacos作為配置中心
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
group: SEATA_GROUP
data-id: seataServer.properties
注意:請確保client與server的註冊中心和配置中心namespace和group一致
5) 在全域性事務發起者中新增@GlobalTransactional註解
核心程式碼
@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order saveOrder(OrderVo orderVo){
log.info("=============使用者下單=================");
log.info("當前 XID: {}", RootContext.getXID());
// 儲存訂單
Order order = new Order();
order.setUserId(orderVo.getUserId());
order.setCommodityCode(orderVo.getCommodityCode());
order.setCount(orderVo.getCount());
order.setMoney(orderVo.getMoney());
order.setStatus(OrderStatus.INIT.getValue());
Integer saveOrderRecord = orderMapper.insert(order);
log.info("儲存訂單{}", saveOrderRecord > 0 ? "成功" : "失敗");
//扣減庫存
storageFeignService.deduct(orderVo.getCommodityCode(),orderVo.getCount());
//扣減餘額
accountFeignService.debit(orderVo.getUserId(),orderVo.getMoney());
//更新訂單
Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
log.info("更新訂單id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失敗");
return order;
}
6)測試分散式事務是否生效
- 分散式事務成功,模擬正常下單、扣庫存,扣餘額
- 分散式事務失敗,模擬下單扣庫存成功、扣餘額失敗,事務是否回滾