分布式事務框架seata入門


一、簡介

在近幾年流行的微服務架構中,由於對服務和數據庫進行了拆分,原來的一個單進程本地事務變成多個進程的本地事務,這時要保證數據的一致性,就需要用到分布式事務了。分布式事務的解決方案有很多,其中國內比較主流的框架就是Seata了。

Seata 是一款開源的分布式事務解決方案,致力於提供高性能和簡單易用的分布式事務服務。Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務模式,為用戶打造一站式的分布式解決方案。

這里推薦使用AT模式,該模式具備代碼侵入性小,性能高等優點,前提是:

  1. 基於支持本地 ACID 事務的關系型數據庫

  2. Java 應用,通過 JDBC 訪問數據庫。

二、環境搭建

版本清單:

名稱 版本
Nacos 1.3.2
Seata 1.4.2
spring-boot 2.2.6.RELEASE
spring-cloud-starter-alibaba-nacos-discovery 2.2.6.RELEASE
spring-cloud-starter-openfeign 2.2.6.RELEASE
seata-spring-boot-starter 1.4.2
spring-cloud-starter-alibaba-seata 2.2.1.RELEASE

2.1 Nacos安裝

本地搭建nacos比較簡單,首先通過github下載nacos(我的是1.3.2版本,下載地址),然后解壓縮進入bin目錄,打開命令行工具運行如下命令即可啟動。

startup.sh -m standalone

啟動后訪問http://localhost:8848/nacos/index.html即可,默認賬號密碼是nacos/nacos。

2.2 部署seata-server

  • 1.下載解壓

    進入seata的發行頁面,選擇需要的版本下載,然后解壓。

  • 2.修改配置文件

    進入conf目錄(如/Users/ship/program/seata/seata-server-1.4.2/conf),可以看到有file.conf和registry.conf兩個配置文件。

    首先打開file.conf文件,修改配置如下

    ## transaction log store, only used in seata-server
    store {
      ## store mode: file、db、redis
      mode = "file" // 改為db
      ## rsa decryption public key
      publicKey = ""
      ## file store property
      file {
        ## store location dir
        dir = "sessionStore"
        # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
        maxBranchSessionSize = 16384
        # globe session size , if exceeded throws exceptions
        maxGlobalSessionSize = 512
        # file buffer size , if exceeded allocate new buffer
        fileWriteBufferCacheSize = 16384
        # when recover batch read size
        sessionReloadReadSize = 100
        # async, sync
        flushDiskMode = async
      }
    
      ## database store property
      db {
        ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
        datasource = "druid"
        ## mysql/oracle/postgresql/h2/oceanbase etc.
        dbType = "mysql"
        driverClassName = "com.mysql.jdbc.Driver"
        ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
        url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"// 改成自己的數據庫地址
        user = "mysql"   // 改成自己的數據庫用戶名
        password = "mysql"// 改成自己的數據庫密碼
        minConn = 5
        maxConn = 100
        globalTable = "global_table"
        branchTable = "branch_table"
        lockTable = "lock_table"
        queryLimit = 100
        maxWait = 5000
      }
    
      ...
    }
    
    

    這個數據庫會在下面的第四步再創建。

    然后打開registry.conf文件,修改注冊中心為nacos

    registry {
      # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
      type = "file" // 改為nacos
    
      nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        group = "SEATA_GROUP"
        namespace = "" // 默認名稱空間為public
        cluster = "default"
        username = "" // 默認是不需要密碼的,如果開啟了安全驗證則要填寫
        password = ""
      }
      eureka {
        serviceUrl = "http://localhost:8761/eureka"
        application = "default"
        weight = "1"
      }
      ...
    }
    
    config {
      # file、nacos 、apollo、zk、consul、etcd3
      type = "file" 
    
      nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = ""
        group = "SEATA_GROUP"  
        username = ""
        password = ""
        dataId = "seataServer.properties"
      }
      ...
    }
    
    
  • 3.同步配置到nacos

    低版本的seata需要在項目的resource目錄下創建file.conf和registry.conf文件,高版本的只需要將配置信息同步到nacos,然后讀取即可。

    首先需要在conf的同級目錄(如/Users/ship/program/seata/seata-server-1.4.2)下創建config.txt(下載地址)文件,然后修改數據庫配置信息。

    store.db.datasource=druid
    store.db.dbType=mysql
    store.db.driverClassName=com.mysql.jdbc.Driver
    store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
    store.db.user=seata  // 改為自己的賬號
    store.db.password=nova2020 // 改為自己的密碼
    store.db.minConn=5
    store.db.maxConn=30
    

    最后進入conf目錄(如/Users/ship/program/seata/seata-server-1.4.2/conf),下載nacos-config.sh(下載地址)並使用nacos-config.sh文件同步上傳配置到Nacos,命令如下:

    sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 023933c2-2825-46b2-a05a-4a407b479877
    

    命令參數介紹:

    -h : 指定nacos的ip地址。

    -p: 指定nacos的端口號。

    -g: 指定分組

    -t: 指定nacos的名稱空間,建議為seata單獨創建一個名稱空間。

    -u: nacos的用戶名,開啟認證才需要。

    -w: nacos的密碼,開啟認證才需要。

    打開nacos即可在對應的名稱空間下看到那些配置信息,如圖:

    不得不吐槽一下,像config.text和nacos-config.sh這些文件在0.9.0版本的seata都是和安裝包放一起的,高版本的還需要自己找真的坑。

  • 4.創建數據庫

    為seata-server創建seata庫並執行db_store.sql ,下載地址

    為每個業務庫執行db_undo_log.sql以添加回滾日志的表,下載地址

  • 5.啟動

