分散式事務 SEATA-1.4.1 AT模式 配合NACOS 應用

SEATA 配置

使用 nacos 做為配置中心配置 SEATA

當前 SEATA 版本: 1.4.1

TC (Transaction Coordinator) – 事務協調者

維護全局和分支事務的狀態,驅動全局事務提交或回滾。

配置參數

參數文檔

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.app-server-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=url
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=10
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

nacos bash 腳本

參考 SEATA github 配置說明

nacos-config.sh

while getopts ":h:p:g:t:u:w:" opt
do
  case $opt in
  h)
    host=$OPTARG
    ;;
  p)
    port=$OPTARG
    ;;
  g)
    group=$OPTARG
    ;;
  t)
    tenant=$OPTARG
    ;;
  u)
    username=$OPTARG
    ;;
  w)
    password=$OPTARG
    ;;
  ?)
    echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] "
    exit 1
    ;;
  esac
done

if [[ -z ${host} ]]; then
    host=localhost
fi
if [[ -z ${port} ]]; then
    port=8848
fi
if [[ -z ${group} ]]; then
    group="SEATA_GROUP"
fi
if [[ -z ${tenant} ]]; then
    tenant=""
fi
if [[ -z ${username} ]]; then
    username=""
fi
if [[ -z ${password} ]]; then
    password=""
fi

nacosAddr=$host:$port
contentType="content-type:application/json;charset=UTF-8"

echo "set nacosAddr=$nacosAddr"
echo "set group=$group"

failCount=0
tempLog=$(mktemp -u)
function addConfig() {
  curl -X POST -H "${contentType}" "//$nacosAddr/nacos/v1/cs/configs?dataId=$1&group=$group&content=$2&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null
  if [[ -z $(cat "${tempLog}") ]]; then
    echo " Please check the cluster status. "
    exit 1
  fi
  if [[ $(cat "${tempLog}") =~ "true" ]]; then
    echo "Set $1=$2 successfully "
  else
    echo "Set $1=$2 failure "
    (( failCount++ ))
  fi
}

count=0
for line in $(cat config.txt | sed s/[[:space:]]//g); do
  (( count++ ))
	key=${line%%=*}
    value=${line#*=}
	addConfig "${key}" "${value}"
done

echo "========================================================================="
echo " Complete initialization parameters,  total-count:$count ,  failure-count:$failCount "
echo "========================================================================="

if [[ ${failCount} -eq 0 ]]; then
	echo " Init nacos config finished, please start seata-server. "
else
	echo " init nacos config fail. "
fi

同步 config 配置到 nacos

進入 TC 伺服器

  • 新建文件夾 seata-config

  • 進入 seata-config

  • 新建 config.txt 文件並複製配置參數到 config.txt 文件中

  • 新建 nacos-config.sh 文件,同時複製 nacos bash 腳本到 nacos-config.sh 中

  • 使用以下命令同步配置參數到 nacos

    bash nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t 3a2aea46-07c6-4e21-9a1e-8946cde9e2b3 -u nacos -w nacos
    

    得到輸出

    set nacosAddr=127.0.0.1:8848
    set group=SEATA_GROUP
    Set transport.type=TCP successfully 
    Set transport.server=NIO successfully 
    .
    .
    .
    =========================================================================
    Complete initialization parameters,  total-count:80 ,  failure-count:0 
    =========================================================================
    Init nacos config finished, please start seata-server. 
    

使用 docker 部署 SEATA

Docker 部署 SEATA 官方文檔

進入 TC 伺服器,並進入 seat-config 文件夾

  • 新建 registry.conf 文件,並添加以下內容,registry 配置參考

    registry.conf
    registry {
    # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
    type = "nacos"
    
    nacos {
        application = "seata-server"
        group = "SEATA_GROUP"
        serverAddr = "127.0.0.1"
        namespace = "3a2aea46-07c6-4e21-9a1e-8946cde9e2b3"
        cluster = "default"
    }
    }
    
    config {
    # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
    type = "nacos"
    
    nacos {
        serverAddr = "127.0.0.1"
        namespace = "3a2aea46-07c6-4e21-9a1e-8946cde9e2b3"
        group = "SEATA_GROUP"
        username = "nacos"
        password = "nacos"
    }
    }
    
  • 新建 file.conf 文件並添加以下內容(可選,可通過 nacos 讀取)

    file.conf
    transport {
    # tcp udt unix-domain-socket
    type = "TCP"
    #NIO NATIVE
    server = "NIO"
    #enable heartbeat
    heartbeat = true
    # the client batch send request enable
    enableClientBatchSendRequest = true
    #thread factory for netty
    threadFactory {
        bossThreadPrefix = "NettyBoss"
        workerThreadPrefix = "NettyServerNIOWorker"
        serverExecutorThread-prefix = "NettyServerBizHandler"
        shareBossWorker = false
        clientSelectorThreadPrefix = "NettyClientSelector"
        clientSelectorThreadSize = 1
        clientWorkerThreadPrefix = "NettyClientWorkerThread"
        # netty boss thread size,will not be used for UDT
        bossThreadSize = 1
        #auto default pin or 8
        workerThreadSize = "default"
    }
    shutdown {
        # when destroy server, wait seconds
        wait = 3
    }
    serialization = "seata"
    compressor = "none"
    }
    service {
    #transaction service group mapping
    vgroupMapping.my_test_tx_group = "default"
    #only support when registry.type=file, please don't set multiple addresses
    default.grouplist = "127.0.0.1:8091"
    #degrade, current not support
    enableDegrade = false
    #disable seata
    disableGlobalTransaction = false
    }
    
    client {
    rm {
        asyncCommitBufferLimit = 10000
        lock {
        retryInterval = 10
        retryTimes = 30
        retryPolicyBranchRollbackOnConflict = true
        }
        reportRetryCount = 5
        tableMetaCheckEnable = false
        reportSuccessEnable = false
    }
    tm {
        commitRetryCount = 5
        rollbackRetryCount = 5
    }
    undo {
        dataValidation = true
        logSerialization = "jackson"
        logTable = "undo_log"
    }
    log {
        exceptionRate = 100
    }
    }
    
  • 運行 docker 命令

    • 注意: 當在 config.txt 中配置 store.mode=db 時,需要在配置的資料庫連接中初始化表 global_tablebranch_tablelock_tablesql 傳送門
    docker run -d --name seata-server \
          --net=host \
          -p 8091:8091 \
          -e SEATA_CONFIG_NAME=file:/root/seata-config/registry \
          -v /root/seata-config:/root/seata-config  \
          seataio/seata-server:1.4.1
    

    掛載目錄為 TC 伺服器配置目錄。

TM (Transaction Manager) – 事務管理器

定義全局事務的範圍:開始全局事務、提交或回滾全局事務。

例子:業務聚合服務

  • SEATA 包引入。pom 配置如下,當使用 spring-cloud-starter-openfeign 包時,需要移除 spring-cloud-starter-openfeign 包,spring-cloud-starter-alibaba-seata 中已經包含了 spring-cloud-starter-openfeign ,再次引入可能導致包衝突。

    pom.xml
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.4.1</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>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
  • 添加 registry.conf。在工程中 resource 目錄下添加如下內容

    registry.conf
    registry {
    # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
    type = "nacos"
    
    nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        namespace = "3a2aea46-07c6-4e21-9a1e-8946cde9e2b3"
        cluster = "default"
        username = "nacos"
        password = "nacos"
    }
    }
    
    config {
    # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
    type = "nacos"
    
    nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = "3a2aea46-07c6-4e21-9a1e-8946cde9e2b3"
        group = "SEATA_GROUP"
        username = "nacos"
        password = "nacos"
    }
    }
    
    
  • 配置 bootstrap.properties,添加配置,內容如下,seata.tx-service-groupnamespace 改為對應的值

    bootstrap.properties
      ...
      seata.tx-service-group=app-server-tx-group
      seata.config.type=nacos
      seata.config.nacos.server-addr=127.0.0.1:8848
      seata.config.nacos.namespace=3a2aea46-07c6-4e21-9a1e-8946cde9e2b3
      seata.config.nacos.group=SEATA_GROUP
    

