告别造轮子,分布式事务开箱即用!

152 阅读6分钟

seata是什么

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

docker部署seata

此处使用1.5.x版本的seata

安装准备

#先安装临时容器
docker run -d -p 8091:8091 -p 7091:7091  --name seata-serve seataio/seata-server:1.5.0
#拷贝文件为真正安装做准备
docker cp seata-serve:/seata-server/resources /home/mycontainers/seata/config

首先把镜像拉下来进行部署,然后把seata文件拷贝出来,目的就是为了启动seata时挂载到宿主机方便配置。 结构目录如下:

image.png

修改配置文件

根据你的项目环境进行配置application.yml,此处是将seata注册到nacos,其他方法可查看官方文档。

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 你的nacos主机
      namespace: 你的nacos命名空间
      group: SEATA_GROUP
      username: nacos账号
      password: nacos密码
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 你的nacos主机
      group: SEATA_GROUP
      namespace: 你的nacos命名空间
      cluster: default
      username: nacos账号
      password: nacos密码
  store:
    # support: file 、 db 、 redis
    mode: file
    # db:
    #   datasource: druid
    #   db-type: mysql
    #   driver-class-name: com.mysql.cj.jdbc.Driver
    #   url: jdbc:mysql://mysql:3306/seata?rewriteBatchedStatements=true
    #   user: root
    #   password: 123456
    #   min-conn: 5
    #   max-conn: 100
    #   global-table: global_table
    #   branch-table: branch_table
    #   lock-table: lock_table
    #   distributed-lock-table: distributed_lock
    #   query-limit: 100
    #   max-wait: 5000
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: 此处默认
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

重新启动seata

你可以删除容器或者使用cp命令复制到容器里面进行启动。此处为了安全起见删除原本的容器,重新创建一个并且挂载。

运行docker命令,进行seata启动。

docker run -d
--name seata
-e SEATA_IP=你的宿主机局域网ip
-p 8091:8091 -p 7091:7091
--restart=always
--network mynetwork
-v /home/mycontainers/seata/config/resources:/seata-server/resources
seataio/seata-server:1.5.0

这里挂载到resources文件夹,笔者尝试过挂载到config文件夹,启动失败,但官方文档是这样的,大家注意一下挂在路径。

image.png

注意,如果不指定-e SEATA_IP=xxxxx 的情况下,默认是使用docker的网络ip,比如172.20.0.xx,项目启动的时候会连接不上,报找不到服务的错误,请确保你的局域网IP能准确访问。nacos访问ip如下

image.png

注册seata配置到nacos

访问github,拿到官方提供的config.txt文件 github.com/seata/seata…

然后放到挂载目录的config目录下

image.png

如果有网络不好的朋友,可以直接复制以下的txt
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
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
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
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=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
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
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=file
store.lock.mode=file
store.session.mode=file
#Used for password encryption
store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
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

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
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&rewriteBatchedStatements=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100

#Transaction rule configuration, only for the server
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.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

运行官方提供的脚本,将配置推送到nacos配置中心,脚本链接地址 github.com/seata/seata… 又或者你可以直接复制

#!/bin/sh
# 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"

urlencode() {
  length="${#1}"
  i=0
  while [ $length -gt $i ]; do
    char="${1:$i:1}"
    case $char in
    [a-zA-Z0-9.~_-]) printf $char ;;
    *) printf '%%%02X' "'$char" ;;
    esac
    i=`expr $i + 1`
  done
}

failCount=0
tempLog=$(mktemp -u)
function addConfig() {
  dataId=`urlencode $1`
  content=`urlencode $2`
  curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$dataId&group=$group&content=$content&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=`expr $failCount + 1`
  fi
}

count=0
COMMENT_START="#"
for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do
    if [[ "$line" =~ ^"${COMMENT_START}".*  ]]; then
      continue
    fi
    count=`expr $count + 1`
	  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

使用命令