進入bin目錄,輸入sh seata-server.sh即可啟動,部分啟動日志如下:

SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
SLF4J: See also http://www.slf4j.org/codes.html#replay
16:45:02.688  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
16:45:02.693  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /Users/ship/program/seata/seata-server-1.4.2/conf/registry.conf
16:45:02.785  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
16:45:02.785  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /Users/ship/program/seata/seata-server-1.4.2/conf/file.conf
16:45:03.411  INFO --- [                     main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
16:45:03.843  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091

三、實戰

前面環境已經搭好了,現在通過一個示例來驗證分布式事務的AT模式。

場景是創建訂單時會根據商品的金額來扣減用戶余額,分別對應order服務和account服務。

表結構設計如下:

DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
                             `id` int(11) NOT NULL AUTO_INCREMENT,
                             `user_id` varchar(255) DEFAULT NULL,
                             `commodity_code` varchar(255) DEFAULT NULL,
                             `count` int(11) DEFAULT 0,
                             `money` int(11) DEFAULT 0,
                             PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
                               `id` int(11) NOT NULL AUTO_INCREMENT,
                               `user_id` varchar(255) DEFAULT NULL,
                               `money` int(11) DEFAULT 0,
                               PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3.1order服務

創建一個order項目並添加nacos、fegin和seata的依賴,pom.xml部分如下:

 <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2.2.1.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

這里為了引入最新版本的seata-spring-boot-starter,就在spring-cloud-starter-alibaba-seata作了排除,也是官方推薦的方式。如果發現你的項目啟動不起來或者有其他問題,可能是版本依賴有問題。

啟動類OrderApplication.java添加需要的注解

@EnableAutoDataSourceProxy //這個一定要加,如果不加通過配置文件開啟也可以
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

核心部分OrderService

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2021/8/23
 */
@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private AccountClient accountClient;

    @GlobalTransactional
    @Transactional(rollbackFor = Exception.class)
    public OrderVO create(OrderDTO orderDTO) {
        // 創建訂單
        Order order = new Order();
        BeanUtils.copyProperties(orderDTO,order);
        orderDao.insert(order);
        // 扣除賬戶余額
        AccountDeductDTO accountDeductDTO = new AccountDeductDTO();
        Integer total = orderDTO.getMoney() * orderDTO.getCount();
        accountDeductDTO.setMoney(total);
        accountDeductDTO.setUserId(orderDTO.getUserId());
        accountClient.deduct(accountDeductDTO);
        return new OrderVO(order.getId());
    }
}

只需一個@GlobalTransactional注解即可,用在分布式事務開啟的方法上。

修改配置文件bootstrap.yml

