1. 基礎概念
1.1 什麼是事務
事務可以看做是一次大的活動,它由不同的小活動組成,這些活動要麼全成功,要麼全失敗。比如:一手交錢,一手交貨。
1.2 本地事務
在計算機系統中,更多的是通過關係型數據庫來控制事務,這是利用數據庫本身的事務特性來實現的,因此叫數據
庫事務,由於應用主要靠關係數據庫來控制事務,而數據庫通常和應用在同一個服務器,所以基於關係型數據庫的
事務又被稱爲本地事務。
數據庫事務在實現時會將一次事務涉及的所有操作全部納入到一個不可分割的執行單元,該執行單元中的所有操作
要麼都成功,要麼都失敗,只要其中任一操作執行失敗,都將導致整個事務的回滾。
1.3 分佈式事務
分佈式系統會把一個應用系統拆分爲可獨立部署的多個服務,因此需要服務與服務之間遠程協作才能完成事務操
作,這種分佈式系統環境下由不同的服務之間通過網絡遠程協作完成事務稱之爲分佈式事務,例如商品入庫單生效後,增加商品庫存就是一個分佈式事務問題。
2. 分佈式事務解決方案
目前業界常見的解決方案有2PC、TCC、可靠消息最終一致性、最大努力通知這四種。
- 2PC
2PC即兩階段提交協議,是將整個事務流程分爲兩個階段,準備階段(Prepare phase)、提交階段(commit phase),2是指兩個階段,P是指準備階段,C是指提交階段。
- TCC
TCC是Try、Confirm、Cancel三個詞語的縮寫,TCC要求每個分支事務實現三個操作:預處理Try、確認Confirm、撤銷Cancel。Try操作做業務檢查及資源預留,Confirm做業務確認操作,Cancel實現一個與Try相反的操作即回滾操作。TM首先發起所有的分支事務的try操作,任何一個分支事務的try操作執行失敗,TM將會發起所有分支事務的Cancel操作,若try操作全部成功,TM將會發起所有分支事務的Confirm操作,其中Confirm/Cancel操作若執行失敗,TM會進行重試。
- 可靠消息最終一致性
可靠消息最終一致性方案是指當事務發起方執行完成本地事務後併發出一條消息,事務參與方(消息消費者)一定能夠接收消息並處理事務成功,此方案強調的是隻要消息發給事務參與方最終事務要達到一致。
- 最大努力通知
最大努力通知是指當A、B兩個事務中,B的執行結果會以異步的方式通知A,若A此時無法接收到通知結果,則B會有一個間隔的時間段,再次通知A執行結果,依次類推,知道若干次後,要麼通知成功;要麼不再推送,由A主動來查詢執行結果。
3. Seata實現2PC事務
3.1 Seata方案
Seata是由阿里中間件團隊發起的開源項目 Fescar,後更名爲Seata,它是一個是開源的分佈式事務框架。它通過對本地關係數據庫的分支事務的協調來驅動完成全局事務,是工作在應用層的中間件。主要優點是性能較好,且不長時間佔用連接資源,它以高效並且對業務0侵入的方式解決微服務場景下面臨的分佈式事務問題。
Seata把一個分佈式事務理解成一個包含了若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一致,要麼一起成功提交,要麼一起失敗回滾。此外,通常分支事務本身就是一個關係數據庫的本地事務,下圖是全局事務與分支事務的關係圖:
Seata定義了3個組件來協調分佈式事務的處理過程:
- Transaction Coordinator (TC): 事務協調器,它是獨立的中間件,需要獨立部署運行,它維護全局事務的運行狀態,接收TM指令發起全局事務的提交與回滾,負責與RM通信協調各個分支事務的提交或回滾。
- Transaction Manager ™: 事務管理器,TM需要嵌入應用程序中工作,它負責開啓一個全局事務,並最終向TC發起全局提交或全局回滾的指令。
- Resource Manager (RM): 控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器TC的指令,驅動分支(本地)事務的提交和回滾。
3.2 業務背景
在商品入庫單生效時,我們要調用基礎微服務的庫存模塊,對入庫單中的商品進行入庫,也就是增加庫存的操作。試想一下,如果增加庫存失敗,那商品入庫單必然是不能生效成功的。這就是分佈式事務的場景。
3.3 實現步驟
3.3.1 下載Seata服務器
下載地址:https://github.com/seata/seata/releases,目前最新版本:1.2.0,我們使用的是1.1.0。
3.3.2 如何運行
解壓後,進入bin目錄,然後雙擊seata-server.bat
文件即可。
3.3.3 添加配置
Seata有兩個比較重要的配置文件,分別在conf文件夾下的file.conf
、registry.conf
,我們的微服務想要連接到Seata的服務就要將這兩個配置文件配置到微服務工程的resources
文件夾去。
registry.conf
註冊到Seata服務的配置文件,包含了註冊方式、配置文件讀取方式等等,具體的大家看源碼中的配置。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
# 這裏使用eureka方式註冊
eureka {
serviceUrl = "http://Anbang713:pwd713@localhost:9010/eureka"
application = "default"
weight = "1"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
# 使用文件方式進行配置
file {
name = "file.conf"
}
}
file.conf
該配置文件內包含了微服務事務方的一些配置,以及Seata服務的事務日誌存儲方式。
service {
#transaction service group mapping
vgroup_mapping.mall-product-provider-fescar-service-group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3380/seata"
user = "root"
password = "Anbang713"
minConn = 1
maxConn = 10
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
需要注意的是,這兩個文件要放在事務參與方所在的微服務中。比如商品入庫單生效增加商品庫存這一個場景,商品入庫單在商品微服務,商品庫存在基礎微服務。
3.3.4 創建表
在數據庫中創建seata
數據庫,然後初始化以下表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` BIGINT(20) NULL DEFAULT NULL,
`resource_group_id` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` VARCHAR(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` VARCHAR(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` TINYINT(4) NULL DEFAULT NULL,
`client_id` VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` VARCHAR(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` DATETIME(0) NULL DEFAULT NULL,
`gmt_modified` DATETIME(0) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` BIGINT(20) NULL DEFAULT NULL,
`status` TINYINT(4) NOT NULL,
`application_id` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` INT(11) NULL DEFAULT NULL,
`begin_time` BIGINT(20) NULL DEFAULT NULL,
`application_data` VARCHAR(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` DATETIME(0) NULL DEFAULT NULL,
`gmt_modified` DATETIME(0) NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` VARCHAR(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` BIGINT(20) NULL DEFAULT NULL,
`branch_id` BIGINT(20) NOT NULL,
`resource_id` VARCHAR(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` VARCHAR(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` DATETIME(0) NULL DEFAULT NULL,
`gmt_modified` DATETIME(0) NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`context` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME(0) NOT NULL,
`log_modified` DATETIME(0) NOT NULL,
`ext` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
在商品微服務和基礎微服務對應的數據庫分別創建undo_log
表:
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`context` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME(0) NOT NULL,
`log_modified` DATETIME(0) NOT NULL,
`ext` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
3.3.5 代碼開發
1)添加pom依賴
<!--引入seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.0.0</version>
</dependency>
2)添加@GlobalTransactional註解
在事務發起方添加@GlobalTransactional
註解,比如對於商品入庫單生效增加庫存,事務的發起方就是商品入庫單生效的方法。
@Override
@Transactional
@GlobalTransactional(rollbackFor = Exception.class)
public void doEffect(String uuid) {
// 商品入庫單生效
// 調用基礎微服務的庫存服務增加庫存
}
至此,使用Seata分佈式事務解決方案介紹結束。實際上,在寫這篇博客之前想了很久,因爲這個分佈式事務這個話題太多,業界也有很多的解決方案,很難在一篇幾千字的博文裏說清楚,這裏也只能介紹一下我們項目中是怎麼解決分佈式事務的,而且這種東西還是要大家自己動手實踐才能感受這整個過程到底是怎麼回事。