分布式事务解决方案 - Seata

image.png

知行.png

1.Seata介绍

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

  • 微服务框架支持

目前已支持 Dubbo、Spring Cloud、Sofa-RPC、Motan 和 Grpc 等RPC框架,其他框架持续集成中

  • AT 模式

提供无侵入自动补偿的事务模式,目前已支持 MySQL、 Oracle 、PostgreSQL和 TiDB的AT模式,H2 开发中

  • TCC 模式

    支持 TCC 模式并可与 AT 混用,灵活度更高

  • SAGA 模式

    为长事务提供有效的解决方案

  • XA 模式

    支持已实现 XA 接口的数据库的 XA 模式

  • 高可用

    支持基于数据库存储的集群模式,水平扩展能力强

2.安装启动seata-server

2.1下载安装

本文档以Seata1.4.2为例,注册中心使用zk,配置中心使用Apollo

seata.io/zh-cn/blog/…

2.2初始化数据库

CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime(6) DEFAULT NULL,
  `gmt_modified` datetime(6) DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) DEFAULT NULL,
  `transaction_service_group` varchar(32) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`) USING BTREE,
  KEY `idx_transaction_id` (`transaction_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,
  `xid` varchar(96) DEFAULT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(36) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`),
  KEY `idx_branch_id` (`branch_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码

2.3配置

2.3.1 registry.conf

registry {
  type = "zk"
  zk {
   #集群名
    cluster = "default"
    serverAddr = "xxx:1500"
    sessionTimeout = 60000
    connectTimeout = 20000
  }
}

config {
  type = "apollo"

  apollo {
    appId = "arch-seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://xxx:8080"
    apolloConfigService = "http://xxx:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
   #bug,cluster源码中需要由seata指定,且env没有参数指定,只能在bat、sh启动时添加-Denv=FAT
    cluster = "test"
    seata = "test"
  }
}
复制代码

2.3.2 Apollo配置

transport.type = TCP
transport.server = NIO
transport.heartbeat = true
transport.enableClientBatchSendRequest = true
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
store.mode = db
store.db.datasource = druid
store.db.dbType = mysql
store.db.driverClassName = com.mysql.jdbc.Driver
store.db.url = jdbc:mysql://xxx:3306/server_seata?rewriteBatchedStatements=true
store.db.user = 
store.db.password = 
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
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
server.distributedLockExpireTime = 10000
server.undo.logSaveDays = 7
server.undo.logDeletePeriod = 86400000
log.exceptionRate = 100
transport.serialization = seata
transport.compressor = none
metrics.enabled = false
metrics.registryType = compact
metrics.exporterList = prometheus
metrics.exporterPrometheusPort = 9898
复制代码

2.4 启动

在bat.sh启动时添加-Denv=FAT

3.客户端

3.1 pom

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>
复制代码

