第十章 Seata--分佈式事務

承接上篇 ,終於我們迎來了最後一章 第九章 Nacos Config–服務配置第十章 Seata–分佈式事務,感謝你能學習到這 !廢話不多說,擼碼

10.1 分佈式事務基礎

10.1.1 事務

事務指的就是一個操作單元,在這個操作單元中的所有操作最終要保持一致的行爲,要麼所有操作都成功,要麼所有的操作都被撤銷。簡單地說,事務提供一種“要麼什麼都不做,要麼做全套”機制。

10.1.2 本地事物

本地事物其實可以認爲是數據庫提供的事務機制。說到數據庫事務就不得不說,數據庫事務中的
四大特性:

  • A:原子性(Atomicity),一個事務中的所有操作,要麼全部完成,要麼全部不完成
  • C:一致性(Consistency),在一個事務執行之前和執行之後數據庫都必須處於一致性狀態
  • I:隔離性(Isolation),在併發環境中,當不同的事務同時操作相同的數據時,事務之間互不影響
  • D:持久性(Durability),指的是隻要事務成功結束,它對數據庫所做的更新就必須永久的保存下來
    取大寫首 字母 也就是 簡稱 ACID
    數據庫事務在實現時會將一次事務涉及的所有操作全部納入到一個不可分割的執行單元,該執行單元中的所有操作要麼都成功,要麼都失敗,只要其中任一操作執行失敗,都將導致整個事務的回滾

10.1.3 分佈式事務

分佈式事務指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不同的分佈式系統的不同節點之上。

簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分佈在不同的服務器上,且屬於不同的,應用,分佈式事務需要保證這些小操作要麼全部成功,要麼全部失敗。
本質上來說,分佈式事務就是爲了保證不同數據庫的數據一致性。

10.1.4 分佈式事務的場景

  • 單體系統訪問多個數據庫

一個服務需要調用多個數據庫實例完成數據的增刪改操作

在這裏插入圖片描述

  • 多個微服務訪問同一個數據庫

多個服務需要調用一個數據庫實例完成數據的增刪改操作
在這裏插入圖片描述

  • 多個微服務訪問多個數據庫

多個服務需要調用一個數據庫實例完成數據的增刪改操作

在這裏插入圖片描述

10.2 分佈式事務解決方案

10.2.1 全局事務

全局事務基於DTP模型實現。DTP是由X/Open組織提出的一種分佈式事務模型——X/Open
Distributed Transaction Processing Reference Model。它規定了要實現分佈式事務,需要三種角色:

  • AP: Application 應用系統 (微服務)
  • TM: Transaction Manager 事務管理器 (全局事務管理)
  • RM: Resource Manager 資源管理器 (數據庫)

整個事務分成兩個階段:

  • 階段一: 表決階段,所有參與者都將本事務執行預提交,並將能否成功的信息反饋發給協調者
  • 階段二: 執行階段,協調者根據所有參與者的反饋,通知所有參與者,步調一致地執行提交或回滾。
    在這裏插入圖片描述

優點

  • 提高了數據一致性的概率,實現成本較低

缺點

  • 單點問題: 事務協調者宕機
  • 同步阻塞: 延遲了提交時間,加長了資源阻塞時間
  • 數據不一致: 提交第二階段,依然存在commit結果未知的情況,有可能導致數據不一致

10.2.2 可靠消息服務

基於可靠消息服務的方案是通過消息中間件保證上、下游應用數據操作的一致性。假設有A和B兩個系統,分別可以處理任務A和任務B。此時存在一個業務流程,需要將任務A和任務B在同一個事務中處理。就可以使用消息中間件來實現這種分佈式事務。

在這裏插入圖片描述

