2PC,全稱為兩階段提交(Two-Phase Commit),是一種在分布式系統(tǒng)中用來保證事務(wù)原子性和一致性的協(xié)議。它主要用于協(xié)調(diào)分布式數(shù)據(jù)庫或分布式事務(wù)環(huán)境中的多個(gè)參與者,確保所有參與者要么一起成功提交事務(wù),要么一起回滾事務(wù),以保持?jǐn)?shù)據(jù)的一致性。
圖片
在2PC協(xié)議中有兩個(gè)主要階段:
事務(wù)協(xié)調(diào)器接收到發(fā)起事務(wù)的客戶端請求后,向所有參與該事務(wù)的資源管理器(例如數(shù)據(jù)庫、服務(wù)節(jié)點(diǎn)等)發(fā)送“準(zhǔn)備提交”請求。
每個(gè)資源管理器執(zhí)行事務(wù)操作,并將事務(wù)相關(guān)的更改鎖定但不提交,然后回復(fù)事務(wù)協(xié)調(diào)器它們是否準(zhǔn)備好提交事務(wù)(根據(jù)各自是否能夠成功完成事務(wù)而定)。
如果事務(wù)協(xié)調(diào)器收到了所有資源管理器的肯定答復(fù),即所有參與者都準(zhǔn)備好提交事務(wù),則向所有參與者發(fā)出“正式提交”指令。
若協(xié)調(diào)器收到任何一個(gè)參與者的否定響應(yīng),或者在等待超時(shí)后仍有參與者未響應(yīng),則向所有參與者發(fā)出“回滾事務(wù)”的指令。
通過這種方式,2PC確保了所有節(jié)點(diǎn)要么全部完成事務(wù),要么全部撤銷事務(wù),從而維護(hù)了分布式環(huán)境下的事務(wù)原子性。然而,2PC也存在一些缺點(diǎn),比如單點(diǎn)故障問題(即事務(wù)協(xié)調(diào)器宕機(jī)可能導(dǎo)致事務(wù)長期阻塞)、網(wǎng)絡(luò)分區(qū)情況下的不確定性以及性能上的潛在瓶頸。
Seata把一個(gè)分布式事務(wù)理解成一個(gè)包含了若干分支事務(wù)的全局事務(wù)。全局事務(wù)的職責(zé)是協(xié)調(diào)其下管轄的分支事務(wù) 達(dá)成一致,要么一起成功提交,要么一起失敗回滾。此外,通常分支事務(wù)本身就是一個(gè)關(guān)系數(shù)據(jù)庫的本地事務(wù),下圖是全局事務(wù)與分支事務(wù)的關(guān)系圖:
圖片
與 傳統(tǒng)2PC 的模型類似,Seata定義了3個(gè)組件來協(xié)議分布式事務(wù)的處理過程
圖片
案例分析:兩個(gè)賬戶在不同的銀行(張三在bank1、李四在bank2),bank1和bank2是兩個(gè)微服務(wù)。交易過程是,張三給李四轉(zhuǎn)賬指定金額。
上述交易步驟,要么一起成功,要么一起失敗,必須是一個(gè)整體性的事務(wù)。
圖片
為了簡化環(huán)境搭建,小編這里采用file啟動(dòng)seata,項(xiàng)目搭建也只是兩個(gè)普通的SpringBoot項(xiàng)目,未使用微服務(wù)。
官方下載地址:https://github.com/seata/seata/releases
registry.type=file 其類型設(shè)置為 file 時(shí),意味著 Seata 的服務(wù)注冊中心不依賴于外部的如 Nacos、Eureka、Zookeeper 等第三方注冊中心,而是使用本地文件的方式來存儲和管理服務(wù)節(jié)點(diǎn)信息。這種模式主要用于快速測試或簡單的單機(jī)部署場景,因?yàn)樵谶@種模式下無法自動(dòng)發(fā)現(xiàn)和管理集群環(huán)境中的其他 Seata Server 節(jié)點(diǎn),不具備高可用性。
config.type=file 表示 Seata 使用本地文件作為配置源。這意味著 Seata 會(huì)從指定的本地文件中讀取全局事務(wù)協(xié)調(diào)器(TC)、事務(wù)管理器(TM)和資源管理器(RM)等組件所需的配置信息,而不是通過Nacos、Apollo或其他遠(yuǎn)程配置中心獲取配置。這種方式同樣適用于快速驗(yàn)證和簡單部署情況,實(shí)際生產(chǎn)環(huán)境中可能需要結(jié)合分布式配置中心來動(dòng)態(tài)更新和管理配置。
圖片
圖片
CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';CREATE TABLE `account_info` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '戶主姓名',`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '銀行卡號',`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帳戶密碼',`account_balance` double NULL DEFAULT NULL COMMENT '帳戶余額',PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;INSERT INTO `account_info` VALUES (2, '張三的賬戶', '1', '', 10000);CREATE DATABASE `bank2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';CREATE TABLE `account_info` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '戶主姓名',`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '銀行卡號',`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帳戶密碼',`account_balance` double NULL DEFAULT NULL COMMENT '帳戶余額',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;INSERT INTO `account_info` VALUES (3, '李四的賬戶', '2', NULL, 0);備注:分別在bank1、bank2庫中創(chuàng)建undo_log表,此表為seata框架使用
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--bank-2 不需要--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> </dependencies>server: port: 8081 #port: 8082spring: application: name: bank-1 #name: bank-2 datasource: url: jdbc:mysql://localhost:3306/bank1?characterEncoding=utf8&useSSL=false #url: jdbc:mysql://localhost:3306/bank2?characterEncoding=utf8&useSSL=false driver-class-name: com.mysql.jdbc.Driver username: root password: rootseata: tx-service-group: order_tx_group #自定義事務(wù)組名稱需要與seata-server中的對應(yīng) service: vgroup-mapping: order_tx_group: default # TC 集群(必須與seata-server保持一致)# bank-1@Update("update account_info set account_balance = account_balance + #{amount} where account_no = #{accountNo}")int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);# bank-2@Update("UPDATE account_info SET account_balance = account_balance + #{amount} WHERE account_no = #{accountNo}")int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);bank-1:
@GlobalTransactional @Override public void updateAccountBalance(String accountNo, Double amount) { log.info("******** Bank1 Service Begin ... xid: {}" , RootContext.getXID()); //張三扣減金額 baseMapper.updateAccountBalance(accountNo,amount * -1); //向李四轉(zhuǎn)賬 CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://localhost:8082/bank2/transfer?amount="+amount); httpget.addHeader(RootContext.KEY_XID,RootContext.getXID()); try{ CloseableHttpResponse response = httpclient.execute(httpget); HttpEntity entity = response.getEntity(); String result = EntityUtils.toString(entity); log.info("bank2 服務(wù)返回結(jié)果:"+result); }catch (Exception e){ throw new RuntimeException("bank2 服務(wù)異常"); } //人為制造錯(cuò)誤 if(amount > 100){ throw new RuntimeException("bank1 make exception amount > 100"); } }當(dāng)業(yè)務(wù)方法開啟全局異常處理器后,TM注冊到TC獲取到一個(gè)XID,此時(shí)在業(yè)務(wù)中,服務(wù)遠(yuǎn)程訪問時(shí),此XID會(huì)被下面分支業(yè)務(wù)方法RM接收到,當(dāng)各個(gè)方法處理完成后RM會(huì)向TC直接交互把結(jié)果通過XID通知給TC,最后業(yè)務(wù)方法結(jié)束后,TM會(huì)通知TC業(yè)務(wù)已經(jīng)完成,TC會(huì)根據(jù)RM通知的結(jié)果來通知各個(gè)RM提交或者回滾。但是在分布式事務(wù)中,入口TM傳出時(shí)不會(huì)將XID放入請求頭中向其他服務(wù)傳遞,這樣就導(dǎo)致全局異常捕獲失效,因此需要手動(dòng)將XID設(shè)置到請求頭中,攜帶給各分支業(yè)務(wù)來避免事務(wù)失效問題。
bank-2:
@Transactional @Override public void updateAccountBalance(String accountNo, Double amount) { log.info("******** Bank2 Service Begin ... xid: {}" , RootContext.getXID()); //李四增加金額 baseMapper.updateAccountBalance(accountNo,amount); //制造異常 if(amount < 100){ throw new RuntimeException("bank1 make exception amount < 100"); } }file.conf:
圖片
registry.conf:
圖片
正常流程:
圖片
回滾流程:
圖片
本文鏈接:http://m.www897cc.com/showinfo-26-69008-0.htmlSeata如何實(shí)現(xiàn)兩階段提交(2PC)分布式事務(wù)
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: React 19 即將推出的四個(gè)全新 Hooks,很實(shí)用!
下一篇: 聊聊什么是JSX以及在React中的使用