2、分布式事务Seata的应用与原理分析

Seata官方文档open in new window

Seata Githubopen in new window

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

1、Seata的安装部署

1.1、二进制安装

Github地址open in new window

安装手册open in new window

从发布版本中下载目标版本的二进制安装包,2.0.0版本之后只提供源码,需要自己编译。为了省事这里下载的是2.0.0版本

image-20251226110345482

1.1.1、目录结构

下载之后解压的目录结构如下:

image-20251226110533843

  • bin:启动脚本,提供了windows和linux的启动脚本

  • conf:配置文件目录

  • target:seata-server.jar存放目录

  • logs:日志目录

1.1.2、启动

执行下面命令启动:

./bin/seata-server.sh

看到下面提示表示服务启动成功

image-20251226111514812

可以啊看到,这里提示服务启动成功,日志目录在对应标注的位置

image-20251226111632606

查看日志:

image-20251226111801067

1.1.3、UI控制台

从上面的启动日志可以看到这里提示还有一个控制台,必须得看一眼,于是输入真实的IP和端口号就可以看到

image-20251226111934568

用默认的账号和密码进去:账号(seata),密码(seata)

image-20251226112411355

这个控制台主要就是提供了可视化的事务管理功能,包括事务信息、全局锁信息和saga状态机的设计器

1.2、docker安装

1.2.1、镜像构建

从下载的解压包可以看到一个Dockerfile文件,通过这个文件就可以构建自己的seata-server的镜像

Dockerfile内容如下:

FROM openjdk:8u342

# set label
LABEL maintainer="Seata <seata.io>"

WORKDIR /$BASE_DIR

# ADD FORM distribution
ADD bin/ /seata-server/bin
ADD ext/ /seata-server/ext
ADD target/ /seata-server/target
ADD lib/ /seata-server/lib
ADD conf/ /seata-server/conf
ADD LICENSE /seata-server/LICENSE

# set extra environment
ENV LOADER_PATH="/seata-server/lib"
ENV TZ="Asia/Shanghai"
CMD ["bash","-c","/seata-server/bin/seata-server.sh && tail -f /dev/null"]

通过docker build . -t xxx就可以构建自己的镜像了,当然,也可以用别人已经构建好的镜像,直接拉取下来用就行

1.2.2、docker-compose启动

在解压目录下的script/server/docker-compose目录下有一个docker-compose.yaml文件:

version: "3"
services:
  seata-server:
    image: seataio/seata-server
    hostname: seata-server
    ports:
      - "8091:8091"
    environment:
      - SEATA_PORT=8091
      - STORE_MODE=file

找到这个文件,直接运行docker-compose up -d就可以启动seata-server了,这里会自动拉取seataio/seata-server:latest最新镜像,如果要用自己构建的镜像,就需要替换这个镜像名称

1.3、k8s安装

下面脚本是部署在middleware命名空间下的,如果命名空间不存在,要么创建一个,要么改成自己k8s集群已经存在的命名空间

1.3.1、配置文件cm

application-cm.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: seata-server-config
  namespace: middleware
data:
  application.yml: |
    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:
        type: nacos
        nacos:
          server-addr: 192.168.0.101:8848
          #namespace: prod
          group: SEATA_GROUP
          username: nacos
          password: nacos
          data-id: seataServer.properties
      registry:
        type: nacos
        nacos:
          application: seata-server
          server-addr: 192.168.0.101:8848
          group: SEATA_GROUP
          #namespace: prod
          cluster: default
          username: nacos
          password: nacos
      security:
        secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850aaaa
        tokenValidityInMilliseconds: 1800000
        ignore:
          urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

这里指定了console.user.username、console-user.password,同时配置了注册中心和配置中心为nacos,具体配置说明可以参考官方文档

1.3.2、deploy

deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: seata-server
  namespace: middleware
  labels:
    k8s-app: seata-server
spec:
  replicas: 3
  selector:
    matchLabels:
      k8s-app: seata-server
  template:
    metadata:
      labels:
        k8s-app: seata-server
    spec:
      containers:
        - name: seata-server
          image: registry.cn-hangzhou.aliyuncs.com/xhsx/seata-server:latest
          imagePullPolicy: IfNotPresent
          ports:
            - name: http-7091
              containerPort: 7091
              protocol: TCP
            - name: http-8091
              containerPort: 8091
              protocol: TCP
          volumeMounts:
            - name: seata-config
              mountPath: /seata-server/resources/application.yml
              subPath: application.yml
      volumes:
        - name: seata-config
          configMap:
            name: seata-server-config

我这里用的镜像是从官方拉取然后保存在阿里云镜像仓库里的,需要根据具体情况替换

1.3.3、运行

有了配置文件application-cm.yamldeploy.yaml之后就可以执行k8s命令启动了

kubectl -f application-cm.yaml
kubectl -f deploy.yaml

查看容器是否正常启动

ubuntu@ubuntu-server-01:~/k8s/seata$ sudo kubectl -n middleware get po | grep seat
seata-server-65649cbfd-5b5fd                 1/1     Running   28 (44d ago)    252d
seata-server-65649cbfd-dzxc8                 1/1     Running   28 (44d ago)    252d
seata-server-65649cbfd-l9676                 1/1     Running   28 (44d ago)    252d

1.3.4、service

我们都知道,k8s集群部署的容器是无法直接在外部使用的,要么将要访问seata-server的程序也部署到k8s里边,要么就要通过service将seata-server服务暴露出来,这里可以用到nodeport类型的service

apiVersion: v1
kind: Service
metadata:
  name: seata-server
  namespace: middleware
  labels:
    k8s-app: seata-server
spec:
  type: NodePort
  ports:
    - port: 8091
      nodePort: 30893
      protocol: TCP
      name: seata-8091
    - port: 7091
      nodePort: 30793
      protocol: TCP
      name: seata-7091
  selector:
    k8s-app: seata-server

通过k8s集群的节点IP+30793访问UI控制台可以看到也能正常访问,账号和密码是我们上面application-cm.yaml配置的seataSeata!@#

image-20251226115103094

image-20251226115315438

2、Seata提供的四种事务模式

2.1、AT模式

2.1.1、AT模式事务执行流程

Seata的AT模式是基于数据库本身的ACID基础上实现的,也是默认的模式,AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,它对我们的数据源进行了进一步包装,提供了一个DatasourceProxy数据源代理

image-20251225143829087