第一步: 消息由系統A投遞到中間件

  1. 在系統A處理任務A前,首先向消息中間件發送一條消息
  2. 消息中間件收到後將該條消息持久化,但並不投遞。持久化成功後,向A回覆一個確認應答
  3. 系統A收到確認應答後,則可以開始處理任務A
  4. 任務A處理完成後,向消息中間件發送Commit或者Rollback請求。該請求發送完成後,對系統A而言,該事務的處理過程就結束了
  5. 如果消息中間件收到Commit,則向B系統投遞消息;如果收到Rollback,則直接丟棄消息。但是
    如果消息中間件收不到Commit和Rollback指令,那麼就要依靠"超時詢問機制"。

超時詢問機制
系統A除了實現正常的業務流程外,還需提供一個事務詢問的接口,供消息中間件調
用。當消息中間件收到發佈消息便開始計時,如果到了超時沒收到確認指令,就會主動調用
系統A提供的事務詢問接口詢問該系統目前的狀態。該接口會返回三種結果,中間件根據三
種結果做出不同反應:

  • 提交:將該消息投遞給系統B
  • 回滾:直接將條消息丟棄
  • 處理中:繼續等待

第二步: 消息由中間件投遞到系統B

消息中間件向下遊系統投遞完消息後便進入阻塞等待狀態,下游系統便立即進行任務的處理,任務處理完成後便向消息中間件返回應答。

  • 如果消息中間件收到確認應答後便認爲該事務處理完畢

  • 如果消息中間件在等待確認應答超時之後就會重新投遞,直到下游消費者返回消費成功響應爲止。
    一般消息中間件可以設置消息重試的次數和時間間隔,如果最終還是不能成功投遞,則需要手工干預。這裏之所以使用人工干預,而不是使用讓A系統回滾,主要是考慮到整個系統設計的複雜度問題。

    基於可靠消息服務的分佈式事務,前半部分使用異步,注重性能;後半部分使用同步,注重開發成本。

10.2.3 最大努力通知

最大努力通知也被稱爲定期校對,其實是對第二種解決方案的進一步優化。它引入了本地消息表來記錄錯誤消息,然後加入失敗消息的定期校對功能,來進一步保證消息會被下游系統消費。

在這裏插入圖片描述

第一步: 消息由系統A投遞到中間件

  1. 處理業務的同一事務中,向本地消息表中寫入一條記錄
  2. 準備專門的消息發送者不斷地發送本地消息表中的消息到消息中間件,如果發送失敗則重試

第二步: 消息由中間件投遞到系統B

  1. 消息中間件收到消息後負責將該消息同步投遞給相應的下游系統,並觸發下游系統的任務執行
  2. 當下遊系統處理成功後,向消息中間件反饋確認應答,消息中間件便可以將該條消息刪除,從而該事務完成
  3. 對於投遞失敗的消息,利用重試機制進行重試,對於重試失敗的,寫入錯誤消息表
  4. 消息中間件需要提供失敗消息的查詢接口,下游系統會定期查詢失敗消息,並將其消費

這種方式的優缺點:
優點: 一種非常經典的實現,實現了最終一致性。
缺點: 消息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理。

10.2.4 TCC事務

TCC即爲Try Confirm Cancel,它屬於補償型分佈式事務。TCC實現分佈式事務一共有三個步驟:

  • Try:嘗試待執行的業務
    這個過程並未執行業務,只是完成所有業務的一致性檢查,並預留好執行所需的全部資源
  • Confirm:確認執行業務
    確認執行業務操作,不做任何業務檢查, 只使用Try階段預留的業務資源。通常情況下,採用TCC
    則認爲 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。若Confirm階段真的
    出錯了,需引入重試機制或人工處理。
  • Cancel:取消待執行的業務
    取消Try階段預留的業務資源。通常情況下,採用TCC則認爲Cancel階段也是一定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。
    在這裏插入圖片描述

在這裏插入圖片描述

TCC兩階段提交與XA兩階段提交的區別是:

