歡迎關注本人公衆號
概述
Seata 是一款開源的分佈式事務解決方案,致力於提供高性能和簡單易用的分佈式事務服務。Seata 將爲用戶提供了 AT、TCC、SAGA 和 XA 事務模式,爲用戶打造一站式的分佈式解決方案。
本文先將官方實例跑起來,看看運行效果,值後在對其原理和源碼進行分析。
下載源碼
進入seata的GitHub主頁,下載seata和seata-samples兩個項目。下載下來後可以用idea打開。
下載完成後,idea導入seata-samples文件夾下的seata-xa
和seata文件夾下的 server
兩個項目。
AT模式演示
官方文檔寫的是下載seata-server-xxx.zip
解壓運行,我這裏不適用這種方法,因爲後續還要閱讀源碼,所以直接運行上一步下載的seata源碼運行。
準備工作
本地安裝mysql8
安裝步驟:mysql8.0.20安裝教程
配置修改
兩個項目均需要修改爲我們自己的mysql,並且設置使用AT模式。
先將server切換爲1.2.0版本
seata-server修改內容:
同時將pom文件中的connection版本改爲8:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
然後修改seata-xa項目的配置。需要修改以下內容:
application.properties修改內容都是Mysql的地址和用戶名密碼。
pom文件修改的是MySQL驅動的版本,同上。
剩下的幾個DataSourceConfiguration
文件都是修改爲AT模式。
建表
我們這裏測試的是AT模式,需建4個表
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
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;
運行官方實例
先啓動server端服務,直接運行io.seata.server.Server
的main方法即可。啓動成功輸出
2020-07-06 11:14:19.108 INFO [main]io.seata.config.FileConfiguration.<init>:121 -The configuration file used is registry.conf
2020-07-06 11:14:19.145 INFO [main]io.seata.config.FileConfiguration.<init>:121 -The configuration file used is file.conf
2020-07-06 11:14:20.765 INFO [main]io.seata.core.rpc.netty.RpcServerBootstrap.start:155 -Server started ...
依次啓動account/order/storage/business 4個服務。
啓動成功後,會在server端註冊。這裏seata是使用的springCloud Feign。
2020-07-06 11:14:46.080 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/fescar', applicationId='account-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xaa9af4c7, L:/127.0.0.1:8091 - R:/127.0.0.1:50222]
2020-07-06 11:15:00.620 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/test', applicationId='order-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xaef9d3dd, L:/127.0.0.1:8091 - R:/127.0.0.1:50259]
2020-07-06 11:15:07.131 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/test', applicationId='storage-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xf7f0d0cd, L:/127.0.0.1:8091 - R:/127.0.0.1:50281]
2020-07-06 11:15:43.133 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='account-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xedc81195, L:/127.0.0.1:8091 - R:/127.0.0.1:50341]
2020-07-06 11:15:56.732 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='order-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x90901ea5, L:/127.0.0.1:8091 - R:/127.0.0.1:50360]
2020-07-06 11:16:04.010 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='storage-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x7c1fd552, L:/127.0.0.1:8091 - R:/127.0.0.1:50369]
再business服務啓動後,會進行數據初始化:賬戶餘額10000,庫存100
@PostConstruct
public void initData() {
jdbcTemplate.update("delete from account_tbl");
jdbcTemplate.update("delete from order_tbl");
jdbcTemplate.update("delete from storage_tbl");
jdbcTemplate.update("insert into account_tbl(user_id,money) values('" + TestDatas.USER_ID + "','10000') ");
jdbcTemplate.update("insert into storage_tbl(commodity_code,count) values('" + TestDatas.COMMODITY_CODE + "','100') ");
}
總共有4個服務。賬戶服務,訂單服務,庫存服務,採購業務服務。是目前主流的微服務架構,本文演示的也是微服務架構下分佈式事務問題。
訪問http://127.0.0.1:8084/purchase
調用服務。
基於初始化數據,和默認的調用邏輯,purchase 將可以被成功調用 3 次。
每次賬戶餘額扣減 3000,由最初的 10000 減少到 1000。
第 4 次調用,因爲賬戶餘額不足,purchase 調用將失敗。相應的:庫存、訂單、賬戶都回滾。
調用一次以後,數據庫中餘額變化,扣了3000塊,30個庫存,多了一條訂單記錄:
mysql> select * from account_tbl;
+----+---------+-------+
| id | user_id | money |
+----+---------+-------+
| 2 | U100000 | 7000 |
+----+---------+-------+
1 row in set (0.01 sec)
mysql> select * from order_tbl;
+----+---------+----------------+-------+-------+
| id | user_id | commodity_code | count | money |
+----+---------+----------------+-------+-------+
| 3 | U100000 | C100000 | 30 | 3000 |
+----+---------+----------------+-------+-------+
1 row in set (0.00 sec)
mysql> select * from storage_tbl;
+----+----------------+-------+
| id | commodity_code | count |
+----+----------------+-------+
| 2 | C100000 | 70 |
+----+----------------+-------+
1 row in set (0.00 sec)
mysql> select * from undo_log;
Empty set (0.00 sec)
當調用第4此時,賬戶餘額不足,賬戶服務扣減餘額失敗;
下單服務失敗,相應的庫存訂單都需要回滾。
讀者可以自行驗證,自第四次調用開始,都不會成功。DB中的結果不會有任何變化。
AT模式原理初步分析
看一下undoLog表的內容,讀者可以在運行過程中添加斷點查看該表日誌,因爲如果等程序運行完成,該表的日誌會被刪除。
選一條日誌的rollback_info看看
{
"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
"xid": "192.168.252.1:8091:2016197280",
"branchId": 2016197281,
"sqlUndoLogs": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType": "UPDATE",
"tableName": "storage_tbl",
"beforeImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "storage_tbl",
"rows": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": 4,
"value": 2
},
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "count",
"keyType": "NULL",
"type": 4,
"value": 10
}
]
]
}
]
]
},
"afterImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "storage_tbl",
"rows": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": 4,
"value": 2
},
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "count",
"keyType": "NULL",
"type": 4,
"value": -20
}
]
]
}
]
]
}
}
]
]
}
看了這個回覆日誌,就很明顯了。undolog中記錄了兩個快照:beforeImage和afterImage。分別記錄了修改前後的字段值,在需要回滾時,就使用beforeImage中記錄的值來回復原始數據即可。
這跟MySQL本身的undolog有異曲同工之妙。
當然實際分佈式處理比這複雜的多,上面只是將最核心的原理介紹一下,接下來文章會詳細分析seata的原理
XA事務實例演示
在使用XA事務時,我發現 mysql-connector-java 還不能改爲8.X 版本。否則會報錯, 無法創建XAConnection:
Caused by: java.sql.SQLFeatureNotSupportedException
at com.alibaba.druid.util.MySqlUtils.createXAConnection(MySqlUtils.java:165)
at io.seata.rm.datasource.util.XAUtils.createXAConnection(XAUtils.java:62)
at io.seata.rm.datasource.util.XAUtils.createXAConnection(XAUtils.java:41)
at io.seata.rm.datasource.xa.DataSourceProxyXA.getConnectionProxy(DataSourceProxyXA.java:63)
at io.seata.rm.datasource.xa.DataSourceProxyXA.getConnection(DataSourceProxyXA.java:49)
at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151)
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115)
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78)
... 63 more
所以將mysql-connector-java的版本還原回去:5.1.48
。
將三個項目中的數據源全部改爲DataSourceProxyXA
實現
@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
啓動服務,依舊訪問http://127.0.0.1:8084/purchase
即可。操作與上面AT一樣,讀者可以自行驗證。
此時由於使用的時XA事務,所以undo_log
表用不到。