spring:
  application:
    name: order
  datasource:
    username: root
    password: 1234
    url: jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
    type: com.alibaba.druid.pool.DruidDataSource

server:
  port: 9900
seata:
  registry:
    type: nacos
    nacos:
      application: seata-server  # 不配置名稱空間,默認public
      server-addr: 127.0.0.1:8848
      group: "SEATA_GROUP"
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: "SEATA_GROUP"
      namespace: 023933c2-2825-46b2-a05a-4a407b479877 # 配置名稱空間為seata
  enabled: true
  tx-service-group: my_test_tx_group  # 要與nacos上的配置一致
  service:
    vgroup-mapping:
      my_test_tx_group: default # 要與nacos上的配置一致
    disable-global-transaction: false

注意:seata.tx-service-group屬性值要和seata-server的config.txt文件中的service.vgroupMapping.${分組名}=default分組名一一對應,這里我用的默認配置my_test_tx_group。關於事務分組還有很多種玩法,可以參考這里https://seata.io/zh-cn/docs/user/txgroup/transaction-group.html。

3.2 account服務

account服務項目結構基本與order服務一致,只是service代碼不同。

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2021/8/23
 */
@Service
public class AccountService {

    @Autowired
    private AccountDao accountDao;

    @Transactional(rollbackFor = Exception.class)
    public void deduct(AccountDeductDTO accountDeductDTO) {
        QueryWrapper<Account> wrapper = new QueryWrapper();
        wrapper.lambda().eq(Account::getUserId, accountDeductDTO.getUserId());
        Account account = accountDao.selectOne(wrapper);
        Integer money = account.getMoney() - accountDeductDTO.getMoney();
        account.setMoney(money);

        // 更新余額
        accountDao.updateById(account);

//        int i = 1 / 0;
    }
}

3.3 測試

  1. 啟動seata-server

  2. 啟動order服務和account服務,並能成功在nacos上看到注冊實例。

  1. 首先給用戶1111的賬戶初始100元的余額,sql如下

    insert into account_tbl(user_id,money) VALUES(1111,100);
    
  2. 請求下單接口http://localhost:9900/order/create,POST body參數如下:

    {
    	"userId":1111,
    	"commodityCode":"code",
    	"count":2,
    	"money":10
    } 
    

    查詢數據庫可以發現,order_tbl表已經有一條訂單數據了,並且用戶的1111的余額變成了80,說明事務提交成功

  1. 這時將account服務的AccountService中的int i = 1 / 0;這行代碼取消注釋,並在重啟account服務之后再次請求下單接口。

  2. 查看控制台日志發現拋異常了,再次查詢order_tbl表發現還是一條訂單數據,account_tbl表的用戶余額也還是80,說明發生了全局事務回滾。 通過order服務的日志也可以看出,account服務扣減余額接口異常導致了回滾。

    2021-09-04 21:18:28.323  INFO 72668 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.3.253:8091:3900290643103043585,branchId=3900290643103043591,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_order,applicationData=null
    2021-09-04 21:18:28.326  INFO 72668 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.3.253:8091:3900290643103043585 3900290643103043591 jdbc:mysql://127.0.0.1:3306/seata_order null
    2021-09-04 21:18:28.327  INFO 72668 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
    2021-09-04 21:36:17.280  INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.3.253:8091:3900290643103043595]
    2021-09-04 21:36:17.282 ERROR 72668 --- [nio-9900-exec-3] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, jdbcUrl : jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, lastPacketReceivedIdleMillis : 1068019
    2021-09-04 21:36:17.782  INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : Suspending current transaction, xid = 192.168.3.253:8091:3900290643103043595
    2021-09-04 21:36:17.782  INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.3.253:8091:3900290643103043595] rollback status: Rollbacked //回滾
    

    至此說明我們的分布式事務控制生效了,示例代碼包括腳本都已經提交到我的github上,需要的請點擊

四、總結

Seata框架上手不難,重點還是理解其實現原理和做到靈活使用,比如事務分組的設計就很巧妙,其AT模式比起之前用過的TCC框架好太多,阿里出品還是厲害啊。

參考資料:

seata官方文檔,https://seata.io/zh-cn/docs/overview/what-is-seata.html

https://github.com/seata/seata-samples


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM