分佈式事務一2PC

分佈式事務解決方案之2PC(兩階段提交)

前面已經學習了分佈式事務的基礎理論,以理論爲基礎,針對不一樣的分佈式場景業界常見的解決方案有2PC、TCC、可靠消息最終一致性、最大努力通知這幾種。java

3.1.什麼是2PC

  2PC即兩階段提交協議,是將整個事務流程分爲兩個階段,準備階段(Prepare phase)、提交階段(commit phase),2是指兩個階段,P是指準備階段,C是指提交階段。git

舉例:張三和李四很久不見,老友約起聚餐,飯店老闆要求先買單,才能出票。這時張三和李四分別抱怨近況不如意,囊中羞澀,都不肯意請客,這時只能AA。只有張三和李四都付款,老闆才能出票安排就餐。但因爲張三和李四都是鐵公雞,造成了尷尬的一幕:github

  準備階段:老闆要求張三付款,張三付款。老闆要求李四付款,李四付款。spring

  提交階段:老闆出票,兩人拿票紛紛落座就餐。sql

  例子中造成了一個事務,若張三或李四其中一人拒絕付款,或錢不夠,店老闆都不會給出票,而且會把已收款退回。數據庫

整個事務過程由事務管理器和參與者組成,店老闆就是事務管理器,張3、李四就是事務參與者,事務管理器負責決策整個分佈式事務的提交和回滾,事務參與者負責本身本地事務的提交和回滾。編程

在計算機中部分關係數據庫如Oracle、MySQL支持兩階段提交協議,以下圖:服務器

  1. 準備階段(Prepare phase):事務管理器給每一個參與者發送Prepare消息,每一個數據庫參與者在本地執行事務,並寫本地的Undo/Redo日誌,此時事務沒有提交。架構

(Undo日誌是記錄修改前的數據,用於數據庫回滾,Redo日誌是記錄修改後的數據,用於提交事務後寫入數據文件)app

  2. 提交階段(commit phase):若是事務管理器收到了參與者的執行失敗或者超時消息時,直接給每一個參與者發送回滾(Rollback)消息;不然,發送提交(Commit)消息;參與者根據事務管理器的指令執行提交或者回滾操做,並釋放事務處理過程當中使用的鎖資源。注意:必須在最後階段釋放鎖資源。

下圖展現了2PC的兩個階段,分紅功和失敗兩個狀況說明:成功狀況:

 

  

失敗狀況:

 

3.2.解決方案

3.2.1 XA方案

  2PC的傳統方案是在數據庫層面實現的,如Oracle、MySQL都支持2PC協議,爲了統一標準減小行業內沒必要要的對接成本,須要制定標準化的處理模型及接口標準,國際開放標準組織Open Group定義了分佈式事務處理模型DTP(Distributed Transaction Processing Reference Model)。

爲了讓你們更明確XA方案的內容程,下面新用戶註冊送積分爲例來講明:

 

執行流程以下:

  一、應用程序(AP)持有用戶庫和積分庫兩個數據源。

  二、應用程序(AP)經過TM通知用戶庫RM新增用戶,同時通知積分庫RM爲該用戶新增積分,RM此時並未提交事務,此時用戶和積分資源鎖定。

  三、TM收到執行回覆,只要有一方失敗則分別向其餘RM發起回滾事務,回滾完畢,資源鎖釋放。

  四、TM收到執行回覆,所有成功,此時向全部RM發起提交事務,提交完畢,資源鎖釋放。

DTP模型定義以下角色:

  AP(Application Program):即應用程序,能夠理解爲使用DTP分佈式事務的程序。

  RM(Resource Manager):即資源管理器,能夠理解爲事務的參與者,通常狀況下是指一個數據庫實例,經過資源管理器對該數據庫進行控制,資源管理器控制着分支事務。

   TM(Transaction Manager):事務管理器,負責協調和管理事務,事務管理器控制着全局事務,管理事務生命週期,並協調各個RM。全局事務是指分佈式事務處理環境中,須要操做多個數據庫共同完成一個工做,這個工做便是一個全局事務。

   DTP模型定義TM和RM之間通信的接口規範叫XA,簡單理解爲數據庫提供的2PC接口協議,基於數據庫的XA協議來實現2PC又稱爲XA方案。

 以上三個角色之間的交互方式以下:

  1)TM向AP提供 應用程序編程接口,AP經過TM提交及回滾事務。

  2)TM交易中間件經過XA接口來通知RM數據庫事務的開始、結束以及提交、回滾等。

總結:

  整個2PC的事務流程涉及到三個角色AP、RM、TM。AP指的是使用2PC分佈式事務的應用程序;RM指的是資源管理器,它控制着分支事務;TM指的是事務管理器,它控制着整個全局事務。

  1)在準備階段RM執行實際的業務操做,但不提交事務,資源鎖定;

  2)在提交階段TM會接受RM在準備階段的執行回覆,只要有任一個RM執行失敗,TM會通知全部RM執行回滾操做,不然,TM將會通知全部RM提交該事務。提交階段結束資源鎖釋放。

XA方案的問題: 

  一、須要本地數據庫支持XA協議。

  二、資源鎖須要等到兩個階段結束才釋放,性能較差。

3.2.2 Seata方案

  Seata是由阿里中間件團隊發起的開源項目 Fescar,後改名爲Seata,它是一個是開源的分佈式事務框架。

  傳統2PC的問題在Seata中獲得瞭解決,它經過對本地關係數據庫的分支事務的協調來驅動完成全局事務,是工做在應用層的中間件。主要優勢是性能較好,且不長時間佔用鏈接資源,它以高效而且對業務0侵入的方式解決微服務場景下面臨的分佈式事務問題,它目前提供AT模式(即2PC)及TCC模式的分佈式事務解決方案。

Seata的設計思想以下:

  Seata的設計目標其一是對業務無侵入,所以從業務無侵入的2PC方案着手,在傳統2PC的基礎上演進,並解決2PC方案面臨的問題

  Seata把一個分佈式事務理解成一個包含了若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一致,要麼一塊兒成功提交,要麼一塊兒失敗回滾。此外,一般分支事務自己就是一個關係數據庫的本地事務,下圖是全局事務與分支事務的關係圖:

 

與 傳統2PC 的模型相似,Seata定義了3個組件來協議分佈式事務的處理過程:

 

    Transaction Coordinator (TC): 事務協調器,它是獨立的中間件,須要獨立部署運行,它維護全局事務的運行狀態,接收TM指令發起全局事務的提交與回滾,負責與RM通訊協調各各分支事務的提交或回滾。

    Transaction Manager (TM): 事務管理器,TM須要嵌入應用程序中工做,它負責開啓一個全局事務,並最終向TC發起全局提交或全局回滾的指令。

   Resource Manager (RM): 控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器TC的指令,驅動分支(本地)事務的提交和回滾

還拿新用戶註冊送積分舉例Seata的分佈式事務過程:

 

具體的執行流程以下:

  1. 用戶服務的 TM 向 TC 申請開啓一個全局事務,全局事務建立成功並生成一個全局惟一的XID。

  2. 用戶服務的 RM 向 TC 註冊 分支事務,該分支事務在用戶服務執行新增用戶邏輯,並將其歸入 XID 對應全局事務的管轄。

  3. 用戶服務執行分支事務,向用戶表插入一條記錄。

  4. 邏輯執行到遠程調用積分服務時(XID 在微服務調用鏈路的上下文中傳播)。積分服務的RM 向 TC 註冊分支事務,該分支事務執行增長積分的邏輯,並將其歸入 XID 對應全局事務的管轄。

  5. 積分服務執行分支事務,向積分記錄表插入一條記錄,執行完畢後,返回用戶服務。

  6. 用戶服務分支事務執行完畢。

  7. TM 向 TC 發起針對 XID 的全局提交或回滾決議。

  8. TC 調度 XID 下管轄的所有分支事務完成提交或回滾請求。

Seata實現2PC與傳統2PC的差異:

  架構層次方面,傳統2PC方案的 RM 其實是在數據庫層,RM 本質上就是數據庫自身,經過 XA 協議實現,而Seata的 RM 是以jar包的形式做爲中間件層部署在應用程序這一側的。

  兩階段提交方面,傳統2PC不管第二階段的決議是commit仍是rollback,事務性資源的鎖都要保持到Phase2完成才釋放。而Seata的作法是在Phase1 就將本地事務提交,這樣就能夠省去Phase2持鎖的時間,總體提升效率。

3.3.seata實現2PC事務

3.3.1.業務說明

本示例經過Seata中間件實現分佈式事務,模擬三個帳戶的轉帳交易過程。

兩個帳戶在三個不一樣的銀行(張三在bank一、李四在bank2),bank1和bank2是兩個個微服務。交易過程是,張三給李四轉帳指定金額。

 

 

 

 

上述交易步驟,要麼一塊兒成功,要麼一塊兒失敗,必須是一個總體性的事務。

3.3.2.程序組成部分

本示例程序組成部分以下:

  數據庫:MySQL-5.7.25,包括bank1和bank2兩個數據庫。

  JDK:64位 jdk1.8.0_201

  微服務框架:spring-boot-2.1.三、spring-cloud-Greenwich.RELEASE

  seata客戶端(RM、TM):spring-cloud-alibaba-seata-2.1.0.RELEASE

  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,傳入轉帳金額。

3.3.3.建立數據庫

  導入數據庫腳本:資料\sql\bank1.sql、資料\sql\bank2.sql

包括以下數據庫:

bank1庫,包含張三帳戶

CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
DROP TABLE IF EXISTS `account_info`;
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);

bank2庫,包含李四帳戶

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);

分別在bank一、bank2庫中建立undo_log表,此表爲seata框架使用:

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=1 DEFAULT CHARSET=utf8;

3.3.4.啓動TC(事務協調器)

(1)下載seata服務器,下載地址:https://github.com/seata/seata/releases/download/v0.7.1/seata-server-0.7.1.zip也能夠直接解壓:資料\seata-server-0.7.1.zip

(2)解壓並啓動,[seata服務端解壓路徑]/bin/seata-server.bat -p 8888 -m file 注:其中8888爲服務端口號;file爲啓動模式,這裏指seata服務將採用文件的方式存儲信息。

如上圖出現「Server started...」的字樣則表示啓動成功。

3.3.5 discover-server

  discover-server是服務註冊中心,測試工程將本身註冊至discover-server。

導入:資料\基礎代碼\dtx 父工程,此工程自帶了discover-server,discover-server基於Eureka實現。

3.3.6 導入案例工程dtx-seata-demo

  dtx-seata-demo是seata的測試工程,根據業務需求須要建立兩個dtx-seata-demo工程。

(1)導入dtx-seata-demo 導入:資料\基礎代碼\dtx-seata-demo到父工程dtx下。

兩個測試工程以下:

  dtx/dtx-seata-demo/dtx-seata-demo-bank1 ,操做張三帳戶,鏈接數據庫bank1

  dtx/dtx-seata-demo/dtx-seata-demo-bank2 ,操做李四帳戶,鏈接數據庫bank2

(2)父工程maven依賴說明

在dtx父工程中指定了SpringBoot和SpringCloud版本

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring‐boot‐dependencies</artifactId>
    <version>2.1.3.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring‐cloud‐dependencies</artifactId>
    <version>Greenwich.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

在dtx-seata-demo父工程中指定了spring-cloud-alibaba-dependencies的版本。

 

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring‐cloud‐alibaba‐dependencies</artifactId>
    <version>2.1.0.RELEASE</version>
    <type>pom</type>    
    <scope>import</scope>
</dependency>

(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;
    }
}

 

3.3.7 Seata執行流程

一、正常提交流程

 

 

二、回滾流程

回滾流程省略前的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 並執行,以完成分支事務回滾到以前的狀態,若是回滾失敗則會重試回滾操做。

3.3.8 dtx-seata-demo-bank1

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;
    }
}

3.3.9 dtx-seata-demo-bank2

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;
    }
}

3.3.10 測試場景

 張三向李四轉帳成功。

 李四事務失敗,張三事務回滾成功。

 張三事務失敗,李四事務回滾成功。

 分支事務超時測試。

3.4.小結

  本節講解了傳統2PC(基於數據庫XA協議)和Seata實現2PC的兩種2PC方案,因爲Seata的0侵入性而且解決了傳統2PC長期鎖資源的問題,因此推薦採用Seata實現2PC。

  Seata實現2PC要點:

  一、全局事務開始使用 @GlobalTransactional標識 。

  二、每一個本地事務方案仍然使用@Transactional標識。

  三、每一個數據都須要建立undo_log表,此表是seata保證本地事務一致性的關鍵。

相關文章
相關標籤/搜索