天天看點

分布式事務一2PC

分布式事務解決方案之2PC(兩階段送出)

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

3.1.什麼是2PC

  2PC即兩階段送出協定,是将整個事務流程分為兩個階段,準備階段(Prepare phase)、送出階段(commit phase),2是指兩個階段,P是指準備階段,C是指送出階段。

舉例:張三和李四好久不見,老友約起聚餐,飯店老闆要求先買單,才能出票。這時張三和李四分别抱怨近況不如意,囊中羞澀,都不願意請客,這時隻能AA。隻有張三和李四都付款,老闆才能出票安排就餐。但由于張三和李四都是鐵公雞,形成了尴尬的一幕:

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

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

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

整個事務過程由事務管理器和參與者組成,店老闆就是事務管理器,張三、李四就是事務參與者,事務管理器負責決策整個分布式事務的送出和復原,事務參與者負責自己本地事務的送出和復原。

在計算機中部分關系資料庫如Oracle、MySQL支援兩階段送出協定,如下圖:

  1. 準備階段(Prepare phase):事務管理器給每個參與者發送Prepare消息,每個資料庫參與者在本地執行事務,并寫本地的Undo/Redo日志,此時事務沒有送出。

(Undo日志是記錄修改前的資料,用于資料庫復原,Redo日志是記錄修改後的資料,用于送出事務後寫入資料檔案)

  2. 送出階段(commit phase):如果事務管理器收到了參與者的執行失敗或者逾時消息時,直接給每個參與者發送復原(Rollback)消息;否則,發送送出(Commit)消息;參與者根據事務管理器的指令執行送出或者復原操作,并釋放事務處理過程中使用的鎖資源。注意:必須在最後階段釋放鎖資源。

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

分布式事務一2PC

失敗情況:

分布式事務一2PC

3.2.解決方案

3.2.1 XA方案

  2PC的傳統方案是在資料庫層面實作的,如Oracle、MySQL都支援2PC協定,為了統一标準減少行業内不必要的對接成本,需要制定标準化的處理模型及接口标準,國際開放标準組織Open Group定義了分布式事務處理模型DTP(Distributed Transaction Processing Reference Model)。

為了讓大家更明确XA方案的内容程,下面新使用者注冊送積分為例來說明:

分布式事務一2PC

執行流程如下:

  1、應用程式(AP)持有使用者庫和積分庫兩個資料源。

  2、應用程式(AP)通過TM通知使用者庫RM新增使用者,同時通知積分庫RM為該使用者新增積分,RM此時并未送出事務,此時使用者和積分資源鎖定。

  3、TM收到執行回複,隻要有一方失敗則分别向其他RM發起復原事務,復原完畢,資源鎖釋放。

  4、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方案的問題: 

  1、需要本地資料庫支援XA協定。

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

3.2.2 Seata方案

  Seata是由阿裡中間件團隊發起的開源項目 Fescar,後更名為Seata,它是一個是開源的分布式事務架構。

  傳統2PC的問題在Seata中得到了解決,它通過對本地關系資料庫的分支事務的協調來驅動完成全局事務,是工作在應用層的中間件。主要優點是性能較好,且不長時間占用連接配接資源,它以高效并且對業務0侵入的方式解決微服務場景下面臨的分布式事務問題,它目前提供AT模式(即2PC)及TCC模式的分布式事務解決方案。

Seata的設計思想如下:

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

  Seata把一個分布式事務了解成一個包含了若幹分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一緻,要麼一起成功送出,要麼一起失敗復原。此外,通常分支事務本身就是一個關系資料庫的本地事務,下圖是全局事務與分支事務的關系圖:

分布式事務一2PC

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

分布式事務一2PC

    Transaction Coordinator (TC): 事務協調器,它是獨立的中間件,需要獨立部署運作,它維護全局事務的運作狀态,接收TM指令發起全局事務的送出與復原,負責與RM通信協調各各分支事務的送出或復原。

    Transaction Manager (TM): 事務管理器,TM需要嵌入應用程式中工作,它負責開啟一個全局事務,并最終向TC發起全局送出或全局復原的指令。

   Resource Manager (RM): 控制分支事務,負責分支注冊、狀态彙報,并接收事務協調器TC的指令,驅動分支(本地)事務的送出和復原

還拿新使用者注冊送積分舉例Seata的分布式事務過程:

分布式事務一2PC

具體的執行流程如下:

  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中間件實作分布式事務,模拟三個賬戶的轉賬交易過程。

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

分布式事務一2PC

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

3.3.2.程式組成部分

本示例程式組成部分如下:

  資料庫:MySQL-5.7.25,包括bank1和bank2兩個資料庫。

  JDK:64位 jdk1.8.0_201

  微服務架構:spring-boot-2.1.3、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 

本示例程式技術架構如下:

分布式事務一2PC

互動流程如下:

  1、請求bank1進行轉賬,傳入轉賬金額。

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

分别在bank1、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服務将采用檔案的方式存儲資訊。

分布式事務一2PC

如上圖出現“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:

分布式事務一2PC

在file.conf中更改service.vgroup_mapping.[springcloud服務名]-fescar-service-group = "default",并修改 service.default.grouplist =[seata服務端位址]

分布式事務一2PC

關于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執行流程

1、正常送出流程

分布式事務一2PC

2、復原流程

復原流程省略前的RM注冊過程。

分布式事務一2PC

要點說明:

  1、每個RM使用DataSourceProxy連接配接資料庫,其目的是使用ConnectionProxy,使用資料源和資料連接配接代理的目的就是在第一階段将undo_log和業務資料放在一個本地事務送出,這樣就儲存了隻要有業務操作就一定有undo_log。

  2、在第一階段undo_log中存放了資料修改前和修改後的值,為事務復原作好準備,是以第一階段完成就已經将分支事務送出,也就釋放了鎖資源。

  3、TM開啟全局事務開始,将XID全局事務id放在事務上下文中,通過feign調用也将XID傳入下遊分支事務,每個分支事務将自己的Branch ID分支事務ID與XID關聯。

  4、第二階段全局事務送出,TC會通知各各分支參與者送出分支事務,在第一階段就已經送出了分支事務,這裡各各參與者隻需要删除undo_log即可,并且可以異步執行,第二階段很快可以完成。

  5、第二階段全局事務復原,TC會通知各各分支參與者復原分支事務,通過 XID 和 Branch ID 找到相應的復原日志,通過復原日志生成反向的 SQL 并執行,以完成分支事務復原到之前的狀态,如果復原失敗則會重試復原操作。

3.3.8 dtx-seata-demo-bank1

dtx-seata-demo-bank1實作如下功能:

  1、張三賬戶減少金額,開啟全局事務。

  2、遠端調用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實作如下功能:

1、李四賬戶增加金額。

dtx-seata-demo-bank2在本賬号事務中作為分支事務不使用@GlobalTransactional。

@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要點:

  1、全局事務開始使用 @GlobalTransactional辨別 。

  2、每個本地事務方案仍然使用@Transactional辨別。

  3、每個資料都需要建立undo_log表,此表是seata保證本地事務一緻性的關鍵。