分布式事务seata之AT模式原理分析及实战

seata是干什么的?

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。本文主要介绍seata中AT模式的原理以及使用方式。

seata中关键角色

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

整体机制

seata是对两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志(undo_log)记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

运行流程示意图如下(假设我们的业务系统有四个服务,现在模拟一次购物行为,business:聚合业务层,account:扣费,storage:减库存,order:存订单):

分布式事务seata之AT模式原理分析及实战_第1张图片

运行流程

  1. 在business模块中使用@GlobalTransactional注解,TM向TC发起全局事物,生成XID(全局锁),分别调用account、storage、order模块;
  2. 在account模块中进行扣费逻辑处理,这时进行写表操作,undo_log记录回滚日志,通知TC操作结果;
  3. 在storage模块中进行减库存逻辑处理,这时进行写表操作,undo_log记录回滚日志,通知TC操作结果;
  4. 在order模块中进行保存订单逻辑处理,这时进行写表操作,undo_log记录回滚日志,通知TC操作结果;
  5. 正常情况:business调用其他模块全部成功,TM通知TC全部提交,TC通知所有RM提交成功,删除本地undo_log;
  6. 异常情况:business调用其他模块出现异常,TM通知TC全局rollback,TC通知所有RM进行回滚,根据undo_log进行反向操作,还原数据,最后删除undo_log;

实战操作

  1. 环境说明

    macOS Mojave 10.14.6
    spring-cloud-alibaba 2.2.1.RELEASE

    spring-cloud-starter-alibaba-seata

    2.2.1.RELEASE
    seata-spring-boot-starter 1.4.0
    seata 1.4.0
    nacos 1.3.1
    mysql 5.7.17-log
  2. 搭建部署TC(seata-server)
    修改配置
    store {
      ## store mode: file、db、redis
      mode = "db"
    
      ## 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"
        url = "jdbc:mysql://127.0.0.1:3306/seata"
        user = "root"
        password = "123456"
        minConn = 5
        maxConn = 100
        globalTable = "global_table"
        branchTable = "branch_table"
        lockTable = "lock_table"
        queryLimit = 100
        maxWait = 5000
      }
    
      ## redis store property
      redis {
        host = "127.0.0.1"
        port = "6379"
        password = ""
        database = "0"
        minConn = 1
        maxConn = 10
        maxTotal = 100
        queryLimit = 100
      }
    
    }

    sql初始化,先创建数据库seata,再执行建表sql

    -- the table to store GlobalSession data
    CREATE TABLE IF NOT EXISTS `global_table`
    (
        `xid`                       VARCHAR(128) NOT NULL,
        `transaction_id`            BIGINT,
        `status`                    TINYINT      NOT NULL,
        `application_id`            VARCHAR(32),
        `transaction_service_group` VARCHAR(32),
        `transaction_name`          VARCHAR(128),
        `timeout`                   INT,
        `begin_time`                BIGINT,
        `application_data`          VARCHAR(2000),
        `gmt_create`                DATETIME,
        `gmt_modified`              DATETIME,
        PRIMARY KEY (`xid`),
        KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
    -- the table to store BranchSession data
    CREATE TABLE IF NOT EXISTS `branch_table`
    (
        `branch_id`         BIGINT       NOT NULL,
        `xid`               VARCHAR(128) NOT NULL,
        `transaction_id`    BIGINT,
        `resource_group_id` VARCHAR(32),
        `resource_id`       VARCHAR(256),
        `branch_type`       VARCHAR(8),
        `status`            TINYINT,
        `client_id`         VARCHAR(64),
        `application_data`  VARCHAR(2000),
        `gmt_create`        DATETIME(6),
        `gmt_modified`      DATETIME(6),
        PRIMARY KEY (`branch_id`),
        KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
    -- the table to store lock data
    CREATE TABLE IF NOT EXISTS `lock_table`
    (
        `row_key`        VARCHAR(128) NOT NULL,
        `xid`            VARCHAR(96),
        `transaction_id` BIGINT,
        `branch_id`      BIGINT       NOT NULL,
        `resource_id`    VARCHAR(256),
        `table_name`     VARCHAR(32),
        `pk`             VARCHAR(36),
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;

    更改config.txt(https://github.com/seata/seata/tree/v1.4.0),并将其上传至nacos配置中心,其中需要修改的是store.mode、store.db.url、store.db.user、store.db.password,注意service.default.grouplist需要改成当前seata-server所在服务器的ip地址,service.vgroupMapping.my_test_tx_group中的my_test_tx_group为事务分组的名称要与微服务中配置tx-service-group的值一致,后面会说到这个配置;
    上传配置至nacos(在seata-1.4.0/script/config-center/nacos目录下):./nacos-config.sh -h 10.0.0.251
    启动seata-server并指定本机IP:./seata-server.sh -h 10.0.0.251

    # seata-1.4.0/script/config-center/config.txt
    transport.type=TCP
    transport.server=NIO
    transport.heartbeat=true
    transport.enableClientBatchSendRequest=false
    transport.threadFactory.bossThreadPrefix=NettyBoss
    transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
    transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
    transport.threadFactory.shareBossWorker=false
    transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
    transport.threadFactory.clientSelectorThreadSize=1
    transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
    transport.threadFactory.bossThreadSize=1
    transport.threadFactory.workerThreadSize=default
    transport.shutdown.wait=3
    service.vgroupMapping.my_test_tx_group=default
    service.default.grouplist=127.0.0.1:8091
    service.enableDegrade=false
    service.disableGlobalTransaction=false
    client.rm.asyncCommitBufferLimit=10000
    client.rm.lock.retryInterval=10
    client.rm.lock.retryTimes=30
    client.rm.lock.retryPolicyBranchRollbackOnConflict=true
    client.rm.reportRetryCount=5
    client.rm.tableMetaCheckEnable=false
    client.rm.sqlParserType=druid
    client.rm.reportSuccessEnable=false
    client.rm.sagaBranchRegisterEnable=false
    client.tm.commitRetryCount=5
    client.tm.rollbackRetryCount=5
    client.tm.defaultGlobalTransactionTimeout=60000
    client.tm.degradeCheck=false
    client.tm.degradeCheckAllowTimes=10
    client.tm.degradeCheckPeriod=2000
    store.mode=db
    store.file.dir=file_store/data
    store.file.maxBranchSessionSize=16384
    store.file.maxGlobalSessionSize=512
    store.file.fileWriteBufferCacheSize=16384
    store.file.flushDiskMode=async
    store.file.sessionReloadReadSize=100
    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=root
    store.db.password=123456
    store.db.minConn=5
    store.db.maxConn=30
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxWait=5000
    store.redis.host=127.0.0.1
    store.redis.port=6379
    store.redis.maxConn=10
    store.redis.minConn=1
    store.redis.database=0
    store.redis.password=null
    store.redis.queryLimit=100
    server.recovery.committingRetryPeriod=1000
    server.recovery.asynCommittingRetryPeriod=1000
    server.recovery.rollbackingRetryPeriod=1000
    server.recovery.timeoutRetryPeriod=1000
    server.maxCommitRetryTimeout=-1
    server.maxRollbackRetryTimeout=-1
    server.rollbackRetryTimeoutUnlockEnable=false
    client.undo.dataValidation=true
    client.undo.logSerialization=jackson
    client.undo.onlyCareUpdateColumns=true
    server.undo.logSaveDays=7
    server.undo.logDeletePeriod=86400000
    client.undo.logTable=undo_log
    client.log.exceptionRate=100
    transport.serialization=seata
    transport.compressor=none
    metrics.enabled=false
    metrics.registryType=compact
    metrics.exporterList=prometheus
    metrics.exporterPrometheusPort=9898
  3. undo_log建表初始化
    分别在account、storage、order对应的DB中执行以下sql(由于我这里business服务是聚合服务,没有连接DB,所以不需要执行此sql):
    CREATE TABLE `undo_log` (
      `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
      `xid` varchar(100) NOT NULL COMMENT 'global transaction id',
      `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
      `rollback_info` longblob NOT NULL COMMENT 'rollback info',
      `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
      `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
      `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

     

  4. 搭建部署TM(business)
    配置文件(application.yml,这里只有seata相关配置,其他配置大家根据自己的情况配置):
    #分布式事物seata
    seata:
      enabled: true
      application-id: ${spring.application.name}
      tx-service-group: my_test_tx_group
      enable-auto-data-source-proxy: true
      config:
        type: nacos
        nacos:
          server-addr: 10.0.0.251:8848
          group: SEATA_GROUP
      registry:
        type: nacos
        nacos:
          application: seata-server
          server-addr: 10.0.0.251:8848
    maven依赖,RM的依赖和TM相同(为了匹配seata-server的版本,我这里自己定义了seata-spring-boot-starter的版本也为1.4.0):
    
        com.alibaba.cloud
        spring-cloud-starter-alibaba-seata
        
            
                io.seata
                seata-spring-boot-starter
            
        
    
    
        io.seata
        seata-spring-boot-starter
        1.4.0
    

    代码入口:

    @GlobalTransactional(timeoutMills = 300000, name = "dubbo-seata-example")
    @Override
    public void saveOrder(OrderReqVo req) {
        log.info("开始全局事务,XID = " + RootContext.getXID());
        //扣余额
    
        boolean flag = rpcUserBalanceService.decreaseUserBalance(xxxx);
        if (!flag) {
            throw new BusinessException("XXXX");
        }
        //减库存
        flag = rpcStorageService.decreaseProductStock(xxxx);
        if (!flag) {
            throw new BusinessException("XXXX");
        }
        //存订单
        flag = rpcOrderService.saveOrder(xxxxx);
        if (!flag) {
            throw new BusinessException("XXXX");
        }
    }

     

  5. 搭建部署RM(account、storage、order)
    配置文件和maven依赖和TM一样,三个微服务模块分别创建对应方法,然后启动服务:
    ########每个微服务需要开启本地事务
    # account
    public boolean decreaseUserBalance(xxxxx) {
      return 扣款结果;      
    }
    # storage
    public boolean decreaseProductStock(xxxxx) {
      return 减库存结果;       
    }
    # order
    public boolean saveOrder(xxxxx) {   
      return 保存订单结果;
    }

     

  6. 实战模拟
    模拟正常请求,查看对应数据库的数据是否正常;
    模拟出现异常时(比如扣款成功,减库存成功,保存订单失败),查看对应数据库的数据是否正常;
    debug查看分布式事务中间过程,undo_log的数据是什么样子的。

你可能感兴趣的:(分布式事务,java,分布式,数据库)