前两周花了些时间在研究tcc分布式事务的一些相关基础上边,这周来写一篇关于seata的实践文章。
网上关于seata落地的demo其实也蛮多的,自己在结合案例和相关文章进行实际落地的过程中踩了不少坑,所以这篇文章主要记录关于落地案例中遇到的困难。
技术选型
SpringBoot + Dubbo + JdbcTemplate + MySQL + Seata + Nacos
使用场景
购买商品的时候,扣减库存并且同时插入一条订单数据。
ps:简单的模拟场景,没有做锁定库存相关的复杂操作,只是为了验证seata能够保证多库场景下的分布式事务能够生效
搭建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; 复制代码
然后是分别搭建订单数据库和商品数据库mall_order和mall_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:
本人使用的是1.1.0版本安装包,安装环境是mac pro
安装包结构介绍
下载之后解压会看到以下文件:
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目录同级别的位置才可以:
这里也就应对了文章开头我提及的config.txt文件为何会出现的情况了。
执行脚本
sh nacos-config.sh localhost 复制代码
导入成功之后查看nacos界面
然后此时再启动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程序编码接入的环节了。
项目完整代码案例: