今日目標
1.分布式事務問題
1.1.本地事務
本地事務,也就是傳統(tǒng)的單機事務。在傳統(tǒng)數(shù)據(jù)庫事務中,必須要滿足四個原則:
1.2.分布式事務
分布式事務,就是指不是在單個服務或單個數(shù)據(jù)庫架構下,產(chǎn)生的事務,例如:
- 跨數(shù)據(jù)源的分布式事務
- 跨服務的分布式事務
- 綜合情況
在數(shù)據(jù)庫水平拆分、服務垂直拆分之后,一個業(yè)務操作通常要跨多個數(shù)據(jù)庫、服務才能完成。例如電商行業(yè)中比較常見的下單付款案例,包括下面幾個行為:
- 創(chuàng)建新訂單
- 扣減商品庫存
- 從用戶賬戶余額扣除金額
完成上面的操作需要訪問三個不同的微服務和三個不同的數(shù)據(jù)庫。
訂單的創(chuàng)建、庫存的扣減、賬戶扣款在每一個服務和數(shù)據(jù)庫內(nèi)是一個本地事務,可以保證ACID原則。
但是當我們把三件事情看做一個"業(yè)務",要滿足保證“業(yè)務”的原子性,要么所有操作全部成功,要么全部失敗,不允許出現(xiàn)部分成功部分失敗的現(xiàn)象,這就是分布式系統(tǒng)下的事務了。
此時ACID難以滿足,這是分布式事務要解決的問題
1.3.演示分布式事務問題
我們通過一個案例來演示分布式事務的問題:
1)創(chuàng)建數(shù)據(jù)庫,名為seata_demo,然后導入課前資料提供的SQL文件:
2)導入課前資料提供的微服務:
微服務結構如下:
其中:
seata-demo:父工程,負責管理項目依賴
- account-service:賬戶服務,負責管理用戶的資金賬戶。提供扣減余額的接口
- storage-service:庫存服務,負責管理商品庫存。提供扣減庫存的接口
- order-service:訂單服務,負責管理訂單。創(chuàng)建訂單時,需要調(diào)用account-service和storage-service
3)啟動nacos、所有微服務
4)測試下單功能,發(fā)出Post請求:
先嘗試扣減成功的情況
postMan輸入
http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=2&money=200
點擊send后發(fā)現(xiàn)返回200
查看數(shù)據(jù)庫,訂單表,產(chǎn)生數(shù)量是2的訂單
庫存表庫存從10變?yōu)榱?
個人余額從1000變成了800
再嘗試扣減失敗的情況
請求如下:
在postman中訪問
http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=10&money=200
如圖:
點擊send之后,發(fā)現(xiàn)報了500
看下數(shù)據(jù)庫,個人賬戶從800竟然變成了600,這明顯是有問題的
測試發(fā)現(xiàn),當庫存不足時,如果余額已經(jīng)扣減,并不會回滾,出現(xiàn)了分布式事務問題。
分布式服務的事務問題
在分布式系統(tǒng)下,一個業(yè)務跨越多個服務或數(shù)據(jù)源,每個服務都是一個分支事務, 要保證所有分支事務最終狀態(tài)一致,這樣的事務就是分布式事務。
2.理論基礎
解決分布式事務問題,需要一些分布式系統(tǒng)的基礎知識作為理論指導。
2.1.CAP定理
1998年,加州大學的計算機科學家 Eric Brewer 提出,分布式系統(tǒng)有三個指標。
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分區(qū)容錯性)
它們的第一個字母分別是 C、A、P。
Eric Brewer 說,這三個指標不可能同時做到。這個結論就叫做 CAP 定理。
2.1.1.一致性
Consistency(一致性):用戶訪問分布式系統(tǒng)中的任意節(jié)點,得到的數(shù)據(jù)必須一致。
比如現(xiàn)在包含兩個節(jié)點,其中的初始數(shù)據(jù)是一致的:
當我們修改其中一個節(jié)點的數(shù)據(jù)時,兩者的數(shù)據(jù)產(chǎn)生了差異:
要想保住一致性,就必須實現(xiàn)node01 到 node02的數(shù)據(jù) 同步:
2.1.2.可用性
Availability (可用性):用戶訪問集群中的任意健康節(jié)點,必須能得到響應,而不是超時或拒絕。
如圖,有三個節(jié)點的集群,訪問任何一個都可以及時得到響應:
當有部分節(jié)點因為網(wǎng)絡故障或其它原因無法訪問時,代表節(jié)點不可用:
解決方案:在node3節(jié)點沒恢復網(wǎng)絡之前,所有訪問node3的請求全都阻塞住,等到網(wǎng)絡恢復后,再進行數(shù)據(jù)同步和請求響應。
問題:在網(wǎng)絡恢復前,無法保證可用性,響應都阻塞了。這說明了,想要保證一致性,卻犧牲了可用性。
2.1.3.分區(qū)容錯
Partition(分區(qū)):因為網(wǎng)絡故障或其它原因?qū)е路植际较到y(tǒng)中的部分節(jié)點與其它節(jié)點失去連接,形成獨立分區(qū)。
Tolerance(容錯):在集群出現(xiàn)分區(qū)時,整個系統(tǒng)也要持續(xù)對外提供服務
2.1.4.矛盾
在分布式系統(tǒng)中,系統(tǒng)間的網(wǎng)絡不能100%保證健康,一定會有故障的時候,而服務有必須對外保證服務。因此Partition Tolerance不可避免。
當節(jié)點接收到新的數(shù)據(jù)變更時,就會出現(xiàn)問題了:
如果此時要保證一致性,就必須等待網(wǎng)絡恢復,完成數(shù)據(jù)同步后,整個集群才對外提供服務,服務處于阻塞狀態(tài),不可用。
如果此時要保證可用性,就不能等待網(wǎng)絡恢復,那node01、node02與node03之間就會出現(xiàn)數(shù)據(jù)不一致。
也就是說,在P一定會出現(xiàn)的情況下,A和C之間只能實現(xiàn)一個。
總結:
簡述CAP定理內(nèi)容?
● 分布式系統(tǒng)節(jié)點通過網(wǎng)絡連接,一定會出現(xiàn)分區(qū)問題§
● 當分區(qū)出現(xiàn)時,系統(tǒng)的一致性?和可用性(A)就無法同時滿足
思考: elasticsearch集群 是CP還是AP?
答:CP
ES集群出現(xiàn)分區(qū)時,故障節(jié)點會被剔除集群,數(shù)據(jù)分片會重新分配到其它節(jié)點,保證數(shù)據(jù)一致。因此是低可用性,高一致性,屬于CP
2.2.BASE理論
BASE理論是對CAP的一種解決思路,包含三個思想:
- Basically Available (基本可用):分布式系統(tǒng)在出現(xiàn)故障時,允許損失部分可用性,即保證核心可用。
- **Soft State(軟狀態(tài)):**在一定時間內(nèi),允許出現(xiàn)中間狀態(tài),比如臨時的不一致狀態(tài)。
- Eventually Consistent(最終一致性):雖然無法保證強一致性,但是在軟狀態(tài)結束后,最終達到數(shù)據(jù)一致。
2.3.解決分布式事務的思路
分布式事務最大的問題是各個子事務的一致性問題,因此可以借鑒CAP定理和BASE理論,有兩種解決思路:
-
AP模式:各子事務分別執(zhí)行和提交,允許出現(xiàn)結果不一致,然后采用彌補措施恢復數(shù)據(jù)即可,實現(xiàn)最終一致。
-
CP模式:各個子事務執(zhí)行后互相等待,同時提交,同時回滾,達成強一致。但事務等待過程中,處于弱可用狀態(tài)。
但不管是哪一種模式,都需要在子系統(tǒng)事務之間互相通訊,協(xié)調(diào)事務狀態(tài),也就是需要一個事務協(xié)調(diào)者(TC):
這里的子系統(tǒng)事務,稱為分支事務;有關聯(lián)的各個分支事務在一起稱為全局事務。
總結:
簡述BASE理論三個思想:
● 基本可用
● 軟狀態(tài)
● 最終一致
解決分布式事務的思想和模型:
3.初識Seata
Seata是 2019 年 1 月份螞蟻金服和阿里巴巴共同開源的分布式事務解決方案。致力于提供高性能和簡單易用的分布式事務服務,為用戶打造一站式的分布式解決方案。
官網(wǎng)地址:http://seata.io/,其中的文檔、播客中提供了大量的使用說明、源碼分析。
3.1.Seata的架構
Seata事務管理中有三個重要的角色:
-
TC (Transaction Coordinator) - **事務協(xié)調(diào)者:**維護全局和分支事務的狀態(tài),協(xié)調(diào)全局事務提交或回滾。
-
TM (Transaction Manager) - **事務管理器:**定義全局事務的范圍、開始全局事務、提交或回滾全局事務。
-
RM (Resource Manager) - **資源管理器:**管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態(tài),并驅(qū)動分支事務提交或回滾。
整體的架構如圖:
Seata基于上述架構提供了四種不同的分布式事務解決方案:
- XA模式:強一致性分階段事務模式,犧牲了一定的可用性,無業(yè)務侵入
- TCC模式:最終一致的分階段事務模式,有業(yè)務侵入
- AT模式:最終一致的分階段事務模式,無業(yè)務侵入,也是Seata的默認模式
- SAGA模式:長事務模式,有業(yè)務侵入
無論哪種方案,都離不開TC,也就是事務的協(xié)調(diào)者。
3.2.部署TC服務
參考課前資料提供的文檔《 seata的部署和集成.md 》:
seata的部署和集成
一、部署Seata的tc-server
1.下載
首先我們要下載seata-server包,地址在http??/seata.io/zh-cn/blog/download.html
當然,課前資料也準備好了:
2.解壓
在非中文目錄解壓縮這個zip包,其目錄結構如下:
3.修改配置
修改conf目錄下的registry.conf文件:
內(nèi)容如下:
registry {
# tc服務的注冊中心類,這里選擇nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服務注冊到 nacos的服務名稱,可以自定義
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 讀取tc服務端的配置文件的方式,這里是從nacos配置中心讀取,這樣如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
? group = “DEFAULT_GROUP” :與Nacos中分組一致
4.在nacos添加配置
特別注意,為了讓tc服務的集群可以共享配置,我們選擇了nacos作為統(tǒng)一配置中心。因此服務端配置文件seataServer.properties文件需要在nacos中配好。
格式如下:
DataID: seataServer.properties
Group:DEFAULT_GROUP
配置格式:Properties
配置內(nèi)容如下:
# 數(shù)據(jù)存儲方式,db代表數(shù)據(jù)庫
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_tc?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事務、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客戶端與服務端傳輸方式
transport.serialization=seata
transport.compressor=none
# 關閉metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
其中的數(shù)據(jù)庫地址、用戶名、密碼都需要修改成你自己的數(shù)據(jù)庫信息。
5.創(chuàng)建數(shù)據(jù)庫表
特別注意:tc服務在管理分布式事務時,需要記錄事務相關數(shù)據(jù)到數(shù)據(jù)庫中,你需要提前創(chuàng)建好這些表。
新建一個名為seata_tc的數(shù)據(jù)庫,運行課前資料提供的sql文件:
這里數(shù)據(jù)庫名稱和上面nacos中配置文件的數(shù)據(jù)庫名稱一致即可
這些表主要記錄全局事務、分支事務、全局鎖信息:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事務表
-- ----------------------------
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(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) 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 = Compact;
-- ----------------------------
-- 全局事務表
-- ----------------------------
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(32) 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 NULL DEFAULT NULL,
`gmt_modified` datetime 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 = Compact;
SET FOREIGN_KEY_CHECKS = 1;
6.啟動TC服務
進入bin目錄,運行其中的seata-server.bat即可:
默認端口是8091
啟動成功后,seata-server應該已經(jīng)注冊到nacos注冊中心了。
打開瀏覽器,訪問nacos地址:http://localhost:8848,然后進入服務列表頁面,可以看到seata-tc-server的信息:
點擊進去看到詳情
二、微服務集成seata
1.引入依賴
首先,我們需要在微服務中引入seata依賴:
參與微服務的所有模塊pom.xml都要加上
<!-- 版本信息 父工程指定即可 -->
<properties>
<seata.version>1.4.2</seata.version>
</properties>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本較低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
2.修改配置文件
需要修改application.yml文件,添加一些配置:
把所有服務模塊的application.yml文件都修改一下
seata:
registry: # TC服務注冊中心的配置,微服務根據(jù)這些信息去注冊中心獲取tc服務地址
# 參考tc服務自己的registry.conf中的配置
type: nacos
nacos: # tc
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server # tc服務在nacos中的服務名稱
username: nacos
password: nacos
tx-service-group: seata-demo # 事務組,根據(jù)這個獲取tc服務的cluster名稱
service:
vgroup-mapping: # 事務組與TC服務cluster的映射關系
seata-demo: SH
配置好之后,重啟服務即可,看到seata中顯示RM register success即成功了
三、TC服務的高可用和異地容災
1.模擬異地容災的TC集群
計劃啟動兩臺seata的tc服務節(jié)點:
節(jié)點名稱 | ip地址 | 端口號 | 集群名稱 |
---|---|---|---|
seata | 127.0.0.1 | 8091 | SH |
seata2 | 127.0.0.1 | 8092 | HZ |
之前我們已經(jīng)啟動了一臺seata服務,端口是8091,集群名為SH。
現(xiàn)在,將seata目錄復制一份,起名為seata2
修改seata2/conf/registry.conf內(nèi)容如下:
registry {
# tc服務的注冊中心類,這里選擇nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服務注冊到 nacos的服務名稱,可以自定義
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "HZ"
username = "nacos"
password = "nacos"
}
}
config {
# 讀取tc服務端的配置文件的方式,這里是從nacos配置中心讀取,這樣如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
進入seata2/bin目錄,然后運行命令:
seata-server.bat -p 8092
打開nacos控制臺,查看服務列表:
點進詳情查看:
2.將事務組映射配置到nacos
接下來,我們需要將tx-service-group與cluster的映射關系都配置到nacos配置中心。
新建一個配置:
配置的內(nèi)容如下:
# 事務組映射關系
service.vgroupMapping.seata-demo=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
# 與TC服務的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
3.微服務讀取nacos配置
接下來,需要修改每一個微服務的application.yml文件,讓微服務讀取nacos中的client.properties文件:
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
data-id: client.properties
重啟微服務,現(xiàn)在微服務到底是連接tc的SH集群,還是tc的HZ集群,都統(tǒng)一由nacos的client.properties來決定了。
3.3.微服務集成Seata
我們以order-service為例來演示。
3.3.1.引入依賴
首先,在order-service中引入依賴:
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本較低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<!--seata starter 采用1.4.2版本-->
<version>${seata.version}</version>
</dependency>
3.3.2.配置TC地址
在order-service中的application.yml中,配置TC服務信息,通過注冊中心nacos,結合服務名稱獲取TC地址:
seata:
registry: # TC服務注冊中心的配置,微服務根據(jù)這些信息去注冊中心獲取tc服務地址
type: nacos # 注冊中心類型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "" # namespace,默認為空
group: DEFAULT_GROUP # 分組,默認是DEFAULT_GROUP
application: seata-tc-server # seata服務名稱
username: nacos
password: nacos
tx-service-group: seata-demo # 事務組名稱
service:
vgroup-mapping: # 事務組與cluster的映射關系
seata-demo: SH
微服務如何根據(jù)這些配置尋找TC的地址呢?
我們知道注冊到Nacos中的微服務,確定一個具體實例需要四個信息:
- namespace:命名空間
- group:分組
- application:服務名
- cluster:集群名
以上四個信息,在剛才的yaml文件中都能找到:
namespace為空,就是默認的public
結合起來,TC服務的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,這樣就能確定TC服務集群了。然后就可以去Nacos拉取對應的實例信息了。
3.3.3.其它服務
其它兩個微服務也都參考order-service的步驟來做,完全一樣。
4.動手實踐
下面我們就一起學習下Seata中的四種不同的事務模式。
4.1.XA模式
XA 規(guī)范 是 X/Open 組織定義的分布式事務處理(DTP,Distributed Transaction Processing)標準,XA 規(guī)范 描述了全局的TM與局部的RM之間的接口,幾乎所有主流的數(shù)據(jù)庫都對 XA 規(guī)范 提供了支持。
4.1.1.兩階段提交
XA是規(guī)范,目前主流數(shù)據(jù)庫都實現(xiàn)了這種規(guī)范,實現(xiàn)的原理都是基于兩階段提交。
正常情況:
異常情況:
一階段:
- 事務協(xié)調(diào)者通知每個事物參與者執(zhí)行本地事務
- 本地事務執(zhí)行完成后報告事務執(zhí)行狀態(tài)給事務協(xié)調(diào)者,此時事務不提交,繼續(xù)持有數(shù)據(jù)庫鎖
二階段:
- 事務協(xié)調(diào)者基于一階段的報告來判斷下一步操作
- 如果一階段都成功,則通知所有事務參與者,提交事務
- 如果一階段任意一個參與者失敗,則通知所有事務參與者回滾事務。
4.1.2.Seata的XA模型
Seata對原始的XA模式做了簡單的封裝和改造,以適應自己的事務模型,基本架構如圖:
RM一階段的工作:
? ① 注冊分支事務到TC
? ② 執(zhí)行分支業(yè)務sql但不提交
? ③ 報告執(zhí)行狀態(tài)到TC
TC二階段的工作:
-
TC檢測各分支事務執(zhí)行狀態(tài)
a.如果都成功,通知所有RM提交事務
b.如果有失敗,通知所有RM回滾事務
RM二階段的工作:
- 接收TC指令,提交或回滾事務
4.1.3.優(yōu)缺點
XA模式的優(yōu)點是什么?
- 事務的強一致性,滿足ACID原則。
- 常用數(shù)據(jù)庫都支持,實現(xiàn)簡單,并且沒有代碼侵入
XA模式的缺點是什么?
- 因為一階段需要鎖定數(shù)據(jù)庫資源,等待二階段結束才釋放,性能較差
- 依賴關系型數(shù)據(jù)庫實現(xiàn)事務
4.1.4.實現(xiàn)XA模式
Seata的starter已經(jīng)完成了XA模式的自動裝配,實現(xiàn)非常簡單,步驟如下:
1)修改application.yml文件(每個參與事務的微服務),開啟XA模式:
seata:
data-source-proxy-mode: XA
2)給發(fā)起全局事務的入口方法添加@GlobalTransactional注解:
本例中是OrderServiceImpl中的create方法.
3)重啟所有服務并測試
重啟order-service,再次測試,發(fā)現(xiàn)無論怎樣,三個微服務都能成功回滾。
先看下數(shù)據(jù)庫目前的狀態(tài)
個人賬戶余額600
庫存數(shù)量8
用postman發(fā)送訂單,成功的
點擊send
查看數(shù)據(jù)庫,個人賬戶余額400
庫存數(shù)量6
沒問題,然后我們嘗試一下失敗的情況
點擊send
查看數(shù)據(jù)庫,個人賬戶余額不變
庫存也不變
事務的一致性得到了保障
4.2.AT模式
AT模式同樣是分階段提交的事務模型,不過缺彌補了XA模型中資源鎖定周期過長的缺陷。
4.2.1.Seata的AT模型
基本流程圖:
階段一RM的工作:
- 注冊分支事務
- 記錄undo-log(數(shù)據(jù)快照)
- 執(zhí)行業(yè)務sql并提交
- 報告事務狀態(tài)
階段二提交時RM的工作:
- 刪除undo-log即可
階段二回滾時RM的工作:
- 根據(jù)undo-log恢復數(shù)據(jù)到更新前
- 再刪除undo-log
4.2.2.流程梳理
我們用一個真實的業(yè)務來梳理下AT模式的原理。
比如,現(xiàn)在又一個數(shù)據(jù)庫表,記錄用戶余額:
id | money |
---|---|
1 | 100 |
其中一個分支業(yè)務要執(zhí)行的SQL為:
update tb_account set money = money - 10 where id = 1
AT模式下,當前分支事務執(zhí)行流程如下:
一階段:
1)TM發(fā)起并注冊全局事務到TC
2)TM調(diào)用分支事務
3)分支事務準備執(zhí)行業(yè)務SQL
4)RM攔截業(yè)務SQL,根據(jù)where條件查詢原始數(shù)據(jù),形成快照。
{
"id": 1, "money": 100
}
5)RM執(zhí)行業(yè)務SQL,提交本地事務,釋放數(shù)據(jù)庫鎖。此時 money = 90
6)RM報告本地事務狀態(tài)給TC
二階段:
1)TM通知TC事務結束
2)TC檢查分支事務狀態(tài)
? a)如果都成功,則立即刪除快照
? b)如果有分支事務失敗,需要回滾。讀取快照數(shù)據(jù)({"id": 1, "money": 100}
),將快照恢復到數(shù)據(jù)庫。此時數(shù)據(jù)庫再次恢復為100
流程圖:
4.2.3.AT與XA的區(qū)別
簡述AT模式與XA模式最大的區(qū)別是什么?
- XA模式一階段不提交事務,鎖定資源;AT模式一階段直接提交,不鎖定資源。
- XA模式依賴數(shù)據(jù)庫機制實現(xiàn)回滾;AT模式利用數(shù)據(jù)快照實現(xiàn)數(shù)據(jù)回滾。
- XA模式強一致;AT模式最終一致
4.2.4.臟寫問題
在多線程并發(fā)訪問AT模式的分布式事務時,有可能出現(xiàn)臟寫問題,如圖:
解決思路就是引入了全局鎖的概念。在釋放DB鎖之前,先拿到全局鎖。避免同一時刻有另外一個事務來操作當前數(shù)據(jù)。
4.2.5.優(yōu)缺點
AT模式的優(yōu)點:
- 一階段完成直接提交事務,釋放數(shù)據(jù)庫資源,性能比較好
- 利用全局鎖實現(xiàn)讀寫隔離
- 沒有代碼侵入,框架自動完成回滾和提交
AT模式的缺點:
- 兩階段之間屬于軟狀態(tài),屬于最終一致
- 框架的快照功能會影響性能,但比XA模式要好很多
4.2.6.實現(xiàn)AT模式
AT模式中的快照生成、回滾等動作都是由框架自動完成,沒有任何代碼侵入,因此實現(xiàn)非常簡單。
只不過,AT模式需要一個表來記錄全局鎖、另一張表來記錄數(shù)據(jù)快照undo_log。
1)導入數(shù)據(jù)庫表,記錄全局鎖
導入課前資料提供的Sql文件:seata-at.sql,其中l(wèi)ock_table導入到TC服務關聯(lián)的數(shù)據(jù)庫,undo_log表導入到微服務關聯(lián)的數(shù)據(jù)庫:
lock_table表:放入seata_tc數(shù)據(jù)庫
undo_log表:放入seata_demo數(shù)據(jù)庫
添加后如下:
2)修改application.yml文件,將事務模式修改為AT模式即可:
seata:
data-source-proxy-mode: AT # 默認就是AT
3)重啟所有微服務并測試
我們用postman直接測試失敗的情況,初始情況余額是400,庫存是6
點擊send,發(fā)現(xiàn)報錯500
查看余額,還是400沒問題
查看庫存還是6沒問題
4.3.TCC模式
TCC模式與AT模式非常相似,每階段都是獨立事務,不同的是TCC通過人工編碼來實現(xiàn)數(shù)據(jù)恢復。需要實現(xiàn)三個方法:
- Try:資源的檢測和預留;
- Confirm:完成資源操作業(yè)務;要求 Try 成功 Confirm 一定要能成功。
- Cancel:預留資源釋放,可以理解為try的反向操作。
4.3.1.流程分析
舉例,一個扣減用戶余額的業(yè)務。假設賬戶A原來余額是100,需要余額扣減30元。
- 階段一( Try ):檢查余額是否充足,如果充足則凍結金額增加30元,可用余額扣除30
初識余額:
余額充足,可以凍結:
此時,總金額 = 凍結金額 + 可用金額,數(shù)量依然是100不變。事務直接提交無需等待其它事務。
- 階段二(Confirm):假如要提交(Confirm),則凍結金額扣減30
確認可以提交,不過之前可用金額已經(jīng)扣減過了,這里只要清除凍結金額就好了:
此時,總金額 = 凍結金額 + 可用金額 = 0 + 70 = 70元
- 階段二(Canncel):如果要回滾(Cancel),則凍結金額扣減30,可用余額增加30
需要回滾,那么就要釋放凍結金額,恢復可用金額:
4.3.2.Seata的TCC模型
Seata中的TCC模型依然延續(xù)之前的事務架構,如圖:
4.3.3.優(yōu)缺點
TCC模式的每個階段是做什么的?
- Try:資源檢查和預留
- Confirm:業(yè)務執(zhí)行和提交
- Cancel:預留資源的釋放
TCC的優(yōu)點是什么?
- 一階段完成直接提交事務,釋放數(shù)據(jù)庫資源,性能好
- 相比AT模型,無需生成快照,無需使用全局鎖,性能最強
- 不依賴數(shù)據(jù)庫事務,而是依賴補償操作,可以用于非事務型數(shù)據(jù)庫
TCC的缺點是什么?
- 有代碼侵入,需要人為編寫try、Confirm和Cancel接口,太麻煩
- 軟狀態(tài),事務是最終一致
- 需要考慮Confirm和Cancel的失敗情況,做好冪等處理
4.3.4.事務懸掛和空回滾
1)空回滾
當某分支事務的try階段阻塞時,可能導致全局事務超時而觸發(fā)二階段的cancel操作。在未執(zhí)行try操作時先執(zhí)行了cancel操作,這時cancel不能做回滾,就是空回滾。
如圖:
執(zhí)行cancel操作時,應當判斷try是否已經(jīng)執(zhí)行,如果尚未執(zhí)行,則應該空回滾。
2)業(yè)務懸掛
對于已經(jīng)空回滾的業(yè)務,之前被阻塞的try操作恢復,繼續(xù)執(zhí)行try,就永遠不可能confirm或cancel ,事務一直處于中間狀態(tài),這就是業(yè)務懸掛。
執(zhí)行try操作時,應當判斷cancel是否已經(jīng)執(zhí)行過了,如果已經(jīng)執(zhí)行,應當阻止空回滾后的try操作,避免懸掛
4.3.5.實現(xiàn)TCC模式
解決空回滾和業(yè)務懸掛問題,必須要記錄當前事務狀態(tài),是在try、還是cancel?
1)思路分析
這里我們定義一張分支事務表:
在seata-demo中執(zhí)行
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '凍結金額',
`state` int(1) DEFAULT NULL COMMENT '事務狀態(tài),0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
其中:
- xid:是全局事務id
- freeze_money:用來記錄用戶凍結金額
- state:用來記錄事務狀態(tài)
執(zhí)行后如下:
那此時,我們的業(yè)務開怎么做呢?
- Try業(yè)務:
- 記錄凍結金額和事務狀態(tài)到account_freeze表
- 扣減account表可用金額
- Confirm業(yè)務
- 根據(jù)xid刪除account_freeze表的凍結記錄
- Cancel業(yè)務
- 修改account_freeze表,凍結金額為0,state為2
- 修改account表,恢復可用金額
- 如何判斷是否空回滾?
- cancel業(yè)務中,根據(jù)xid查詢account_freeze,如果為null則說明try還沒做,需要空回滾
- 如何避免業(yè)務懸掛?
- try業(yè)務中,根據(jù)xid查詢account_freeze ,如果已經(jīng)存在則證明Cancel已經(jīng)執(zhí)行,拒絕執(zhí)行try業(yè)務
接下來,我們改造account-service,利用TCC實現(xiàn)余額扣減功能。
2)聲明TCC接口
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解來聲明,
我們在account-service項目中的cn.itcast.account.service
包中新建一個接口,聲明TCC三個接口:
package cn.itcast.account.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
3)編寫實現(xiàn)類
在account-service服務中的cn.itcast.account.service.impl
包下新建一個類,實現(xiàn)TCC業(yè)務:
package cn.itcast.account.service.impl;
import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 0.獲取事務id
String xid = RootContext.getXID();
// 1.扣減可用余額
accountMapper.deduct(userId, money);
// 2.記錄凍結金額,事務狀態(tài)
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
// 1.獲取事務id
String xid = ctx.getXid();
// 2.根據(jù)id刪除凍結記錄
int count = freezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 0.查詢凍結記錄
String xid = ctx.getXid();
AccountFreeze freeze = freezeMapper.selectById(xid);
// 1.恢復可用余額
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 2.將凍結金額清零,狀態(tài)改為CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
}
修改AccountController.java
@Autowired
private AccountTCCService accountService;
重啟服務測試即可
目前數(shù)據(jù)庫中余額400,庫存6
執(zhí)行postman
點擊send,發(fā)現(xiàn)報錯500
看下啊數(shù)據(jù)庫
freeze表中有回滾記錄
余額還是400
庫存依然是6
4.4.SAGA模式
Saga 模式是 Seata 即將開源的長事務解決方案,將由螞蟻金服主要貢獻。
其理論基礎是Hector & Kenneth 在1987年發(fā)表的論文Sagas。
Seata官網(wǎng)對于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html
4.4.1.原理
在 Saga 模式下,分布式事務內(nèi)有多個參與者,每一個參與者都是一個沖正補償服務,需要用戶根據(jù)業(yè)務場景實現(xiàn)其正向操作和逆向回滾操作。
分布式事務執(zhí)行過程中,依次執(zhí)行各參與者的正向操作,如果所有正向操作均執(zhí)行成功,那么分布式事務提交。如果任何一個正向操作執(zhí)行失敗,那么分布式事務會去退回去執(zhí)行前面各參與者的逆向回滾操作,回滾已提交的參與者,使分布式事務回到初始狀態(tài)。
Saga也分為兩個階段:
- 一階段:直接提交本地事務
- 二階段:成功則什么都不做;失敗則通過編寫補償業(yè)務來回滾
4.4.2.優(yōu)缺點
優(yōu)點:
- 事務參與者可以基于事件驅(qū)動實現(xiàn)異步調(diào)用,吞吐高
- 一階段直接提交事務,無鎖,性能好
- 不用編寫TCC中的三個階段,實現(xiàn)簡單
缺點:
- 軟狀態(tài)持續(xù)時間不確定,時效性差
- 沒有鎖,沒有事務隔離,會有臟寫
4.5.四種模式對比
我們從以下幾個方面來對比四種實現(xiàn):
- 一致性:能否保證事務的一致性?強一致還是最終一致?
- 隔離性:事務之間的隔離性如何?
- 代碼侵入:是否需要對業(yè)務代碼改造?
- 性能:有無性能損耗?
- 場景:常見的業(yè)務場景
如圖:
5.高可用
Seata的TC服務作為分布式事務核心,一定要保證集群的高可用性。
5.1.高可用架構模型
搭建TC服務集群非常簡單,啟動多個TC服務,注冊到nacos即可。
但集群并不能確保100%安全,萬一集群所在機房故障怎么辦?所以如果要求較高,一般都會做異地多機房容災。
比如一個TC集群在上海,另一個TC集群在杭州:
微服務基于事務組(tx-service-group)與TC集群的映射關系,來查找當前應該使用哪個TC集群。當SH集群故障時,只需要將vgroup-mapping中的映射關系改成HZ。則所有微服務就會切換到HZ的TC集群了。
5.2.實現(xiàn)高可用
具體實現(xiàn)請參考課前資料提供的文檔《seata的部署和集成.md》:文章來源:http://www.zghlxwxcb.cn/news/detail-404262.html
第三章節(jié):文章來源地址http://www.zghlxwxcb.cn/news/detail-404262.html
到了這里,關于SpringCloud微服務技術棧.黑馬跟學(九)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!