目录

springcloudAlibaba_seata 介绍与安装

  1. Seata 官网
  2. Seata 源码 GitHub 地址
  3. Seata 官方文档(比较鸡肋,介绍不清楚)
  4. Seata 下载地址

分布式事务问题

用例

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

架构图

20201116102512

问题

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局数据一致性问题是无法保证的。

一句话,一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

Seata是什么

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

在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各部门业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

下面介绍Seata在分布式处理过程中的ID+三组件模型

术语

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

seata服务器扮演tc的角色

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

事务的发起方,也就是标了@GlobalTransactional注解的方法所在服务扮演着tm的角色

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

RM和数据源一一对应

处理过程

http://img.cana.space/picStore/20201116103008.png

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;XID 在微服务调用链路的上下文中传播;
  2. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  3. TM 向 TC 发起针对 XID 的全局提交或回滚决议;
  4. TC 调度 XID 下管辖的全局分支事务,完成分支提交或回滚请求。

Seata特色功能

http://img.cana.space/picStore/20201116103828.png

AT模式

AT模式是使用seata最常用的分布式事务模式,下面简单介绍一下,官网说明地址

前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

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

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

20201117124015

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

20201117124205

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

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

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

注意
说白了,就是防止死锁,破坏死锁条件,引入超时机制

读隔离

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

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

http://img.cana.space/picStore/20201117124641.png

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

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

示例

以一个示例来说明整个 AT 分支的工作过程。

业务表:product

Field Type Key
id bigint(20) PRI
name varchar(100)
since varchar(100)

AT 分支事务的业务逻辑:

1
update product set name = 'GTS' where name = 'TXC';

一阶段

http://img.cana.space/picStore/20201116172417.png

过程:Seata会拦截“业务SQL”

  1. 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
  2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
1
select id, name, since from product where name = 'TXC';

得到前镜像:

id name since
1 TXC 2014
  1. 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。

  2. 查询后镜像:根据前镜像的结果,通过 主键 定位数据。

1
select id, name, since from product where id = 1`;

得到后镜像:

id name since
1 GTS 2014
  1. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
	"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"
}
  1. 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁

  2. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。

  3. 将本地事务提交的结果上报给 TC。

二阶段-回滚

http://img.cana.space/picStore/20201116172514.png

  1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  3. 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
  4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
1
update product set name = 'TXC' where id = 1;
  1. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

二阶段-提交

http://img.cana.space/picStore/20201116172447.png

  1. 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

Seata安装

官方说明

版本说明

seata script

环境说明

  • mysql:5.7
  • nacos:1.2.0,强烈建议第二梯队,开始使用了最新版1.4.0,最新版访问这个http://localhost:8848/nacos/v1/ns/instance一直报server is DOWN now, please try again later!`,导致seata启动不了,降成1.2就可以了。
  • seata:1.3.0

seata/script介绍

  • client
    • client客户端参数配置,我们采用AT模式,此处选择db方式(at/db文件夹)
    • 存放client端sql脚本,参数配置
    • at/db/mysql.sql,undo_log建表语句
    • spring目录,整合spring,会用到spring的相关配置,里面是.yml.properties相关配置信息
  • config-center
    • 配置中心参数,本文选用nacos,选择nacos文件夹,详细配置在config.txt文件夹下
    • 各个配置中心参数导入脚本以及推送脚本,config.txt(包含server和client,原名为nacos-config.txt)为通用参数文件
    • nacos目录 选用nacos,里面是将配置信息批量发送到nacos的脚本
    • config.txt 配置在nacos中的数据
  • server
    • server端参数配置,同样选用db方式(db文件夹)
    • server端数据库脚本及各个容器配置
    • mysql.sql:里面是seata server端global_table、branch_table、lock_table三张表的建表语句

安装