XA是資源層面的分佈式事務,強一致性,在兩階段提交的整個過程中,一直會持有資源的鎖。

  • TCC是業務層面的分佈式事務,最終一致性,不會一直持有資源的鎖。
  • TCC事務的優缺點:
    優點:把數據庫層的二階段提交上提到了應用層來實現,規避了數據庫層的2PC性能低下問題。
    缺點:TCC的Try、Confirm和Cancel操作功能需業務提供,開發成本高。

10.3 Seata介紹

2019 年 1 月,阿里巴巴中間件團隊發起了開源項目 Fescar(Fast & EaSy Commit And
Rollback),其願景是讓分佈式事務的使用像本地事務的使用一樣,簡單和高效,並逐步解決開發者們
遇到的分佈式事務方面的所有難題。後來更名爲 Seata,意爲:Simple Extensible Autonomous Transaction Architecture,是一套分佈式事務解決方案。
Seata的設計目標是對業務無侵入,因此從業務無侵入的2PC方案着手,在傳統2PC的基礎上演進。它把一個分佈式事務理解成一個包含了若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一致,要麼一起成功提交,要麼一起失敗回滾。此外,通常分支事務本身就是一個關係數據庫的本地事務。

在這裏插入圖片描述

Seata主要由三個重要組件組成:

  • TC:Transaction Coordinator 事務協調器,管理全局的分支事務的狀態,用於全局性事務的提交和回滾。
  • TM:Transaction Manager 事務管理器,用於開啓、提交或者回滾全局事務。
  • RM:Resource Manager 資源管理器,用於分支事務上的資源管理,向TC註冊分支事務,上報分支事務的狀態,接受TC的命令來提交或者回滾分支事務。
    在這裏插入圖片描述

Seata的執行流程如下:

  1. A服務的TM向TC申請開啓一個全局事務,TC就會創建一個全局事務並返回一個唯一的XID
  2. A服務的RM向TC註冊分支事務,並及其納入XID對應全局事務的管轄
  3. A服務執行分支事務,向數據庫做操作
  4. A服務開始遠程調用B服務,此時XID會在微服務的調用鏈上傳播
  5. B服務的RM向TC註冊分支事務,並將其納入XID對應的全局事務的管轄
  6. B服務執行分支事務,向數據庫做操作
  7. 全局事務調用鏈處理完畢,TM根據有無異常向TC發起全局事務的提交或者回滾
  8. TC協調其管轄之下的所有分支事務, 決定是否回滾

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

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

  2. 兩階段提交方面,傳統2PC無論第二階段的決議是commit還是rollback,事務性資源的鎖都要保持到Phase2完成才釋放。而Seata的做法是在Phase1 就將本地事務提交,這樣就可以省去Phase2持鎖的時間,整體提高效率。

10.4 Seata實現分佈式事務控制

本示例通過Seata中間件實現分佈式事務,模擬電商中的下單和扣庫存的過程
我們通過訂單微服務執行下單操作,然後由訂單微服務調用商品微服務扣除庫存

在這裏插入圖片描述

10.4.1 案例基本代碼

10.4.1.1 修改order微服務

controller

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class OrderController5 {

    @Autowired
    private OrderServiceImpl5 orderService;

    @RequestMapping("/order/prod/{pid}")
    public Order order(@PathVariable("pid") Integer pid) {
        return orderService.createOrder(pid);
    }

}

OrderService

import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderServiceImpl5{

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private ProductService productService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @GlobalTransactional//全局事務控制
    public Order createOrder(Integer pid) {
        log.info("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);

        //1 調用商品微服務,查詢商品信息
        Product product = productService.findByPid(pid);
        log.info("查詢到{}號商品的信息,內容是:{}", pid, JSON.toJSONString(product));

        //2 下單(創建訂單)
        Order order = new Order();
        order.setUid(1);
        order.setUsername("測試用戶");
        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.save(order);
        log.info("創建訂單成功,訂單信息爲{}", JSON.toJSONString(order));

        //3 扣庫存m
        productService.reduceInventory(pid, order.getNumber());

        //4 向mq中投遞一個下單成功的消息
        rocketMQTemplate.convertAndSend("order-topic", order);


        return order;
    }

}

ProductService

   @FeignClient(value = "service-product")
    public interface ProductService {
        //減庫存
        @RequestMapping("/product/reduceInventory")
        void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num")
                int num);
    }