AT模式的两阶段提交:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源(需要在本地数据库中创建一张表,undo_log 表,2.x版本的sql脚本地址:sqlopen in new window)。
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) 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(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

undo_log的内容大概如下,记录了修改前和修改后的完整内容,如果事务要进行回滚,则会根据sqlType通过sql解析器生成一个逆向sql去回滚

{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}
  • 二阶段:
    • 提交事务,异步化,非常快速地完成。
    • 事务回滚,通过一阶段的回滚日志进行反向补偿。

2.1.2、写隔离

Seata的AT模式写隔离是基于全局锁来实现的,以官网的一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁

image-20251226152448829

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

image-20251226152542450

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

2.1.2、读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

image-20251226152729967

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

2.2、TCC模式

TCC模式是一种代码侵入的分布式事务解决方案,它不依赖底层数据库,完全由程序员自己通过代码控制,我们需要给分布式事务方法除了提供具体的业务方法之外,还需要手动去实现业务逻辑正常执行的时候的事务提交的方法以及业务逻辑执行失败的时候的事务回滚方法,也就是Try、Confirm、Cancel这三个方法都需要我们去实现

public interface TccActionOne {
    @TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
    public boolean commit(BusinessActionContext actionContext);
    public boolean rollback(BusinessActionContext actionContext);
}

2.2.1、TCC模式事务执行流程

image-20251226150718935

上图TM为事务管理器,简单理解就是我们开启全局事务的地方,RM是资源管理器,比如数据库,TC为事务协调器,也就是seata-server。上述流程概括就是:

  • TM开启全局事务,向TC注册一个全局事务,并会得到一个全局事务ID
  • 全局事务里边包含两个子业务,这两个子业务分别会向TC也就是seata-server注册一个分支事务
  • 两个子业务执行结束预提交,并且向seata-server主动上报prepare阶段的分支事务状态
  • 如果所有的分支事务都执行成功,TM会通知TC提交全局事务,TC会通知各分支事务进行提交。反之则会通知进行回滚

2.2.2、TCC模式下的空回滚问题

由于TCC是基于补偿的机制,件一个事务拆分成了三个环节。假如现在因为网络问题,Try阶段因为超时导致了事务执行失败,按照逻辑全局事务就会执行Cancel逻辑进行回滚。

当时由于我们的try都没有执行,比如是A往B转100块钱,那么回滚的话就会B往A转100块钱。当时现在前一个逻辑没有执行,这样一来B不就亏大发了吗?

则就是TCC的空回滚问题,怎么解决?

方案:本地事务控制表

设计一张表,记录下本次事务相关的信息,与try阶段的本地事务一起写入,在cancel阶段 去这张事务控制表去查,如果有对应的记录,则说明try阶段执行成功了,反之则说明try阶段没有执行

2.2.3、TCC模式下的幂等性问题

在TC通知RM提交分支事务的时候,如果因为网络超时没有收到对方的响应,它会重复发送提交或者回滚的通知。如何这个时候不考虑消息的幂等性,则会出现数据的不一致问题

Confirm和Cancel则两个环节的幂等性如何保障呢?

方案:状态机

设计一个状态机,方法的执行与否会根据事务的状态去判断,并且状态是不可逆的,这样当指定状态已经发出一次修改之后,就无法再通过该状态修改同一条数据了

2.2.4、TCC模式下的空悬挂问题

我们知道分布式场景下网络是不可靠的,假如我们前面已经开启了允许空回滚问题,这个时候如果是因为网络延迟,try执行超时导致了回滚,这个时候cancel先执行了,当时实际上try阶段的请求已经发送出去了,这个时候try会在cancel之后执行,这种情况同样会出现数据一致性问题,这就是空悬挂问题。

怎么解决呢?

方案:本地事务控制表

方案还是通过一张本地事务控制表来控制。前面可以通过这张表来判断cancel是否需要执行,那么反过来同样也可以通过这张表来控制try是否能够执行。也就是说在cancel阶段写入这张表,然后在try方法执行的时候去这张表里去查,如果对应事务的cancel操作执行了,则不允许在执行try操作

2.3、XA模式

XA 模式是从 1.2 版本支持的强一致性事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

image-20251226162828913

缺点:

XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,性能差

2.4、saga模式

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

image-20251226163204889

前面看UI控制台的时候可以看到一个Saga状态机设计器菜单,实际上seata的saga模式是基于状态机引擎来实现的:

  • 1、通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
  • 2、状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
  • 3、状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚

注意: 异常发生时是否进行补偿也可由用户自定义决定

  • 4、可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

3、AT模式示例

https://github.com/apache/incubator-seata-samples/tree/master/at-sample

以SpringBoot+Dubbo+Seata为例

3.1、引入依赖

<dependency>
    <groupId>org.apache.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

其它集成所需要的依赖这里没有列出

3.2、配置文件

seata.tx-service-group=my_test_tx_group
seata.enabled=true
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=${seata.address:127.0.0.1}:8091
seata.registry.type=file
seata.config.type=file

这里会根据seata.tx-service-group对应的值去找seata.service.vgroup-mapping为前缀的配置项的值default,这个default就是seata-server的集群分组,程序会再去找seata.service.grouplist+default对应的seata-server的地址。

注册中心和配置中心的配置方式参考官方文档。

3.3、开启事务

在需要开启全局事务的方法上打上注解

@GlobalTransactional(timeoutMills = 300000, name = "spring-dubbo-tx")

AT模式在使用起来应该是非常简单的了,为了我们的应用数据的安全稳定,不妨把seata用起来吧


扫码关注

最后更新时间: 2026/1/9 14:14:44