分布式事务之超详细的Seata实践记录

前两周花了些时间在研究tcc分布式事务的一些相关基础上边,这周来写一篇关于seata的实践文章。
网上关于seata落地的demo其实也蛮多的,自己在结合案例和相关文章进行实际落地的过程中踩了不少坑,所以这篇文章主要记录关于落地案例中遇到的困难。

技术选型

SpringBoot + Dubbo + JdbcTemplate + MySQL + Seata + Nacos

使用场景

购买商品的时候,扣减库存并且同时插入一条订单数据。
ps:简单的模拟场景,没有做锁定库存相关的复杂操作,只是为了验证seata能够保证多库场景下的分布式事务能够生效

分布式事务之超详细的Seata实践记录_第1张图片

搭建seata单机部署

首先保证搭建好nacos环境和mysql环境,关于这部分的介绍本文不做过多讲解。

mysql建表
搭建seata使用的数据库:seata
然后在seata数据库中建立三张表:

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

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


CREATE TABLE `lock_table` (
  `row_key` varchar(128) COLLATE utf8_bin NOT NULL,
  `xid` varchar(96) COLLATE utf8_bin DEFAULT NULL,
  `transaction_id` mediumtext COLLATE utf8_bin,
  `branch_id` mediumtext COLLATE utf8_bin,
  `resource_id` varchar(256) COLLATE utf8_bin DEFAULT NULL,
  `table_name` varchar(32) COLLATE utf8_bin DEFAULT NULL,
  `pk` varchar(36) COLLATE utf8_bin DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

分布式事务之超详细的Seata实践记录_第2张图片

然后是分别搭建订单数据库和商品数据库mall_ordermall_goods

mall_order数据库中创建一张简单的订单表

CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(60) COLLATE utf8_bin DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `goods_id` int(11) DEFAULT NULL,
  `stock` int(6) DEFAULT NULL COMMENT '库存',
  `unit` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '单位',
  `status` tinyint(3) DEFAULT NULL COMMENT '订单状态 0 提交中,1提交成功,2提交失败',
  `valid_status` tinyint(3) DEFAULT NULL COMMENT '订单是否有效 0有效 2无效',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='订单表';

mall_order库中建立一张undo_log

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

mall_goods数据库中创建一张简单的商品表

CREATE TABLE `t_goods` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(30) COLLATE utf8_bin DEFAULT NULL,
  `stock` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='商品信息表';

mall_goods库中建立一张undo_log

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

两张undo_log是为了seata专门使用的,和业务核心流程无太多关联,这里我们暂时不用管,下边我会介绍。

接下来是seata的安装
初次学习seata,选择的是搭建单机环境进行部署。
在官方地址下载seata:
https://github.com/seata/seata/releases
本人使用的是1.1.0版本安装包,安装环境是mac pro
https://github.com/seata/seata/releases/tag/v1.1.0

分布式事务之超详细的Seata实践记录_第3张图片

安装包结构介绍
下载之后解压会看到以下文件:
分布式事务之超详细的Seata实践记录_第4张图片
bin 目录底下是主要的启动文件
conf 底下是相关的配置文件
config.txt 是我后边加入的一份配置文件,这里读者门可以先忽略
lib 是存放一些依赖包的目录

配置文件调整
按照官网的指导,我进入到来conf目录底下然后打开相关的配置文件:
/conf/register.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  # seata的注册中心这里选择使用nacos
  type = "nacos"

  
  nacos {
    #注册地址
    serverAddr = "localhost:8848"
    #默认写空
    namespace = ""
    #单机版搭建这里写default就可以了
    cluster = "default"


  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  # 配置模块的type也要写nacos
  type = "nacos"

  #同理,这里也这么配置即可
  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

这份配置文件主要改动点为config模块选择使用nacos,register模块也选择使用nacos,并且均需要设置相关的ip地址等属性。

然后我们来看到一份config.txt文件,如果没有就新建该文件。
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_tcc=default #默认格式为service.vgroupMapping加一个自定义的事务分组名称,这里我取名为my_tcc,然后赋值为default

service.default.grouplist=127.0.0.1:8091 #这里写nacos的配置地址
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
client.support.spring.datasource.autoproxy=true 
store.mode=db #这里默认写的是file模式,现在需要改为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=dbcp #这里默认写的是使用druid数据连接池,这里我改为使用dbcp
store.db.dbType=mysql 
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://prod.min.mall.com:3306/seata?useUnicode=true
store.db.user=youruser #数据库账号名
store.db.password=yourpassword #数据库密码
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

ps:配置文件最底部有个无用的空行,部分网友说不删除会有异常,建议删除。

将相关的seata的config.txt配置导入到nacos中
这部分操作在网上搜了很多资料,似乎都是使用以下脚本进行同步数据,这里我把脚本的内容贴出来给到各位查阅:

#!/usr/bin/env bash
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at、
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


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}" "http://$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 $(dirname "$PWD")/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.txt文件拷贝到和bin目录同级别的位置才可以:
图片: https://uploader.shimo.im/f/cIpElXNIdT4GjbE8.png

这里也就应对了文章开头我提及的config.txt文件为何会出现的情况了。
执行脚本

sh nacos-config.sh localhost

导入成功之后查看nacos界面

分布式事务之超详细的Seata实践记录_第5张图片

然后此时再启动seata-server

简单的启动脚本:

echo "======== prepare to start seata ======="
nohup sh ./seata-server.sh &
echo "======== now, it is starting ======="

启动成功会看到日志中输出这么一段话:

2020-11-07 15:31:53.116 INFO [main]io.seata.core.rpc.netty.RpcServerBootstrap.start:155 -Server started ... 
2020-11-07 15:34:43.032 INFO [UndoLogDelete_1]io.seata.server.coordinator.DefaultCoordinator.undoLogDelete:359 -no active rm channels to delete undo log

并且nacos中有相关的seata服务注册
在这里插入图片描述

接下来便进入了java程序编码接入的环节了。
项目完整代码案例:

maven依赖配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo-service-tcc</artifactId>
        <groupId>org.idea</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo-service-pay</artifactId>

    <properties>
        <mysql.version>5.1.26</mysql.version>
        <druid.version>1.1.10</druid.version>
        <seata.version>1.1.0</seata.version>
        <nacos.client.version>1.1.3</nacos.client.version>
        <nacos.springboot.starter.version>0.2.1</nacos.springboot.starter.version>
        <dubbo.service.interface.version>1.0-SNAPSHOT</dubbo.service.interface.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>${seata.version}</version>
        </dependency>

        <dependency>
            <groupId>org.idea</groupId>
            <artifactId>dubbo-service-interface</artifactId>
            <version>${dubbo.service.interface.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- Dubbo Registry Nacos -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-nacos</artifactId>
            <version>${dubbo.version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>${nacos.client.version}</version>
        </dependency>

        <!-- 1. nacos-如果希望注入一些功能则需要使用starter -->
        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>nacos-config-spring-boot-starter</artifactId>
            <version>${nacos.springboot.starter.version}</version>
        </dependency>
        <!-- 2. nacos-服务发现功能依赖 -->
        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>nacos-discovery-spring-boot-starter</artifactId>
            <version>${nacos.springboot.starter.version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

    </dependencies>

</project>
配置文件

配置数据源application.propertie文件

server.port=8081
spring.application.name=seata-demo
# goods
spring.datasource.goods.jdbc-url=jdbc:mysql://prod.min.mall.com:3306/mall-goods?useUnicode=true&characterEncoding=utf8
spring.datasource.goods.username=root
spring.datasource.goods.password=root
spring.datasource.goods.driver-class-name=com.mysql.jdbc.Driver
# order
spring.datasource.order.jdbc-url=jdbc:mysql://prod.min.mall.com:3306/mall-order?useUnicode=true&characterEncoding=utf8
spring.datasource.order.username=root
spring.datasource.order.password=root
spring.datasource.order.driver-class-name=com.mysql.jdbc.Driver

关于seata的配置主要写在了application.yml中:

seata:
  enabled: true
  application-id: tcc-seata-service
  tx-service-group: SEATA_GROUP # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
  client:
    rm-report-success-enable: true
    rm-table-meta-check-enable: false # 自动刷新缓存中的表结构(默认false)
    rm-report-retry-count: 5 # 一阶段结果上报TC重试次数(默认5)
    rm-async-commit-buffer-limit: 10000 # 异步提交缓存队列长度(默认10000)
    rm:
      lock:
        lock-retry-internal: 10 # 校验或占用全局锁重试间隔(默认10ms)
        lock-retry-times:    30 # 校验或占用全局锁重试次数(默认30)
        lock-retry-policy-branch-rollback-on-conflict: true # 分支事务与其它全局回滚事务冲突时锁策略(优先释放本地锁让回滚成功)
    tm-commit-retry-count:   3 # 一阶段全局提交结果上报TC重试次数(默认1次,建议大于1)
    tm-rollback-retry-count: 3 # 一阶段全局回滚结果上报TC重试次数(默认1次,建议大于1)
    undo:
      undo-data-validation: true # 二阶段回滚镜像校验(默认true开启)
      undo-log-serialization: jackson # undo序列化方式(默认jackson)
      undo-log-table: undo_log  # 自定义undo表名(默认undo_log)
    log:
      exceptionRate: 100 # 日志异常输出概率(默认100)
    support:
      spring:
        datasource-autoproxy: true
  service:
    vgroup-mapping:
              my_tcc: default # TC 集群(必须与seata-server保持一致)
    enable-degrade: false # 降级开关
    disable-global-transaction: false # 禁用全局事务(默认false)
    grouplist:
       default: 127.0.0.1:8091
  transport:
    shutdown:
      wait: 3
    thread-factory:
      boss-thread-prefix: NettyBoss
      worker-thread-prefix: NettyServerNIOWorker
      server-executor-thread-prefix: NettyServerBizHandler
      share-boss-worker: false
      client-selector-thread-prefix: NettyClientSelector
      client-selector-thread-size: 1
      client-worker-thread-prefix: NettyClientWorkerThread
    type: TCP
    server: NIO
    heartbeat: true
    serialization: seata
    compressor: none
    enable-client-batch-send-request: true # 客户端事务消息请求是否批量合并发送(默认true)
  registry:
    file:
      name: file.conf
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace:
      cluster: default
  config:
    file:
      name: file.conf
    type: nacos
    nacos:
      namespace:
      server-addr: localhost:8848
这里面有几个坑的地方,建议下边的配置模块要和seata的server端配置相同,否则会出现错误。
 service:
    vgroup-mapping:
              my_tcc: default # TC 集群(必须与seata-server保持一致)
       grouplist:
       default: 127.0.0.1:8091           

seata:       
     tx-service-group: SEATA_GROUP #这个属性后边我会讲解

dubbo.properties配置文件

dubbo.application.id=dubbo-service
dubbo.application.name=dubbo-service
dubbo.registry.address=nacos://localhost:8848
dubbo.provider.threads=10
dubbo.provider.threadpool=fixed
dubbo.provider.loadbalance=roundrobin
dubbo.server=true
dubbo.protocol.name=dubbo
dubbo.protocol.port=9091
dubbo.protocol.threadpool=fixed
#dubbo.protocol.dispatcher=execution
dubbo.protocol.threads=100
dubbo.protocol.accepts=100
dubbo.protocol.queues=100
整体项目结构:

分布式事务之超详细的Seata实践记录_第6张图片

代码模块

相关的数据源配置

package org.idea.service.pay.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

/**
 * @author idea
 * @date created in 6:12 下午 2020/11/14
 */
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.order")
    public DataSource orderDataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.goods")
    public DataSource goodsDataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "orderJdbcTemplate")
    public JdbcTemplate orderJdbcTemplate(@Qualifier("orderDataSource")DataSource orderDataSource){
        return new JdbcTemplate(orderDataSource);
    }

    @Bean(name = "goodsJdbcTemplate")
    public JdbcTemplate goodsJdbcTemplate(@Qualifier("goodsDataSource")DataSource goodsDataSource){
        return new JdbcTemplate(goodsDataSource);
    }
}

dao层

package org.idea.service.pay.dao;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;

/**
 * @author idea
 * @date created in 6:17 下午 2020/11/14
 */
@Repository
public class GoodsDao {

    @Resource
    private JdbcTemplate goodsJdbcTemplate;

    public boolean updateStock(int id,int stock){
        String sql = "update t_goods set stock=stock-? where id=?";
        int result = goodsJdbcTemplate.update(sql,new Object[]{stock,id});
        return result>0;
    }


}
package org.idea.service.pay.dao;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.UUID;

/**
 * @author idea
 * @date created in 6:17 下午 2020/11/14
 */
@Repository
public class OrderDao {

    @Resource
    private JdbcTemplate orderJdbcTemplate;

    public boolean insertOrder(){
        int i=0/0; 
        String orderNo = UUID.randomUUID().toString();
        String sql = "INSERT INTO `t_order`( `order_no`, `user_id`, `goods_id`, `stock`, `unit`, `status`, `valid_status`, `create_time`, `update_time`) VALUES ( '" +
                orderNo+"', 1, 1, 1, '件', 1, 1, NOW(), NOW())";
        orderJdbcTemplate.execute(sql);
        return true;
    }
}

在insertOrder方法中我特意写了一段异常,为后续测试seata使用。

service层

package org.idea.service.pay.service;

import io.seata.spring.annotation.GlobalTransactional;
import org.apache.dubbo.config.annotation.Service;
import org.idea.interfaces.pay.ITccService;
import org.idea.service.pay.dao.GoodsDao;
import org.idea.service.pay.dao.OrderDao;

import javax.annotation.Resource;

/**
 * @author idea
 * @date created in 7:41 下午 2020/11/14
 */
@Service(interfaceName = "iTccService")
public class ITccServiceImpl implements ITccService {

    @Resource
    private GoodsDao goodsDao;
    @Resource
    private OrderDao orderDao;


    @GlobalTransactional(timeoutMills = 300000,name = "tcc-seata-service-group")
    @Override
    public void doTcc(){
        System.out.println("====== 开始执行事务 ====== ");
        goodsDao.updateStock(1,1);
        orderDao.insertOrder();
        System.out.println("====== 执行事务结束 ====== ");
    }
}

注意这里代码中写的 @GlobalTransactional注解是seata用于捕获分布式事务的关键点,
可以看到我这里写入的name是:tcc-seata-service-group,这块可以自定义不影响。

启动类

package org.idea.service.pay;

import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.idea.interfaces.pay.ITccService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author idea
 * @date created in 6:00 下午 2020/11/14
 */
@SpringBootApplication
@EnableDubbo
@RestController
public class Application {

    @Reference
    private ITccService iTccService;

    @GetMapping(value = "tcc")
    public String doTcc(){
        iTccService.doTcc();
        return "success";
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
        System.out.println("Application tcc demo");
    }
}

启动程序,我们可以看到日志中会有相关的seata信息日志打印:
分布式事务之超详细的Seata实践记录_第7张图片

seata的分布式事务验证

异常验证

请求接口http://localhost:8081/tcc
在创建订单的环节中,出现异常,seata进行了回滚处理,此时数据库库存保持和一开始一致。订单库也没有新增数据。
分布式事务之超详细的Seata实践记录_第8张图片

正常流程验证
将异常代码去除,重新请求:
图片: https://uploader.shimo.im/f/ni70tKjfCjz9Av7T.png

此时日志打印正常,seata的环境基本搭建成功!

可能出现的异常

io.seata.common.exception.FrameworkException: No available service

不知道各位读者在最后的接口验证环境是不是会和我一样遇到类似的异常,这段异常如下所示:
分布式事务之超详细的Seata实践记录_第9张图片

网上搜索了相关资料,比较少看到讲解这块的原因,我只能硬着头皮去源码debug分析。
分布式事务之超详细的Seata实践记录_第10张图片

结合自己以前对于nacos源码的理解经验,快速地定位到了问题点:

io.seata.config.nacos.NacosConfiguration#getConfig

分布式事务之超详细的Seata实践记录_第11张图片

此处的查询dataId为service.vgroupMapping.SEATA_GROUP,group值为SEATA_GROUP,然后到nacos中一查询,发现此配置不存在,于是手动添加:

其实这里到dataId尾部的SEATA_GREOUP名称就是我们文章上边提及的application.yml中配置的一项参数。

知道对应的dataid,group,猜测相关的value应该要和yml一致,于是我便配置了default。
分布式事务之超详细的Seata实践记录_第12张图片

结果控制台立马发生响应变化:
图片: https://uploader.shimo.im/f/FK9U7aYUbFWHdN3T.png

此时再重试接口,就恢复正常了。

小结

之前在工作中用得比较多的还是借助消息队列来实现分布式环境下的事务最终一致性,对于seata这款技术框架的原理还有很多细节不是很熟悉。
整理梳理下来,感觉seata的入门要比nacos,dubbo那些中间件高一些,配置复杂,细节繁琐。后边看了官网的介绍,这里还只是其中的at模型,对于更加多更加复杂的其他事务模型还有待继续学习。

你可能感兴趣的:(中间件,java,数据库)