好長時間沒發文了,最近着實是有點忙,當爹的第 43 天,身心疲憊。這又趕上年底,公司沖 KPI
強制技術部加班到十點,晚上孩子隔兩三個小時一醒,基本沒睡囫囵覺的機會,天天處于迷糊的狀态,孩子還時不時起一些奇奇怪怪的疹子,總讓人擔驚受怕的。
本就不多的寫文章時間又被無限分割,哎~ 打勞工真是太難了。

Seata
中間件做分布式事務,那就做一個實踐分享吧!
介紹
Seata
之前在簡單回顧一下分布式事務的基本概念。 分布式事務的産生
我們先看看百度上對于分布式事務的定義:分布式事務是指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分别位于不同的分布式系統的不同節點之上。
額~ 有點抽象,簡單的畫個圖好了解一下,拿下單減庫存、扣餘額來說舉例:
當系統的體量很小時,單體架構完全可以滿足現有業務需求,所有的業務共用一個資料庫,整個下單流程或許隻用在一個方法裡同一個事務下操作資料庫即可。此時做到所有操作要麼全部送出 或 要麼全部復原很容易。
分庫分表、SOA
可随着業務量的不斷增長,單體架構漸漸扛不住巨大的流量,此時就需要對資料庫、表做
分庫分表
處理,将應用
SOA
服務化拆分。也就産生了訂單中心、使用者中心、庫存中心等,由此帶來的問題就是業務間互相隔離,每個業務都維護着自己的資料庫,資料的交換隻能進行
RPC
調用。
當使用者再次下單時,需同時對訂單庫
order
、庫存庫
storage
、使用者庫
account
進行操作,可此時我們隻能保證自己本地的資料一緻性,無法保證調用其他服務的操作是否成功,是以為了保證整個下單流程的資料一緻性,就需要分布式事務介入。
Seata 優勢
實作分布式事務的方案比較多,常見的比如基于
XA
協定的
2PC
、
3PC
,基于業務層的
TCC
,還有應用消息隊列 + 消息表實作的最終一緻性方案,還有今天要說的
Seata
中間件,下邊看看各個方案的優缺點。
2PC
2PC
基于 XA 協定實作的分布式事務,XA 協定中分為兩部分:事務管理器和本地資料總管。其中本地資料總管往往由資料庫實作,比如 Oracle、MYSQL 這些資料庫都實作了 XA 接口,而事務管理器則作為一個全局的排程者。
兩階段送出(
2PC
),對業務侵⼊很小,它最⼤的優勢就是對使⽤⽅透明,使用者可以像使⽤本地事務⼀樣使⽤基于 XA 協定的分布式事務,能夠嚴格保障事務 ACID 特性。
可
2PC
的缺點也是顯而易見,它是一個強一緻性的同步阻塞協定,事務執⾏過程中需要将所需資源全部鎖定,也就是俗稱的
剛性事務
。是以它比較适⽤于執⾏時間确定的短事務,整體性能比較差。
一旦事務協調者當機或者發生網絡抖動,會讓參與者一直處于鎖定資源的狀态或者隻有一部分參與者送出成功,導緻資料的不一緻。是以,在⾼并發性能⾄上的場景中,基于 XA 協定的分布式事務并不是最佳選擇。
3PC
3PC
三段送出(
3PC
)是二階段送出(
2PC
)的一種改進版本 ,為解決兩階段送出協定的阻塞問題,上邊提到兩段送出,當協調者崩潰時,參與者不能做出最後的選擇,就會一直保持阻塞鎖定資源。
2PC
中隻有協調者有逾時機制,
3PC
在協調者和參與者中都引入了逾時機制,協調者出現故障後,參與者就不會一直阻塞。而且在第一階段和第二階段中又插入了一個準備階段(如下圖,看着有點啰嗦),保證了在最後送出階段之前各參與節點的狀态是一緻的。
雖然
3PC
用逾時機制,解決了協調者故障後參與者的阻塞問題,但與此同時卻多了一次網絡通信,性能上反而變得更差,也不太推薦。
TCC
所謂的
TCC
程式設計模式,也是兩階段送出的一個變種,不同的是
TCC
為在業務層編寫代碼實作的兩階段送出。
TCC
分别指
Try
Confirm
Cancel
,一個業務操作要對應的寫這三個方法。
以下單扣庫存為例,
Try
階段去占庫存,
Confirm
階段則實際扣庫存,如果庫存扣減失敗
Cancel
階段進行復原,釋放庫存。
TCC 不存在資源阻塞的問題,因為每個方法都直接進行事務的送出,一旦出現異常通過則
Cancel
來進行復原補償,這也就是常說的補償性事務。
原本一個方法,現在卻需要三個方法來支援,可以看到 TCC 對業務的侵入性很強,而且這種模式并不能很好地被複用,會導緻開發量激增。還要考慮到網絡波動等原因,為保證請求一定送達都會有重試機制,是以考慮到接口的幂等性。
消息事務(最終一緻性)
消息事務其實就是基于消息中間件的兩階段送出,将本地事務和發消息放在同一個事務裡,保證本地操作和發送消息同時成功。
下單扣庫存原理圖:
- 訂單系統向
發送一條預備扣減庫存消息,MQ
儲存預備消息并傳回成功MQ
ACK
- 接收到預備消息執行成功
,訂單系統執行本地下單操作,為防止消息發送成功而本地事務失敗,訂單系統會實作ACK
的回調接口,其内不斷的檢查本地事務是否執行成功,如果失敗則MQ
復原預備消息;成功則對消息進行最終rollback
送出。commit
- 庫存系統消費扣減庫存消息,執行本地事務,如果扣減失敗,消息會重新投,一旦超出重試次數,則本地表持久化失敗消息,并啟動定時任務做補償。
基于消息中間件的兩階段送出方案,通常用在高并發場景下使用,犧牲資料的強一緻性換取性能的大幅提升,不過實作這種方式的成本和複雜度是比較高的,還要看實際業務情況。
Seata
Seata
也是從兩段送出演變而來的一種分布式事務解決方案,提供了
AT
TCC
SAGA
和
XA
等事務模式,這裡重點介紹
AT
模式。
既然
Seata
是兩段送出,那我們看看它在每個階段都做了點啥?下邊我們還以下單扣庫存、扣餘額舉例。
先介紹
Seata
分布式事務的幾種角色:
-
: 全局事務協調者,用來協調全局事務和各個分支事務(不同服務)的狀态, 驅動全局事務和各個分支事務的復原或送出。Transaction Coordinator(TC)
-
: 事務管理者,業務層中用來開啟/送出/復原一個整體事務(在調用服務的方法中用注解開啟事務)。Transaction Manager™
-
: 資源管理者,一般指業務資料庫代表了一個分支事務(Resource Manager(RM)
),管理分支事務與Branch Transaction
進行協調注冊分支事務并且彙報分支事務的狀态,驅動分支事務的送出或復原。TC
Seata 實作分布式事務,設計了一個關鍵角色(復原日志記錄表),我們在每個應用分布式事務的業務庫中建立這張表,這個表的核心作用就是,将業務資料在更新前後的資料鏡像組織成復原日志,備份在
UNDO_LOG
表中,以便業務異常能随時復原。
UNDO_LOG
第一個階段
比如:下邊我們更新
user
表的
name
字段。
update user set name = '小富最帥' where name = '程式員内點事'
首先 Seata 的
JDBC
資料源代理通過對業務 SQL 解析,提取 SQL 的中繼資料,也就是得到 SQL 的類型(
UPDATE
),表(
user
),條件(
where name = '程式員内點事'
)等相關的資訊。
先查詢資料前鏡像,根據解析得到的條件資訊,生成查詢語句,定位一條資料。
select name from user where name = '程式員内點事'
緊接着執行業務 SQL,根據前鏡像資料主鍵查詢出後鏡像資料
select name from user where id = 1
把業務資料在更新前後的資料鏡像組織成復原日志,将業務資料的更新和復原日志在同一個本地事務中送出,分别插入到業務表和
UNDO_LOG
表中。
復原記錄資料格式如下:包括
afterImage
前鏡像、
beforeImage
後鏡像、
branchId
分支事務ID、
xid
全局事務ID
{
"branchId":641789253,
"xid":"xid:xxx",
"undoItems":[
{
"afterImage":{
"rows":[
{
"fields":[
{
"name":"id",
"type":4,
"value":1
}
]
}
],
"tableName":"product"
},
"beforeImage":{
"rows":[
{
"fields":[
{
"name":"id",
"type":4,
"value":1
}
]
}
],
"tableName":"product"
},
"sqlType":"UPDATE"
}
]
}
這樣就可以保證,任何送出的業務資料的更新一定有相應的復原日志。
在本地事務送出前,各分支事務需向TC 注冊分支 (
全局事務協調者
) ,為要修改的記錄申請 全局鎖 ,要為這條資料加鎖,利用
Branch Id
語句。而如果一直拿不到鎖那就需要復原本地事務。TM 開啟事務後會生成全局唯一的
SELECT FOR UPDATE
,會在各個調用的服務間進行傳遞。
XID
有了這樣的機制,本地事務分支(
Branch Transaction
)便可以在全局事務的第一階段送出,并馬上釋放本地事務鎖定的資源。相比于傳統的
XA
事務在第二階段釋放資源,
Seata
降低了鎖範圍提高效率,即使第二階段發生異常需要復原,也可以快速 從
UNDO_LOG
表中找到對應復原資料并反解析成 SQL 來達到復原補償。
最後本地事務送出,業務資料的更新和前面生成的 UNDO LOG 資料一并送出,并将本地事務送出的結果上報給全局事務協調者 TC。
第二個階段
第二階段是根據各分支的決議做送出或復原:
如果決議是全局送出,此時各分支事務已送出并成功,這時
全局事務協調者(TC)
會向分支發送第二階段的請求。收到 TC 的分支送出請求,該請求會被放入一個異步任務隊列中,并馬上傳回送出成功結果給 TC。異步隊列中會異步和批量地根據
Branch ID
查找并删除相應
UNDO LOG
復原記錄。
如果決議是全局復原,過程比全局送出麻煩一點,
RM
服務方收到
TC
全局協調者發來的復原請求,通過
XID
Branch ID
找到相應的復原日志記錄,通過復原記錄生成反向的更新 SQL 并執行,以完成分支的復原。
注意:這裡删除復原日志記錄操作,一定是在本地業務事務執行之後
上邊說了幾種分布式事務各自的優缺點,下邊實踐一下分布式事務中間 Seata 感受一下。
Seata 實踐
Seata 是一個需獨立部署的中間件,是以先搭 Seata Server,這裡以最新的
seata-server-1.4.0
版本為例,下載下傳位址:
https://seata.io/en-us/blog/download.html
解壓後的檔案我們隻需要關心
\seata\conf
目錄下的
file.conf
registry.conf
檔案。
Seata Server
file.conf
file.conf
檔案用于配置持久化事務日志的模式,目前提供
file
db
redis
三種方式。
注意:在選擇
db
方式後,需要在對應資料庫建立
globalTable
(持久化全局事務)、
branchTable
(持久化各送出分支的事務)、
lockTable
(持久化各分支鎖定資源事務)三張表。
-- the table to store GlobalSession data
-- 持久化全局事務
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
-- 持久化各送出分支的事務
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
-- 持久化每個分支鎖表事務
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
registry.conf
registry.conf
檔案設定 注冊中心 和 配置中心:
目前注冊中心支援
nacos
eureka
redis
zk
consul
etcd3
sofa
七種,這裡我使用的
eureka
作為注冊中心 ; 配置中心支援
nacos
apollo
zk
consul
etcd3
五種方式。
配置完以後在
\seata\bin
目錄下啟動
seata-server
即可,到這
Seata
的服務端就搭建好了。
Seata Client
Seata Server
環境搭建完,接下來我們建立三個服務
order-server
(下單服務)、
storage-server
(扣減庫存服務)、
account-server
(賬戶金額服務),分别服務注冊到
eureka
。
每個服務的大體核心配置如下:
spring:
application:
name: storage-server
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.93.6.1:3306/seat-storage
username: root
password: root
# eureka 注冊中心
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:8761/eureka/
instance:
hostname: 47.93.6.5
prefer-ip-address: true
業務大緻流程:使用者發起下單請求,本地 order 訂單服務建立訂單記錄,并通過
RPC
遠端調用
storage
扣減庫存服務和
account
扣賬戶餘額服務,隻有三個服務同時執行成功,才是一個完整的下單流程。如果某個服執行失敗,則其他服務全部復原。
Seata 對業務代碼的侵入性非常小,代碼中使用隻需用
@GlobalTransactional
注解開啟一個全局事務即可。
@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void create(Order order) {
String xid = RootContext.getXID();
LOGGER.info("------->交易開始");
//本地方法
orderDao.create(order);
//遠端方法 扣減庫存
storageApi.decrease(order.getProductId(), order.getCount());
//遠端方法 扣減賬戶餘額
LOGGER.info("------->扣減賬戶開始order中");
accountApi.decrease(order.getUserId(), order.getMoney());
LOGGER.info("------->扣減賬戶結束order中");
LOGGER.info("------->交易結束");
LOGGER.info("全局事務 xid: {}", xid);
}
前邊說過 Seata AT 模式實作分布式事務,必須在相關的業務庫中建立
undo_log
表來存資料復原日志,表結構如下:
-- 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`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
到這環境搭建的工作就完事了,完整案例會在後邊貼出 GitHub
位址,就不在這占用篇幅了。
測試 Seata
項目中的服務調用過程如下圖:
啟動各個服務後,我們直接請求下單接口看看效果,隻要
order
訂單表建立記錄成功,
storage
庫存表
used
字段數量遞增、
account
餘額表
used
字段數量遞增則表示下單流程成功。
請求後正向流程是沒問題的,資料和預想的一樣
而且發現
TM
事務管理者
order-server
服務的控制台也列印出了兩階段送出的日志
那麼再看看如果其中一個服務異常,會不會正常復原呢?在
account-server
服務中模拟逾時異常,看能否實作全局事務復原。
發現資料全沒執行成功,說明全局事務復原也成功了
那看一下
undo_log
復原記錄表的變化情況,由于
Seata
删除復原日志的速度很快,是以要想在表中看見復原日志,必須要在某一個服務上打斷點才看的更明顯。
總結
上邊簡單介紹了
2PC
3PC
TCC
MQ
Seata
這五種分布式事務解決方案,還詳細的實踐了
Seata
中間件。但不管我們選哪一種方案,在項目中應用都要謹慎再謹慎,除特定的資料強一緻性場景外,能不用盡量就不要用,因為無論它們性能如何優越,一旦項目套上分布式事務,整體效率會幾倍的下降,在高并發情況下弊端尤為明顯。
本案例 github 位址: https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-seata-transaction
如果有一絲收獲,歡迎 點贊、轉發 ,您的認可是我最大的動力。
整理了幾百本各類技術電子書,有需要的同學可以,關注公号回複 [ 666 ] 自取。還有想要加技術群的可以加我好友,和大佬侃技術、不定期内推,一起學起來。