分佈式事務解決方案之 Alibaba Seata

關於事務的幾點常識

本地事務

該類事務須要知足四大特性:ACID(原子性、一致性、隔離性、持久性),僅限於對單一數據庫資源的訪問控制。java

  • 原子性(Atomicity):指事務做爲總體來執行,要麼所有執行,要麼所有不執行。
  • 一致性(Consistency):指事務應確保數據從一個一致的狀態轉變爲另外一個一致狀態。
  • 隔離性(Isolation):指多個事務併發時,一個事務的執行不該影響其它事務的執行。
  • 持久性(Durability):指已提交的事務修改數據會被持久保存。

柔性事務

若是將實現了 ACID 的四大事務特性的事務成爲剛性事務的話,那麼基於 BASE 事務要素的事務則成爲柔性事務。node

BASE 是基本可用、柔性狀態和最終一致性這三個特性的縮寫。mysql

  • 基本可用(Basically Available):容許分佈式事務參與方不必定要同時在線。
  • 柔性狀態(Soft state):則容許系統狀態更新有必定的延時。
  • 最終一致性(Eventually consistent):一般是經過消息傳遞的方式保證系統的 最終一致性

ACID 事務中對隔離性的要求很高,在事務執行過程當中,必須將全部的資源鎖定。而柔性事務的理念則是經過業務邏輯將互斥鎖操做從資源層面移至業務層面。經過放寬對 強一致性 的要求,來換取系統吞吐量的提高。git

什麼是分佈式事務

咱們能夠把一個分佈式事務理解成一個包含了 若干分支事務的全局事務,全局事務的職責是協調其下管轄的分支事務達成一致,要麼一塊兒成功提交,要麼一塊兒失敗回滾。此外,一般分支事務自己就是一個知足 ACID 的本地事務。這是咱們對分佈式事務結構的基本認識,與 XA 是一致的。github

https://qiniuyun.antoniopeng.com/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200731140321.png

分佈式事務問題

傳統的單體應用中,一個業務操做可能須要調用三個模塊完成,此時數據的一致性有本地事務來保證。redis

https://qiniuyun.antoniopeng.com/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200731124316.png

隨着業務需求的變化,單體應用被拆分紅微服務應用,業務操做須要調用三個服務來完成,原來的三個模塊被拆分紅三個獨立的應用,分別使用獨立的數據源。此時每一個服務內部的數據一致性由本地事務來保證,可是全局的數據一致性問題沒法保證。spring

https://qiniuyun.antoniopeng.com/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200731135455.png

在微服務分佈式架構中因爲全局數據一致性無法保證所產生的問題就是分佈式事務問題。簡單來講,一次業務操做須要操做多個數據源或須要進行遠程調用,就會產生分佈式事務問題。sql

製造一個分佈式事務問題

這裏咱們會建立三個服務,分別是訂單服務、庫存服務、帳戶服務。當用戶下單時,會在 訂單服務 中建立一個訂單,而後經過遠程調用 庫存服務 扣減當前商品的庫存,再經過遠程調用 帳戶服務 來扣減用戶帳戶裏面的餘額。該業務操做經過兩次遠程調用,跨越三個數據庫,明顯存在分佈式事務問題。數據庫

Alibaba Seata 簡介

概述

Seata 是一款開源的分佈式事務解決方案,提供高性能和簡單易用的分佈式事務服務,提供了 ATSAGAXA 事務模式。bash

組件

  • TC 事務協調者:維護全局和分支事務的狀態,驅動全局事務提交或回滾。
  • TM 事務管理器:定義全局事務的範圍,從開始全局事務 > 提交或回滾事務。
  • RM 資源管理器:管理分支事務處理的資源,與 TC 合做以註冊分支事務和報告分支事務的狀態,驅動分支事務提交或回滾。

接入 Seata 分佈式事務

安裝配置 Seata Server

  • 先從官網下載 seata server,下載地址:https://github.com/seata/seata/releases

  • 解壓安裝包到指定目錄,修改 conf 目錄下的 file.conf 配置文件。主要修改自定義事務名稱、事務日誌存儲模式爲 db 以及數據庫鏈接信息。

    service {
    #vgroup->rgroup
    vgroup_mapping.fsp_tx_group = "default" #修改事務組名稱爲:fsp_tx_group,和客戶端自定義的名稱對應
    #only support single node
    default.grouplist = "127.0.0.1:8091"
    #degrade current not support
    enableDegrade = false
    #disable
    disable = false
    #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
    max.commit.retry.timeout = "-1"
    max.rollback.retry.timeout = "-1"
    }
    
    ## transaction log store
    store {
    ## store mode: file、db
    mode = "db" #修改此處將事務信息存儲到數據庫中
    
    ## database store
    db {
      ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
      datasource = "dbcp"
      ## mysql/oracle/h2/oceanbase etc.
      db-type = "mysql"
      driver-class-name = "com.mysql.jdbc.Driver"
      url = "jdbc:mysql://localhost:3306/seat-server" #修改數據庫鏈接地址
      user = "root" #修改數據庫用戶名
      password = "123456" #修改數據庫密碼
      min-conn = 1
      max-conn = 3
      global.table = "global_table"
      branch.table = "branch_table"
      lock-table = "lock_table"
      query-limit = 100
    }
    }
  • 因爲使用了 db 模式的存儲事務日誌,因此咱們須要建立一個 seata server 數據庫,運行在 seata server 安裝包中的 /conf/db_store.sql 文件。

  • 修改 conf 目錄下的 registry.conf 配置文件,指明配置中心爲 nacos,並配置 nacos 鏈接信息。

    nacos 的安裝及使用能夠參考:使用 Spring Cloud Alibaba Nacos Discovery 實現服務註冊與發現

  • 最後依此啓動 nacos serverseata server 安裝包中的 /bin/seata-server.bat