先从github仓库地址克隆一份下来,要使用到其中的一些配置、脚本

  1. 建数据库(seata)

  2. 进入脚本目录 seata/script/server/db/mysql.sql, 执行sql语句

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    
    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    CREATE TABLE IF NOT EXISTS `global_table`
    (
        `xid`                       VARCHAR(128) NOT NULL,
        `transaction_id`            BIGINT,
        `status`                    TINYINT      NOT NULL,
        `application_id`            VARCHAR(32),
        `transaction_service_group` VARCHAR(32),
        `transaction_name`          VARCHAR(128),
        `timeout`                   INT,
        `begin_time`                BIGINT,
        `application_data`          VARCHAR(2000),
        `gmt_create`                DATETIME,
        `gmt_modified`              DATETIME,
        PRIMARY KEY (`xid`),
        KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
       
    -- the table to store BranchSession data
    CREATE TABLE IF NOT EXISTS `branch_table`
    (
        `branch_id`         BIGINT       NOT NULL,
        `xid`               VARCHAR(128) NOT NULL,
        `transaction_id`    BIGINT,
        `resource_group_id` VARCHAR(32),
        `resource_id`       VARCHAR(256),
        `branch_type`       VARCHAR(8),
        `status`            TINYINT,
        `client_id`         VARCHAR(64),
        `application_data`  VARCHAR(2000),
        `gmt_create`        DATETIME(6),
        `gmt_modified`      DATETIME(6),
        PRIMARY KEY (`branch_id`),
        KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
       
    -- the table to store lock data
    CREATE TABLE IF NOT EXISTS `lock_table`
    (
        `row_key`        VARCHAR(128) NOT NULL,
        `xid`            VARCHAR(96),
        `transaction_id` BIGINT,
        `branch_id`      BIGINT       NOT NULL,
        `resource_id`    VARCHAR(256),
        `table_name`     VARCHAR(32),
        `pk`             VARCHAR(36),
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
  3. Server端参数配置

    我们进入到软件目录/conf (~/soft/seata/conf),修改 registry.conf 这个文件,因为本文使用nacos配置中心存储配置,如果你使用文件存储配置还需要修改file.conf文件。

    修改1:registry->type:nacos,下面的nacos配置信息采用默认即可,根据你的nacos配置相应进行修改

    修改2:registry->config->type:改成nacos,本文使用nacos配置中心来存储数据库等服务器配置信息,同样下面的nacos配置根据你自己的情况进行修改,如果没有定制过直接默认即可。

  4. 设置服务器配置信息

    进入本地 资源目录 seata/script/config-center/config.txt ,展示的是 Seata 1.2.0 版本所有配置中心的内容,全部配置点击链接查看。本文使用db方式,故选择db相关配置,需要用到的配置如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    service.vgroupMapping.my_test_tx_group=default
    service.default.grouplist=127.0.0.1:8091
    service.enableDegrade=false
    service.disableGlobalTransaction=false
    store.mode=db
    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
    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.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxWait=5000
    

    修改之前cp一份config.txt留作以后参考

    如果你的nacos和seata都采用默认配置的话,只需要修改存储方式store.mode(db),数据库配置即可

  5. 推送配置信息到配置中心

    进入到nacos-config.sh脚本所在目录,执行

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    $ sh nacos-config.sh
    set nacosAddr=localhost:8848
    set group=SEATA_GROUP
    Set service.vgroupMapping.my_test_tx_group=default successfully
    Set service.default.grouplist=127.0.0.1:8091 successfully
    Set service.enableDegrade=false successfully
    Set service.disableGlobalTransaction=false successfully
    Set store.mode=db successfully
    Set store.db.datasource=druid successfully
    Set store.db.dbType=mysql successfully
    Set store.db.driverClassName=com.mysql.jdbc.Driver successfully
    Set store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true successfully
    Set store.db.user=username successfully
    Set store.db.password=password successfully
    Set store.db.minConn=5 successfully
    Set store.db.maxConn=30 successfully
    Set store.db.globalTable=global_table successfully
    Set store.db.branchTable=branch_table successfully
    Set store.db.queryLimit=100 successfully
    Set store.db.lockTable=lock_table successfully
    Set store.db.maxWait=5000 successfully
    =========================================================================
     Complete initialization parameters,  total-count:18 ,  failure-count:0
    =========================================================================
     Init nacos config finished, please start seata-server.
    

    你自己打开 nacos-config.sh 脚本 看看它查找 config.txt 的逻辑就可以了,只要能够读取到 config.txt 文件即可。nacos-config.sh 脚本支持传入 四个参数

    1. -h nacos 所在服务器的IP地址,默认为 localhost
    2. -p nacos 端口号,默认为 8848
    3. -g nacos 配置所属 group 名称,默认为 SEATA_GROUP
    4. -t 将 nacos 配置保存到指定的命名空间,默认为 "",代表 public 命名空间(注意:-t 参数值接收的是 命名空间ID,不是 命名空间名称

    成功后查看nacos配置

    http://img.cana.space/picStore/20201116233235.png

    说明配置信息已经成功配置到nacos中了,接下来就是启动seate-server

  6. 进入到执行目录(/Users/david/soft/seata/bin)下,执行

    1
    2
    3
    
    $ ./seata-server.sh
    ...
    2020-11-17 01:19:31.538 INFO [main]io.seata.core.rpc.netty.RpcServerBootstrap.start:155 -Server started ...
    

    查看seata服务是否注册到配置中心,查看nacos服务管理中的服务列表

    http://img.cana.space/picStore/20201117012054.png

    至此,Seata Server端成功启动