10.4.1.2 修改Product微服務

controller

//減少庫存
@RequestMapping("/product/reduceInventory")
public void reduceInventory(Integer pid, int num) {
productService.reduceInventory(pid, num);
}

service

 @Override
    public void reduceInventory(Integer pid, int num) {
        Product product = productDao.findById(pid).get();
        product.setStock(product.getStock() - num);//減庫存
        productDao.save(product);
    }
10.4.1.3 異常模擬

ProductServiceImpl的代碼中模擬一個異常, 然後調用下單接口

  @Override
    public void reduceInventory(Integer pid, Integer number) {
        Product product = productDao.findById(pid).get();
        if (product.getStock() < number) {
            throw new RuntimeException("庫存不足");
        }
        int i = 1 / 0;
        product.setStock(product.getStock() - number);
        productDao.save(product);
    }

10.4.2 啓動Seata

10.4.2.1 下載seata

下載地址:https://github.com/seata/seata/releases/v0.9.0/

10.4.2.2 修改配置文件

將下載得到的壓縮包進行解壓,進入conf目錄,調整下面的配置文件:

  • registry.conf
registry {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

config {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}
  • nacos-config.txt
service.vgroup_mapping.service-product=default
service.vgroup_mapping.service-order=default

這裏的語法爲: service.vgroup_mapping.${your-service-gruop}=default ,中間的
${your-service-gruop} 爲自己定義的服務組名稱, 這裏需要我們在程序的配置文件中配置。

10.4.2.3 初始化seata在nacos的配置

# 初始化seata 的nacos配置
# 注意: 這裏要保證nacos是已經正常運行的
cd conf
nacos-config.sh 127.0.0.1

執行成功後可以打開Nacos的控制檯,在配置列表中,可以看到初始化了很多Group爲SEATA_GROUP的配置。

10.4.2.4 啓動seata服務

cd bin
seata-server.bat -p 9000 -m file

啓動後在 Nacos 的服務列表下面可以看到一個名爲 serverAddr 的服務。

10.4.3 使用Seata實現事務控制

10.4.3.1 初始化數據表

在我們的數據庫中加入一張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;
10.4.3.2 添加配置

在需要進行分佈式控制的微服務中進行下面幾項配置:

添加依賴

     <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

DataSourceProxyConfig

Seata 是通過代理數據源實現事務分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy 的
Bean,且是 @Primary默認的數據源,否則事務不會回滾,無法實現分佈式事務

   @Configuration
    public class DataSourceProxyConfig {
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DruidDataSource druidDataSource() {
            return new DruidDataSource();
        }

        @Primary
        @Bean
        public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
            return new DataSourceProxy(druidDataSource);
        }
    }

registry.conf

在resources下添加Seata的配置文件 registry.conf

registry {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

config {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

bootstrap.yaml

spring:
  application:
    name: service-product
  cloud:
    nacos:
      config:
        server-addr:  localhost:8848 # nacos的服務端地址
        namespace: public
        group: SEATA_GROUP
    alibaba:
      seata:
        tx-service-group: ${spring.application.name}

10.4.3.3 在order微服務開啓全局事務

@GlobalTransactional//全局事務控制
public Order createOrder(Integer pid) {}
10.4.3.4 測試

再次下單測試

10.4.4 seata運行流程分析

在這裏插入圖片描述

要點說明:
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 並執行,以完成分支事務回滾到之前的狀態,如果回滾失敗則會重試回滾操作。

到這裏,我們SpringCloud Alibaba 全家桶 就完結 了 ,感謝 大家的付出,能跟隨小劉學到這裏
截至 2020 . 4.1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章