建立數據庫

  • seata-order:存儲訂單的數據庫。
  • seata-storage:存儲庫存的數據庫。
  • seata-count:存儲帳戶信息的數據庫。

初始化業務表

order 表

CREATE TABLE `order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
  `product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
  `count` int(11) DEFAULT NULL COMMENT '數量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金額',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '訂單狀態:0:建立中;1:已完結' AFTER `money` ;

storage 表

CREATE TABLE `storage` (
                         `id` bigint(11) NOT NULL AUTO_INCREMENT,
                         `product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
                         `total` int(11) DEFAULT NULL COMMENT '總庫存',
                         `used` int(11) DEFAULT NULL COMMENT '已用庫存',
                         `residue` int(11) DEFAULT NULL COMMENT '剩餘庫存',
                         PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');

account 表

CREATE TABLE `account` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
  `total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
  `used` decimal(10,0) DEFAULT NULL COMMENT '已用餘額',
  `residue` decimal(10,0) DEFAULT '0' COMMENT '剩餘可用額度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');

最後還須要在每一個數據庫的表中建立建立事務日誌表,運行在 seata server 安裝包中的 /conf/db_undo_log.sql 文件。

完成後全部數據庫表如圖所示:

https://qiniuyun.antoniopeng.com/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200731132725.png

相關配置

seata-order-serviceseata-storage-serviceseata-account-service 三個服務進行配置大體相同,以 seata-account-service 爲例。

  • application.yml 文件中主要加入如下配置:

    spring:
    cloud:
      alibaba:
        seata:
          tx-service-group: fsp_tx_group #自定義事務組名稱須要與 seata-server 中的對應

    文件完整內容以下

    server:
    port: 8081
    
    spring:
    application:
      name: seata-account-service
    cloud:
      alibaba:
        seata:
          tx-service-group: fsp_tx_group
      nacos:
        discovery:
          server-addr: localhost:8848
    datasource:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/seata-account
      username: root
      password: 123456
    
    mybatis:
    mapperLocations: classpath:mapper/*.xml
    
    logging:
    level:
      io:
        seata: info
  • 建立 file.conf 文件,主要修改自定義事務名稱:

    service {
    #vgroup->rgroup
    vgroup_mapping.fsp_tx_group = "default" #修改自定義事務組名稱
    #only support single node
    default.grouplist = "127.0.0.1:8091"
    #degrade current not support
    enableDegrade = false
    #disable
    disable = false
    #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
    max.commit.retry.timeout = "-1"
    max.rollback.retry.timeout = "-1"
    disableGlobalTransaction = false
    }
  • 建立 registry.conf 配置文件,主要指明 nacos 註冊中心:

    registry {
    # file 、nacos 、eureka、redis、zk
    type = "nacos" #修改成nacos
    
    nacos {
      serverAddr = "localhost:8848" #修改成nacos的鏈接地址
      namespace = ""
      cluster = "default"
    }
    }
  • 在啓動類中取消自動建立數據源

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataOrderServiceApplication {
    
      public static void main(String[] args) {
          SpringApplication.run(SeataOrderServiceApplication.class, args);
      }
    
    }
  • 建立 DataSourceProxyConfig 配置文件使用 Seata 對數據源進行代理

    /**
    * 使用 Seata 對數據源進行代理
    */
    @Configuration
    public class DataSourceProxyConfig {
    
      @Value("${mybatis.mapperLocations}")
      private String mapperLocations;
    
      @Bean
      @ConfigurationProperties(prefix = "spring.datasource")
      public DataSource druidDataSource(){
          return new DruidDataSource();
      }
    
      @Bean
      public DataSourceProxy dataSourceProxy(DataSource dataSource) {
          return new DataSourceProxy(dataSource);
      }
    
      @Bean
      public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
          SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
          sqlSessionFactoryBean.setDataSource(dataSourceProxy);
          sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                  .getResources(mapperLocations));
          sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
          return sqlSessionFactoryBean.getObject();
      }
    
    }

業務代碼

在業務實現類中使用 @GlobalTransaction 註解開啓分佈式事務。

/**
 * 訂單業務實現類
 */
@Service
public class OrderServiceImpl implements OrderService {

    private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private StorageService storageService;
    @Autowired
    private AccountService accountService;

    /**
     * 建立訂單->調用庫存服務扣減庫存->調用帳戶服務扣減帳戶餘額->修改訂單狀態
     */
    @Override
    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    public void create(Order order) {

        LOGGER.info("開始下單");
        //當前服務建立訂單
        orderDao.create(order);

        //遠程調用庫存服務扣減庫存
        LOGGER.info("order-service 中扣減庫存開始");
        storageService.decrease(order.getProductId(),order.getCount());
        LOGGER.info("order-service 中扣減庫存結束");

        //遠程調用帳戶服務扣減餘額
        LOGGER.info("order-service 中扣減餘額開始");
        accountService.decrease(order.getUserId(),order.getMoney());
        LOGGER.info("order-service 中扣減餘額結束");

        //修改訂單狀態爲已完成
        LOGGER.info("order-service 中修改訂單狀態開始");
        orderDao.update(order.getUserId(),0);
        LOGGER.info("order-service 中修改訂單狀態結束");

        LOGGER.info("下單結束");
    }
}
相關文章
相關標籤/搜索