針對不一樣的分佈式場景業界常見的解決方案有2PC、TCC、可靠消息最終一致性、最大努力通知這幾種。java
2PC即兩階段提交協議,是將整個事務流程分爲兩個階段,準備階段(Prepare phase)、提交階段(commit phase),2是指兩階段,P是指準備階段,C是提交階段。
舉例 :張三和李四很久不見,老友約起聚餐,飯店老闆要求先買單,才能出票。這時張三和李四分別抱怨近況不如意,囊腫羞澀,都不肯意請客,這時只能AA。只有張三和李四都付款,老闆才能出票安排就餐。但因爲張三和李四都是鐵公雞,造成兩尷尬的一幕 :
準備階段 :老闆要求張三付款,張三付款。老闆要求李四付款,李四付款。
提交階段 :老闆出票,兩人拿票紛紛落座就餐。
例子中造成兩一個事務,若張三或李四其中一個拒絕付款,或錢不夠,店老闆都不會給出票,而且會把已收款退回。
整個事務過程由事務管理器和參與者組成,店老闆就是事務管理器,張3、李四就是事務參與者,事務管理器負責決策整個分佈式事務的提交和回滾,事務參與者負責本身本地事務的提交和回滾。
在計算機中部分關係數據庫如Oracle、MySQL支持兩階段提交協議,以下圖 :
1. 準備階段(Prepare phase):事務管理器給每一個參與者發送Prepare消息,每一個數據庫參與者在本地執行事務,並寫本地的Undo/Redo日誌,此時事務沒有提交。
(Undo日誌是記錄修改前的數據,用於數據庫回滾,Redo日誌是記錄修改後的數據,用於提交事務後寫入數據文件)
2. 提交階段(commit phase):若是事務管理器收到兩參與者的執行失敗或者超時消息時,直接給每一個參與者發送回滾(Rollback)消息;不然,發送提交(Commit)消息;參與者根據事務管理器的指令執行提交或者回滾操做,並釋放事務處理過程當中使用的鎖資源。注意 :必須在最後階段釋放鎖資源。
下圖展現兩2PC的兩個階段,分紅功和失敗兩個狀況說明 :
成功狀況 :
失敗狀況 :
linux
2PC的傳統方案是在數據庫層面實現的,如Oracle、MySQL都支持2PC協議,爲了統一標準減小行業內沒必要要的對接成本,須要制定標準化的處理模型及接口標準,國際開放標準組織Open Group定義分佈式事務處理模型DTP(Distributed Transaction Processing Reference Model)。
爲了讓你們更明確XA方案的內容,下面新用戶註冊送積分爲例來講明 :
執行流程以下 :
一、應用程序(AP)持有用戶庫和積分庫兩個數據源。
二、應用程序(AP)經過TM通知用戶庫RM新增用戶,同時通知積分庫RM爲該用戶新增積分,RM此時並未提交事務,此時用戶和積分資源鎖定。
三、TM收到執行回覆,只要有一方失敗則分別向其餘RM發起回滾事務,回滾完畢,資源鎖釋放。
四、TM收到執行回覆,所有成功,此時向全部RM發起提交事務,提交完畢,資源鎖釋放。
DTP模型定義以下角色 :git
Seata是阿里中間件團隊發起的開源項目Fescar,後改名Seata,它是一個是開源的分佈式事務框架。傳統2PC的問題在Seata中獲得瞭解決,它經過對本地關係數據庫的分支事務的協調來驅動完成全局事務,是工做在應用層的中間件。主要優勢是性能較好,且不長時間佔用鏈接資源,它以高效而且對業務0入侵的方式解決微服務場景下面臨的分佈式事務問題,它目前提供AT模式(即2PC)及TCC模式的分佈式事務解決方案。
Seata的設計思想以下 :
Seata的設計目標其一是對業務無入侵,所以從業務無入侵的2PC方案着手,在傳統2PC的基礎上演進,並解決2PC方案面臨的問題。
Seata把一個分佈式事務理解成一個包含來若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一致,要麼一塊兒成功提交,要麼一塊兒失敗回滾。此外,一般分支事務自己就是一個關係數據庫的本地事務,下圖是全局事務與分支事務的關係圖 :
與傳統2PC的模型相似,Seata定義了三個組件來協議分佈式事務的處理過程 :
github
Seata實現2PC與傳統2PC的差異 :
架構層次方面,傳統2PC方案的RM其實是在數據庫層,RM本質上就是數據庫自身,經過XA協議實現,而Seata的RM是以jar包的形式做爲中間件層部署在應用程序的這一側的。
兩階段提交方面,傳統2PC不管第二階段的決議是commit仍是rollbcak,事務性資源的鎖都要保持到Phase2完成才釋放。而Seata的作法是在Phase1就將本地事務提交,這樣就能夠省去Phase2持鎖的時間,總體提升效率。spring
本實例經過Seata中間件實現分佈式事務,模擬兩個帳戶的轉帳交易過程。兩個帳戶在兩個不一樣的銀行(張三在bank一、李四在bank2),bank1和bank2是兩個微服務。交易過程當中,張三給李四轉帳制定金額。
上述交易步驟,要麼一塊兒成功,要麼一塊兒失敗,必須是一個總體性的事務。
sql
本實例程序組成 部分以下 :
數據庫 :MySQL-5.7.25
包括bank1和bank2兩個數據庫。
JDK:1.8
微服務框架 :spring-boot-2.1.三、spring-cloud-Greenwich.RELEASE
seata客戶端(RM、TM):spring-cloud-alibaba-seata-2.1.0RELEASE
seata服務端(TC):seata-server-0.7.1
微服務及數據庫的關係 :
dtx/dtx-seata-demo/seata-demo-bank1 銀行1,操做張三帳戶,連接數據庫bank1
dtx/dtx-seata-demo/seata-demo-bank2 銀行2,操做李四帳戶,連接數據庫bank2
服務註冊中興 :dtx/discover-server
本實例程序技術架構以下 :
交互流程以下 :
一、請求bank1進行轉帳,傳入轉帳金額。
二、bank1減小轉帳金額,調用bank2,傳入轉帳金額。數據庫
bank1庫,包含張三帳戶編程
CREATE DATABASE /*!32312 IF NOT EXISTS*/`bank1` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `bank1`; /*Table structure for table `account_info` */ DROP TABLE IF EXISTS `account_info`; CREATE TABLE `account_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `account_name` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '戶主姓名', `account_no` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '銀行卡號', `account_password` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '賬戶密碼', `account_balance` double DEFAULT NULL COMMENT '賬戶餘額', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC; /*Data for the table `account_info` */ insert into `account_info`(`id`,`account_name`,`account_no`,`account_password`,`account_balance`) values (2,'張三','1',NULL,1000); /*Table structure for table `de_duplication` */ DROP TABLE IF EXISTS `de_duplication`; CREATE TABLE `de_duplication` ( `tx_no` varchar(64) COLLATE utf8_bin NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC; /*Data for the table `de_duplication` */ /*Table structure for table `local_cancel_log` */ DROP TABLE IF EXISTS `local_cancel_log`; CREATE TABLE `local_cancel_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `local_cancel_log` */ /*Table structure for table `local_confirm_log` */ DROP TABLE IF EXISTS `local_confirm_log`; CREATE TABLE `local_confirm_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `local_confirm_log` */ /*Table structure for table `local_trade_log` */ DROP TABLE IF EXISTS `local_trade_log`; CREATE TABLE `local_trade_log` ( `tx_no` bigint(20) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC; DROP TABLE IF EXISTS `local_try_log`; CREATE TABLE `local_try_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `local_try_log` */ /*Table structure for table `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) 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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=167 DEFAULT CHARSET=utf8; /*Data for the table `undo_log` */ insert into `undo_log`(`id`,`branch_id`,`xid`,`context`,`rollback_info`,`log_status`,`log_created`,`log_modified`,`ext`) values (166,2019228885,'192.168.1.101:8888:2019228047','serializer=jackson','{}',1,'2019-08-11 15:16:43','2019-08-11 15:16:43',NULL);
bank2庫,包含李四帳戶服務器
CREATE DATABASE /*!32312 IF NOT EXISTS*/`bank2` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `bank2`; /*Table structure for table `account_info` */ DROP TABLE IF EXISTS `account_info`; CREATE TABLE `account_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `account_name` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '戶主姓名', `account_no` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '銀行卡號', `account_password` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '賬戶密碼', `account_balance` double DEFAULT NULL COMMENT '賬戶餘額', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC; /*Data for the table `account_info` */ insert into `account_info`(`id`,`account_name`,`account_no`,`account_password`,`account_balance`) values (3,'李四的帳戶','2',NULL,0); /*Table structure for table `de_duplication` */ DROP TABLE IF EXISTS `de_duplication`; CREATE TABLE `de_duplication` ( `tx_no` varchar(64) COLLATE utf8_bin NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC; /*Data for the table `de_duplication` */ /*Table structure for table `local_cancel_log` */ DROP TABLE IF EXISTS `local_cancel_log`; CREATE TABLE `local_cancel_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `local_cancel_log` */ /*Table structure for table `local_confirm_log` */ DROP TABLE IF EXISTS `local_confirm_log`; CREATE TABLE `local_confirm_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `local_confirm_log` */ /*Table structure for table `local_trade_log` */ DROP TABLE IF EXISTS `local_trade_log`; CREATE TABLE `local_trade_log` ( `tx_no` bigint(20) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC; DROP TABLE IF EXISTS `local_try_log`; CREATE TABLE `local_try_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `local_try_log` */ /*Table structure for table `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) 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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(1)下載seata服務器
下載地址 :seata服務器
(2)解壓並啓動
winodws :【seata服務端解壓路徑】/bin/seata-server.bat -p 8888 -m file
mac/linux : 【seata服務端解壓路徑】nohup sh seata-server.sh -p 8888 -h 127.0.0.1 -m file &> seata.log &
注 :其中8888爲服務端口號;file爲啓動模式,這裏指seata服務將採用文件的方式存儲信息。
如上圖出現「Server started。。。「的字樣則表示啓動成功。架構
discover-server是服務註冊中心,測試工程將本身註冊至discover-server。
dtx-seata-demo是seata的測試工程,根據業務需求須要建立兩個dex-seata-demo工程。
(1)父工程maven依賴說明
在dtx父工程中指定了SpringBoot和SpringCloud版本
在dtx-seata-demo父工程中指定了spring-cloud-alibaba-dependencies的版本。
(3)配置seata
在src/main/resource中,新增registry.conf、file.conf文件,內容可拷貝seata-server-0.7.1中的配置文件子。 在registry.conf中registry.type使用file:
在file.conf中更改service.vgroup_mapping.[springcloud服務名]-fescar-service-group = 「default」,並修改 service.default.grouplist =[seata服務端地址]
關於vgroup_mapping的配置:
vgroup_mapping.事務分組服務名=Seata Server集羣名稱(默認名稱爲default) default.grouplist = Seata Server集羣地址
在 org.springframework.cloud:spring-cloud-starter-alibaba-seata 的 org.springframework.cloud.alibaba.seata.GlobalTransactionAutoConfiguration 類中,默認會使用 ${spring.application.name}-fescar-service-group 做爲事務分組服務名註冊到 Seata Server上,若是和 file.conf 中的配置不一致,會提示 no available server to connect 錯誤
也能夠經過配置 spring.cloud.alibaba.seata.tx-service-group 修改後綴,可是必須和 file.conf 中的配置保持 一致。
(4)建立代理數據源
新增DatabaseConfiguration.java,Seata的RM經過DataSourceProxy才能在業務代碼的事務提交時,經過這個切 入點,與TC進行通訊交互、記錄undo_log等。
@Configuration public class DatabaseConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource.ds0") public DruidDataSource ds0() { DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } @Primary @Bean public DataSource dataSource(DruidDataSource ds0) { DataSourceProxy pds0 = new DataSourceProxy(ds0); return pds0; } }
一、正常提交流程
二、回滾流程
回滾流程省略前的RM註冊過程。
要點說明 :
一、每一個RM使用DataSourceProxy鏈接數據庫,其目的是使用ConnectionProxy,使用數據源和數據鏈接代理的目的就是第一階段將undo_log和業務數據放在一個本地事務提交,這樣就保存了只要有業務操做就必定有undo_log.
二、在第一階段undo_log中存放了數據修改前和修改後的值,爲事務回滾做好準備,因此第一階段完成就已經將分支事務提交,也就釋放了鎖資源。
三、TM開啓全局事務開始,將XID全局事務id放在事務上下午中,經過feign調用也將XID傳入下游分支事務,每一個分支事務將本身的Branch ID分支事務ID與XID關聯。
四、第二階段全局事務提交,TC會通知各個分支參與者提交分支事務,在第一階段就已經提交了分支事務,這裏各個參與者只須要刪除undo_log便可,而且能夠異步執行,第二階段很快能夠完成。
五、第二階段全局事務回滾,TC會通知各個分支參與者回滾分支事務,經過XID和Branch ID找到相應的回滾日誌,經過回滾日誌生成反向的SQL並執行,以完成分支事務回滾到以前的狀態,若是回滾失敗則會重試回滾操做。
dtx-seata-demo-bank1實現以下功能:
一、張三帳戶減小金額,開啓全局事務。
二、遠程調用bank2向李四轉帳。
(1)DAO
@Mapper @Component public interface AccountInfoDao { //更新帳戶金額 @Update("update account_info set account_balance = account_balance + #{amount} where account_no = #{accountNo}") int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount); }
(2) FeignClient
遠程調用bank2的客戶端
@FeignClient(value = "seata‐demo‐bank2",fallback = Bank2ClientFallback.class) public interface Bank2Client { @GetMapping("/bank2/transfer") String transfer(@RequestParam("amount") Double amount); } @Component public class Bank2ClientFallback implements Bank2Client{ @Override public String transfer(Double amount) { return "fallback"; } }
(3)Service
@Service public class AccountInfoServiceImpl implements AccountInfoService { private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class); @Autowired AccountInfoDao accountInfoDao; @Autowired Bank2Client bank2Client; //張三轉帳 @Override @GlobalTransactional @Transactional public void updateAccountBalance(String accountNo, Double amount) { logger.info("******** Bank1 Service Begin ... xid: {}" , RootContext.getXID()); //張三扣減金額 accountInfoDao.updateAccountBalance(accountNo,amount*‐1); //向李四轉帳 String remoteRst = bank2Client.transfer(amount); //遠程調用失敗 if(remoteRst.equals("fallback")){ throw new RuntimeException("bank1 下游服務異常"); } //人爲製造錯誤 if(amount==3){ throw new RuntimeException("bank1 make exception 3"); } } }
將@GlobalTransactional註解標註在全局事務發起的Service實現方法上,開啓全局事務 :
GlobalTransactionalInterceptor會攔截@GlobalTransactional註解的方法,生成全局事務ID(XID),XID會在整個分佈式事務中傳遞。
在遠程調用時,spring-cloud-alibaba-seata會攔截Feign調用將XID傳遞到下游服務。
(6)Controller
@RestController public class Bank1Controller { @Autowired AccountInfoService accountInfoService; //轉帳 @GetMapping("/transfer") public String transfer(Double amount){ accountInfoService.updateAccountBalance("1",amount); return "bank1"+amount; } }
dtx-seata-demo-bank2實現以下功能:
一、李四帳戶增長金額。
dtx-seata-demo-bank2在本帳戶事務中做爲分支事務不使用@GlobalTransactional。
(1)DAO
@Mapper @Component public interface AccountInfoDao { //向李四轉帳 @Update("UPDATE account_info SET account_balance = account_balance + #{amount} WHERE account_no = #{accountNo}") int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount); }
(2)Service
@Service public class AccountInfoServiceImpl implements AccountInfoService { private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class); @Autowired AccountInfoDao accountInfoDao; @Override @Transactional public void updateAccountBalance(String accountNo, Double amount) { logger.info("******** Bank2 Service Begin ... xid: {}" , RootContext.getXID()); //李四增長金額 accountInfoDao.updateAccountBalance(accountNo,amount); //製造異常 if(amount==2){ throw new RuntimeException("bank1 make exception 2"); } } }
(3)Controller
@RestController public class Bank2Controller { @Autowired AccountInfoService accountInfoService; @GetMapping("/transfer") public String transfer(Double amount){ accountInfoService.updateAccountBalance("2",amount); return "bank2"+amount; } }
傳統2PC(基於數據庫XA協議)和Seata實現2PC的兩種2PC方案,因爲Seata的零入侵而且解決了傳統2PC長期鎖資源的問題,因此推薦採用Seata實現2PC。Seata實現2PC要點 :一、全局事務開始使用GlobalTransactional標識。二、每一個本地事務方案仍然使用@Transactional標識。三、每一個數據都須要建立undo_log表,此表是Seata保證本地事務一致性的關鍵。