sh nacos-config.sh -h nacos主机host  -p nacos端口 -g SEATA_GROUP -t nacos命名空间 -u 账号 -w 密码

请根据你的实际情况进行修改

命令运行完成后nacos出现相关配置即表明配置成功

image.png

再看一下nacos是否已经注册到nacos

image.png

seata部署完毕,可参考seata官方的文档进行调试和定制seata

seata.io/zh-cn/docs/…

spring配置seata

核心依赖

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-seata -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2021.1</version>
</dependency>

创建undo_log表

SEATA AT 模式需要 UNDO_LOG 表

-- 注意此处0.3.0+ 增加唯一索引 ux_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=1 DEFAULT CHARSET=utf8;

feign模块

@Component
@FeignClient(name = "order",fallbackFactory = Fallback.class)
public interface orderFeign {
    @PostMapping("/orderc/create")
    public ResponData createOrder(@RequestBody OrderDto orderDto);
}

商品服务

配置

server:
  port: 8102

spring:
  application:
    name: 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: 密码
    username: 用户名
    url: mysql链接
seata:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 10.31.98.22:8848
      group : SEATA_GROUP
      namespace: 命名空间
      username: nacos账号
      password: nacos密码
      cluster: default
  config:
    type: nacos
    nacos:
      server-addr: 10.31.98.22:8848
      group: SEATA_GROUP
      username: nacos账号
      password: nacos密码
      data-id: seataServer.properties
      namespace: 命名空间
  enabled: true
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default

注意此处default_tx_group: default必须和seata配置文件里面的service.vgroupMapping.default_tx_group=default相对应,否则启动会报异常

代码

@GlobalTransactional
public void createOrder(UserImages userImages) {
    // 模拟商品扣除
    userImagesService.save(userImages);
    OrderDto orderDto = OrderDto.builder().goodId(1001).userId(999).build();
    //远程调用
    orderFeign.createOrder(orderDto);

}

此处使用imageinfo表插入一条信息代替商品的扣除,seata对insert,update,delete生效

启动服务并且留意控制台输出日志

image.png

image.png

看到TM和RM注册成功表示成功连接到Seata

订单服务

配置

server:
  port: 8103

spring:
  application:
    name: 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: 密码
    username: 用户名
    url: mysql链接
seata:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 10.31.98.22:8848
      group : SEATA_GROUP
      namespace: 命名空间
      username: nacos账号
      password: nacos密码
      cluster: default
  config:
    type: nacos
    nacos:
      server-addr: 10.31.98.22:8848
      group: SEATA_GROUP
      username: nacos账号
      password: nacos密码
      data-id: seataServer.properties
      namespace: 命名空间
  enabled: true
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default

代码

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, GoodsOrder> implements IOrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Transactional
    public int createOrder(GoodsOrder goodsOrder) {
        int i = 0;
        i = orderMapper.insertOne(goodsOrder.getGoodId(),goodsOrder.getOrderId(),goodsOrder.getUserId());
        //出现异常
//        int e = 1 / 0;
        return i;
    }
}

启动服务,观察控制台,看到RM和TM注册成功即可

测试

正常测试

使用foxApi进行服务访问,这里由于有gateway网关的关系,调用方式会和直接调用有一定点区别

image.png

然后看一下image_info和goods表

image.png

image.png

正常情况下都可以正常插入

再看看控制台

image.png

事务正常提交

测试回滚

现在测试一下订单访问抛出异常

@Transactional
public int createOrder(GoodsOrder goodsOrder) {
    int i = 0;
    i = orderMapper.insertOne(goodsOrder.getGoodId(),goodsOrder.getOrderId(),goodsOrder.getUserId());
    int e = 1 / 0;
    return i;
}

调用接口,看到订单服务的控制台报错

image.png

再看看商品服务的控制台

image.png

控制台出现了rollbacked,然后再去查看数据库里面的表,发现没有插入记录

seata入门引用就到这里了,笔者会进行调试和补充,欢迎评论区留言。