3.2 初始化数据库(AT模式)

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
  `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 NOT NULL COMMENT 'create datetime',
  `log_modified` datetime NOT NULL COMMENT 'modify datetime',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
复制代码

3.3 配置



#配合配置文件的service.vgroupMapping.my_test_tx_group
seata.tx-service-group = my_test_tx_group
#事务分组,key为事务分组,值为seata-server集群名
service.vgroupMapping.my_test_tx_group = default

seata.registry.type = zk
#seata.registry.zk.cluster = default 此配置项在client端无效
seata.registry.zk.server-addr = xxx:2181
seata.registry.zk.session-timeout = 60000
seata.registry.zk.connect-timeout = 20000

seata.config.type = apollo
seata.config.apollo.apollo-meta = http://xxx:8080
seata.config.apollo.app-id = arch-seata-debug
seata.config.apollo.namespace = application

#以下配置可以独立
transport.type = TCP
transport.server = NIO
transport.heartbeat = true
transport.enableClientBatchSendRequest = true
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.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.tableMetaCheckerInterval = 60000
client.rm.sqlParserType = druid
client.rm.reportSuccessEnable = false
client.rm.sagaBranchRegisterEnable = false
client.rm.tccActionInterceptorOrder = -2147482648
client.tm.commitRetryCount = 5
client.tm.rollbackRetryCount = 5
client.tm.defaultGlobalTransactionTimeout = 60000
client.tm.degradeCheck = false
client.tm.degradeCheckAllowTimes = 10
client.tm.degradeCheckPeriod = 2000
client.tm.interceptorOrder = -2147482648
client.undo.dataValidation = true
client.undo.logSerialization = jackson
client.undo.onlyCareUpdateColumns = true
client.undo.logTable = undo_log
client.undo.compress.enable = true
client.undo.compress.type = zip
client.undo.compress.threshold = 64k
#RoundRobinLoadBalance,RandomLoadBalance,LeastActiveLoadBalance,ConsistentHashLoadBalance
client.loadBalance.type = RandomLoadBalance
client.loadBalance.virtualNodes = 10
log.exceptionRate = 100
transport.serialization = seata
transport.compressor = none

复制代码

vm options

-Denv=XXX -Dapollo.cluster=test -Dapollo.meta=http://xxx:8080 -Dconfig.apollo.seata=test -Dconfig.apollo.apolloConfigService=http://xxx:8080

3.4 AT模式

@GlobalTransactional(name = "testAT")
public void testAT() {
   //本地事务
    dosql();
   //RPC调用
    dorpc();
}
复制代码

3.5 TCC模式

@GlobalTransactional(name = "testTCC")
public void testTCC() {
    //RPC或本地
   TccActionTwoImpl.prepare(null,"a",new ArrayList());
    //RPC或本地
    TccActionOneImpl.prepare(null);
}


@LocalTCC
public interface TccActionTwo{
     /**
     * Prepare boolean. TwoPhaseBusinessAction.name =branch.resourseId
     *
     * @param actionContext the action context
     * @param b             the b 会被放入BusinessActionContext的actionContext中  可通过注解的name取出
     * @param list          the list 会被放入BusinessActionContext的actionContext中
     * @return the boolean
     */
      @TwoPhaseBusinessAction(name = "DubboTccActionTwo", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext,
                           @BusinessActionContextParameter(paramName = "b") String b,
                           @BusinessActionContextParameter(paramName = "c", index = 1) List list);
    
    boolean commit(BusinessActionContext var1);

    boolean rollback(BusinessActionContext var1);
}

@LocalTCC
public interface TccActionOne{
      @TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext);
    
    boolean commit(BusinessActionContext var1);

    boolean rollback(BusinessActionContext var1);
}
复制代码

4.SEATA-SERVER

4.1 架构模型

  • TC (Transaction Coordinator) - 事务协调者
    • 维护全局和分支事务的状态,驱动全局事务提交或回滚。
    • 对应Seata-server
  • TM (Transaction Manager) - 事务管理器
    • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器
    • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

4.2 Seata-server概述

TC承担维护全局和分支事务的状态,并驱动全局事务提交或回滚。对整个分布事务的执行和状态需要实时感知。而整个分布式事务由各个执行节点组成,每一个节点执行的事务称之为分支事务。一个全局事务上注册的所有分支事务,一起组成了分布式事务的所有信息。在全局事务提交或回滚时,可以执行到各个分支事务。

4.2.1 全局事务状态表

状态 代码 备注
全局事务开始(Begin) 1 此状态可以接受新的分支事务注册
全局事务提交中(Committing) 2 这个状态会随时改变
全局事务提交重试(CommitRetry) 3 在提交异常被解决后尝试重试提交
全局事务回滚中(Rollbacking) 4 正在重新回滚全局事务
全局事务回滚重试中(RollbackRetrying) 5 在全局回滚异常被解决后尝试事务重试回滚中
全局事务超时回滚中(TimeoutRollbacking) 6 全局事务超时回滚中
全局事务超时回滚重试中(TimeoutRollbackRetrying) 7 全局事务超时回滚重试中
异步提交中(AsyncCommitting) 8 异步提交中
二阶段已提交(Committed) 9 二阶段已提交,此状态后全局事务状态不会再改变
二阶段提交失败(CommitFailed) 10 二阶段提交失败
二阶段决议全局回滚(Rollbacked) 11 二阶段决议全局回滚
二阶段全局回滚失败(RollbackFailed) 12 二阶段全局回滚失败
二阶段超时回滚(TimeoutRollbacked) 13 二阶段超时回滚
二阶段超时回滚失败(TimeoutRollbackFailed) 14 二阶段超时回滚失败
全局事务结束(Finished) 15 全局事务结束
未知状态(UnKnown) 0 未知状态

4.2.2 分支事务状态表

状态 代码 备注
分支事务注册(Registered) 1 向TC注册分支事务
分支事务一阶段完成(PhaseOne_Done) 2 分支事务一阶段业务逻辑完成
分支事务一阶段失败(PhaseOne_Failed) 3 分支事务一阶段业务逻辑失败
分支事务一阶段超时(PhaseOne_Timeout) 4 分支事务一阶段处理超时
分支事务二阶段已提交(PhaseTwo_Committed) 5 分支事务二阶段提交
分支事务二阶段提交失败重试(PhaseTwo_CommitFailed_Retryable) 6 分支事务二阶段提交失败重试
分支事务二阶段提交失败不重试(PhaseTwo_CommitFailed_Unretryable) 7 分支事务二阶段提交失败不重试
分支事务二阶段已回滚(PhaseTwo_Rollbacked) 8 分支事务二阶段已回滚
分支事务二阶段回滚失败重试(PhaseTwo_RollbackFailed_Retryable) 9 分支事务二阶段回滚失败重试
分支事务二阶段回滚失败不重试(PhaseTwo_RollbackFailed_Unretryable) 10 二阶段提交失败
未知状态(UnKnown) 0 未知状态

4.3 seata-server定时任务

4.3.1 重试回滚超时的全局事务

  • 覆盖的全局事务对象:
    • 全局事务回滚中(Rollbacking) 4 正在重新回滚全局事务
    • 全局事务回滚重试中(RollbackRetrying) 5 在全局回滚异常被解决后尝试事务重试回滚中
    • 全局事务超时回滚中(TimeoutRollbacking) 6 全局事务超时回滚中
    • 全局事务超时回滚重试中(TimeoutRollbackRetrying) 7 全局事务超时回滚重试中
  • 间隔:server.recovery.rollbackingRetryPeriod=1000ms
  • 执行条件:
    • 超时:now - beginTime > timeout
    • 最大重试次数:server.maxRollbackRetryTimeout,-1为一直重试
  • 执行:doGlobalRollback()

4.3.2 重试提交超时的全局事务

  • 覆盖的全局事务对象:
    • 全局事务提交中(Committing) 2 这个状态会随时改变
    • 全局事务提交重试(CommitRetry) 3 在提交异常被解决后尝试重试提交
  • 间隔:server.recovery.committingRetryPeriod=1000ms
  • 执行条件:
    • 超时:now - beginTime > timeout
    • 最大重试次数:server.maxCommitRetryTimeout ,-1为一直重试
  • 执行:doGlobalCommit()

4.3.3 提交全局事务的全局事务

  • 覆盖的全局事务对象:
    • 异步提交中(AsyncCommitting) 8 异步提交中
  • 间隔:server.recovery.asynCommittingRetryPeriod=1000ms
  • 执行:doGlobalCommit()

4.3.4 检查事务超时的全局事务

  • 覆盖的全局事务对象:

    • 未知状态(UnKnown) 0 未知状态
    • 全局事务开始(Begin) 1 此状态可以接受新的分支事务注册
    • 全局事务提交中(Committing) 2 这个状态会随时改变
    • 全局事务提交重试(CommitRetry) 3 在提交异常被解决后尝试重试提交
    • 全局事务回滚中(Rollbacking) 4 正在重新回滚全局事务
    • 全局事务回滚重试中(RollbackRetrying) 5 在全局回滚异常被解决后尝试事务重试回滚中
    • 全局事务超时回滚中(TimeoutRollbacking) 6 全局事务超时回滚中
    • 全局事务超时回滚重试中(TimeoutRollbackRetrying) 7 全局事务超时回滚重试中
    • 异步提交中(AsyncCommitting) 8 异步提交中
  • 间隔:server.recovery.asynCommittingRetryPeriod=1000ms

  • 执行:timeout是@GlobalTransactional注解元数据,默认6000ms 将事务状态设置为TimeoutRollbacking,并加入到RETRY_ROLLBACKING_SESSION_MANAGER回滚队列

4.3.5 清理undo_log的定时任务

  • 间隔:server.undo.logDeletePeriod undo清理线程间隔时间 默认86400000,单位毫秒
  • 执行条件:server.undo.logSaveDays undo保留天数 默认7天,超过该值则清理
  • 执行:清理undo_log,log_status=1和未正常清理的undo

5.SEATA注册RM、TM

5.1 APP启动分析

在spring容器启动时,会扫描到自动配置SeataAutoConfiguration.class,从而初始化GlobalTransactionScanner.class

//GlobalTransactionScanner.java
private void initClient() {
        if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Initializing Global Transaction Clients ... ");
        }
        if (StringUtils.isNullOrEmpty(applicationId) || StringUtils.isNullOrEmpty(txServiceGroup)) {
        throw new IllegalArgumentException(String.format("applicationId: %s, txServiceGroup: %s", applicationId, txServiceGroup));
        }
        //init TM
        TMClient.init(applicationId, txServiceGroup, accessKey, secretKey);
        if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Transaction Manager Client is initialized. applicationId[{}] txServiceGroup[{}]", applicationId, txServiceGroup);
        }
        //init RM
        RMClient.init(applicationId, txServiceGroup);
        if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Resource Manager is initialized. applicationId[{}] txServiceGroup[{}]", applicationId, txServiceGroup);
        }

        if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Global Transaction Clients are initialized. ");
        }
        registerSpringShutdownHook();

        }
复制代码

由上面源码可以看到,每个APP即是TM又是RM。

5.2 TM注册

下图是TM注册的时序图,在注册时会将applicationIdtransactionServiceGroup发送给TC。TC会将会话保存至Map中,用于后续保持心跳。

5.3 RM注册

RM注册时会将resourceIdsapplicationIdtransactionServiceGroup发送给TC,TC会将会话保存,后续的对应RM通知时将会话取出。

特别说明,resourceIds是资源ID集合

  • 在AT模式下其等于数据库实例
  • 在TCC模式下其等于@TwoPhaseBusinessAction(name = "DubboTccActionTwo")中name的值

6.总结

本章主要介绍了Seata由来,同时使用了zk+apollo启动了Seata-Server和Seata-client。在TC端,介绍了在Seata是如何定义全局事务和分支事务,并且通过几个定时任务来推动这些事务状态变更。在客户端处,简单介绍了启动时如何将自身注册到TC上。在后面的文章中,将介绍如何使用Seata中的各个模式,以及需要注意的事项。

参考:seata.io/zh-cn/docs/…

推荐阅读

Guava Cache实战—从场景使用到原理分析

详解 HTTP2.0 及 HTTPS 协议

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

image.png

猜你喜欢

转载自juejin.im/post/7049876367143337991