在 TM 中通過 @GlobalTransactional 開啟全局異常,示例程式碼:

 
    @GlobalTransactional
    @GetMapping({"create"})
    public String create(String name,Integer age) {
        ...
        return "創建成功";
    }

RM (Resource Manager) – 資源管理器

管理分支事務處理的資源,與TC交談以註冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。

例子:被調用服務。

  • 配置 bootstrap.properties,添加配置,內容如下,seata.tx-service-groupnamespace 改為對應的值

    bootstrap.properties
      ...
      seata.tx-service-group=app-server-tx-group
      seata.config.type=nacos
      seata.config.nacos.server-addr=127.0.0.1:8848
      seata.config.nacos.namespace=3a2aea46-07c6-4e21-9a1e-8946cde9e2b3
      seata.config.nacos.group=SEATA_GROUP
    
  • 對需要做回滾的業務標記 @Transactional(rollbackFor = Exception.class)

  • 如果配置了全局異常處理,使用 SEATA API 發起事務回滾

      @ExceptionHandler(value = Exception.class)
      @ResponseBody
      public String exceptionHandler(Exception e) {
          ...
          try {
              String xid = RootContext.getXID();
              if (StringUtils.isNotEmpty(xid)) {
                  GlobalTransactionContext.reload(RootContext.getXID()).rollback();
              }
          } catch (TransactionException transactionException) {
              transactionException.printStackTrace();
              log.error("===TransactionException==={}", transactionException.getMessage());
          }
          return e.getMessage();
      }
    

    或者通過 AOP 全局處理回滾

    
    /**
    * @author Zhang_Xiang
    * @since 2021/2/22 17:36:16
    */
    @Aspect
    @Component
    @Slf4j
    public class TxAspect {
    
        @Pointcut("execution(public * *(..))")
        public void publicMethod() {
        }
    
        @Pointcut("within(com.*.service.impl..*)")
        private void services() {
        }
    
        @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
        private void transactional() {
        }
    
        @Pointcut("within(com.*.webapi.controller..*)")
        private void actions() {
        }
    
        @Pointcut("@annotation(org.springframework.web.bind.annotation.ExceptionHandler))")
        private void validatedException() {
        }
    
        @Before(value = "validatedException()")
        public void beforeValidate(JoinPoint joinPoint) throws TransactionException {
            Object[] args = joinPoint.getArgs();
            if (args == null || args.length == 0) {
                return;
            }
            Exception e = (Exception) args[0];
            if (e instanceof MethodArgumentNotValidException || e instanceof BindException || e instanceof
                    ConstraintViolationException) {
                globalRollback();
            }
        }
    
        @AfterThrowing(throwing = "e", pointcut = "publicMethod()&&services()&&transactional()")
        public void doRecoveryMethods(Throwable e) throws TransactionException {
            log.info("===method throw===:{}", e.getMessage());
            globalRollback();
        }
    
        @AfterReturning(value = "publicMethod()&&actions()", returning = "result")
        public void afterReturning(RestResponse<?> result) throws TransactionException {
            log.info("===method finished===:{}", result);
            if (result.isFail()) {
                globalRollback();
            }
        }
    
        //region private methods
    
        private void globalRollback() throws TransactionException {
            if (!StringUtils.isBlank(RootContext.getXID())) {
                log.info("===xid===:{}", RootContext.getXID());
                GlobalTransactionContext.reload(RootContext.getXID()).rollback();
            }
        }
    
        //endregion
    }