Springcloud基础知识(15)- Spring Cloud Alibaba Seata (一) | Seata 简介、事务模式、Seata Server
随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是 “分布式事务” 问题。
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。
例如,用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:
(1) 调用订单服务创建订单数据
(2) 调用库存服务扣减库存
(3) 调用账户服务扣减账户金额
(4) 最后调用订单服务修改订单状态
为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。
Seata 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。
1. Seata 简介
阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。
阿里巴巴对于分布式事务问题先后发布了以下解决方案:
(1) 2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
(2) 2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
(3) 2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
(4) 2019 年 fescar 被重命名为了seata(simple extensiable autonomous transaction architecture)。
(5) TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。
分布式事务主要涉及以下概念:
(1) 事务:由一组操作构成的可靠、独立的工作单元,事务具备 ACID 的特性,即原子性、一致性、隔离性和持久性。
(2) 本地事务:本地事务由本地资源管理器(通常指数据库管理系统 DBMS,例如 MySQL、Oracle 等)管理,严格地支持 ACID 特性,高效可靠。本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器,即本地事务只能对自己数据库的操作进行控制,对于其他数据库的操作则无能为力。
(3) 全局事务:全局事务指的是一次性操作多个资源管理器完成的事务,由一组分支事务组成。
(4) 分支事务:在分布式事务中,就是一个个受全局事务管辖和协调的本地事务。
我们可以将分布式事务理解成一个包含了若干个分支事务的全局事务。全局事务的职责是协调其管辖的各个分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足 ACID 特性的本地事务。
Seata 对分布式事务的协调和控制,主要是通过 XID 和 3 个核心组件实现的。
(1) XID:是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
(2) TC(Transaction Coordinator)组件:事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
(3) TM(Transaction Manager)组件:事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
(4) RM(Resource Manager)组件:资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
Seata 的整体工作流程如下:
(1) TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
(2) XID 通过服务的调用链传递到其他服务;
(3) RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
(4) TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
(5) TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。
Seata: https://seata.io/zh-cn/index.html
Seata GibHub: https://github.com/seata/seata
2. Seata 事务模式
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。
在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
1) AT 模式的前提
任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:
(1) 必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
(2) 应用程序必须是使用 JDBC 对数据库进行访问的 Java 应用。
此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。不同数据库在创建 UNDO_LOG 表时会略有不同,以 MySQL 为例,其 UNDO_LOG 表的创表语句如下:
1 CREATE TABLE `undo_log` ( 2 `id` bigint(20) NOT NULL AUTO_INCREMENT, 3 `branch_id` bigint(20) NOT NULL, 4 `xid` varchar(100) NOT NULL, 5 `context` varchar(128) NOT NULL, 6 `rollback_info` longblob NOT NULL, 7 `log_status` int(11) NOT NULL, 8 `log_created` datetime NOT NULL, 9 `log_modified` datetime NOT NULL, 10 PRIMARY KEY (`id`), 11 UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) 12 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2) AT 模式的工作机制
Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。
假设数据库存在一个 user 表,表结构如下。
列名 | 类型 | 主键 |
id | bigint(20) | Y |
name | varchar(255) | |
url | varchar(255) |
在某次分支事务中,需要在 user 表中执行以下操作。
UPDATE user SET url = 'www.test2.com' WHERE name = 'test';
AT 模式阶段一:
(1) 获取 SQL 的基本信息:Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(UPDATE)、表名(user)、判断条件(WHERE name = 'test')等相关信息。
(2) 查询前镜像:根据得到的业务 SQL 信息,生成 “前镜像查询语句”。
SELECT id,name,url FROM user WHERE name='test';
执行 “前镜像查询语句”,得到即将执行操作的数据,并将其保存为 “前镜像数据(beforeImage)”。
id name url
1 test www.test.com
(3) 执行业务 SQL(UPDATE user SET url = 'www.test2.com' WHERE name = 'test';),将这条记录的 url 修改为 www.test2.com。
(4) 查询后镜像:根据 “前镜像数据”的主键(id : 1),生成 “后镜像查询语句”。
SELECT id,name,url FROM user WHERE id= 1;
执行 “后镜像查询语句”,得到执行业务操作后的数据,并将其保存为 “后镜像数据(afterImage)”。
id name url
1 test www.test2.com
(5) 插入回滚日志:将前后镜像数据和业务 SQL 的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中,示例回滚日志如下。
1 { 2 "@class": "io.seata.rm.datasource.undo.BranchUndoLog", 3 "xid": "172.26.54.1:8091:5962967415319516023", 4 "branchId": 5962967415319516027, 5 "sqlUndoLogs": [ 6 "java.util.ArrayList", 7 [ 8 { 9 "@class": "io.seata.rm.datasource.undo.SQLUndoLog", 10 "sqlType": "UPDATE", 11 "tableName": "user", 12 "beforeImage": { 13 "@class": "io.seata.rm.datasource.sql.struct.TableRecords", 14 "tableName": "user", 15 "rows": [ 16 "java.util.ArrayList", 17 [ 18 { 19 "@class": "io.seata.rm.datasource.sql.struct.Row", 20 "fields": [ 21 "java.util.ArrayList", 22 [ 23 { 24 "@class": "io.seata.rm.datasource.sql.struct.Field", 25 "name": "id", 26 "keyType": "PRIMARY_KEY", 27 "type": -5, 28 "value": [ 29 "java.lang.Long", 30 1 31 ] 32 }, 33 { 34 "@class": "io.seata.rm.datasource.sql.struct.Field", 35 "name": "url", 36 "keyType": "NULL", 37 "type": 12, 38 "value": "www.test.com" 39 } 40 ] 41 ] 42 } 43 ] 44 ] 45 }, 46 "afterImage": { 47 "@class": "io.seata.rm.datasource.sql.struct.TableRecords", 48 "tableName": "user", 49 "rows": [ 50 "java.util.ArrayList", 51 [ 52 { 53 "@class": "io.seata.rm.datasource.sql.struct.Row", 54 "fields": [ 55 "java.util.ArrayList", 56 [ 57 { 58 "@class": "io.seata.rm.datasource.sql.struct.Field", 59 "name": "id", 60 "keyType": "PRIMARY_KEY", 61 "type": -5, 62 "value": [ 63 "java.lang.Long", 64 1 65 ] 66 }, 67 { 68 "@class": "io.seata.rm.datasource.sql.struct.Field", 69 "name": "url", 70 "keyType": "NULL", 71 "type": 12, 72 "value": "www.test2.com" 73 } 74 ] 75 ] 76 } 77 ] 78 ] 79 } 80 } 81 ] 82 ] 83 }
(6) 注册分支事务,生成行锁:在这次业务操作的本地事务提交前,RM 会向 TC 注册分支事务,并针对主键 id 为 1 的记录生成行锁。
(7) 本地事务提交:将业务数据的更新和前面生成的 UNDO_LOG 一并提交。
(8) 上报执行结果:将本地事务提交的结果上报给 TC。
AT 模式阶段二 (提交):
当所有的 RM 都将自己分支事务的提交结果上报给 TC 后,TM 根据 TC 收集的各个分支事务的执行结果,来决定向 TC 发起全局事务的提交或回滚。
若所有分支事务都执行成功,TM 向 TC 发起全局事务的提交,并批量删除各个 RM 保存的 UNDO_LOG 记录和行锁;否则全局事务回滚。
AT 模式阶段二 (回滚):
若全局事务中的任何一个分支事务失败,则 TM 向 TC 发起全局事务的回滚,并开启一个本地事务,执行如下操作。
(1) 查找 UNDO_LOG 记录:通过 XID 和分支事务 ID(Branch ID) 查找所有的 UNDO_LOG 记录。
(2) 数据校验:将 UNDO_LOG 中的后镜像数据(afterImage)与当前数据进行比较,如果有不同,则说明数据被当前全局事务之外的动作所修改,需要人工对这些数据进行处理。
(3) 生成回滚语句:根据 UNDO_LOG 中的前镜像(beforeImage)和业务 SQL 的相关信息生成回滚语句:
UPDATE user SET url = 'www.test.com' WHERE id = 1;
(4) 还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。
(5) 提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
3) 数据源支持
模式 | 数据源支持 |
AT模式 | MySQL、Oracle、PostgreSQL、TiDB、MariaDB。 |
TCC模式 | 1.4.2 版本及之前不依赖数据源,1.4.2 版本之后增加了TCC防悬挂措施,需要数据源支持。 |
Saga模式 | Saga 模式不依赖数据源。 |
XA模式 | 支持实现了XA协议的数据库。比如:MySQL、Oracle、PostgreSQL、MariaDB。 |
4) ORM 框架支持
Seata 虽然是保证数据一致性的组件,但对于 ORM 框架并没有特殊的要求,像主流的 Mybatis,Mybatis-Plus,Spring Data JPA, Hibernate 等都支持。
这是因为 ORM 框架位于 JDBC 结构的上层,而 Seata 的 AT、XA 事务模式是对 JDBC 标准接口操作的拦截和增强。
3. Seata Server
本文以 Seata Server 1.4.2 为例,演示 Windows 下安装和配置 Seata Server,步骤如下。
1) 下载
浏览器访问 Seata Server 下载页面(https://github.com/seata/seata/releases/tag/v1.4.2),并在页面最下方点击链接 seata-server-1.4.2.zip。
下载完成后,解压 seata-server-1.4.2.zip,目录结构如下:
seata
|- bin
|- conf
|- lib
|- logs
各目录说明如下:
bin:用于存放 Seata Server 可执行命令。
conf:用于存放 Seata Server 的配置文件。
lib:用于存放 Seata Server 依赖的各种 Jar 包。
logs:用于存放 Seata Server 的日志。
2) 服务注册和配置
(1) 查看 conf/registry.conf 文件,内容如下
1 registry { 2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 3 type = "file" 4 5 nacos { 6 application = "seata-server" 7 serverAddr = "127.0.0.1:8848" 8 group = "SEATA_GROUP" 9 namespace = "" 10 cluster = "default" 11 username = "" 12 password = "" 13 } 14 15 ... 16 17 file { 18 name = "file.conf" 19 } 20 } 21 22 config { 23 # file、nacos 、apollo、zk、consul、etcd3 24 type = "file" 25 26 nacos { 27 serverAddr = "127.0.0.1:8848" 28 namespace = "" 29 group = "SEATA_GROUP" 30 username = "" 31 password = "" 32 dataId = "seataServer.properties" 33 } 34 35 ... 36 37 file { 38 name = "file.conf" 39 } 40 }
注:registry {} 是注册中心的配置,config {} 是配置中心的配置。
(2) 注册中心
注册中心是微服务架构中的 ”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用。比如 Seata Client 端 (TM, RM),发现 Seata Server (TC) 集群的地址,彼此通信。
在 conf/registry.conf 文件中 registry {} 的注释部分已经提示我们,Seata 支持多种注册中心:
(1) file (默认配置,本地文件:包含 *.conf、*.properties、*.yml 等配置文件)
(2) nacos
(3) eureka
(4) redis
(5) zk (zookeeper)
(6) consul
(7) etcd3 (etcd)
(8) sofa
(3) 配置中心
配置中心是一个 "大货仓",内部放置着各种配置文件,你可以通过自己所需进行获取配置加载到对应的客户端。比如 Seata Client 端 (TM, RM)、Seata Server (TC) 会去读取全局事务开关,事务会话存储模式等信息。
在 conf/registry.conf 文件中 config {} 的注释部分也已经提示我们,Seata 支持多种配置中心:
(1) file (默认配置,本地文件:包含 *.conf、*.properties、*.yml 等配置文件)
(2) nacos
(3) apollo
(4) zk (zookeeper)
(5) consul
(6) etcd3 (etcd)
3) 默认配置 (file)
在 conf/registry.conf 文件里,registry {} 和 config {} 里的 type = "file","file" 的配置如下:
file {
name = "file.conf"
}
查看 conf/file.conf 文件,内容如下:
1 ## transaction log store, only used in seata-server 2 store { 3 ## store mode: file、db、redis 4 mode = "file" 5 ## rsa decryption public key 6 publicKey = "" 7 8 ## file store property 9 file { 10 11 ... 12 13 } 14 15 ## database store property 16 db { 17 18 ... 19 20 } 21 22 ## redis store property 23 redis { 24 ## redis mode: single、sentinel 25 mode = "single" 26 27 ... 28 29 } 30 }
注:conf/file.conf 除了 store {},还可以添加其它配置,如 transport {}、server {} 等,具体配置可以参考 conf/file.conf.example 。
在 conf/registry.conf 里 type = "file" 时,conf/file.conf 才有效。同理,当 type = "nacos" 时,我们需要把 conf/file.conf 里需要的配置,移植到 nacos 的配置中心。
当 conf/file.conf 有效,并且它的 store.mode 配置为 "file",启动 Seata Server 时, bin 目录下会自动生成一个 sessionStore 子目录。
4) 启动 Seata Server
双击 bin 目录下的 seata-server.bat ,启动 Seata Server,默认端口是 8091。
1 ... 2 3 22:36:41.922 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry 4 22:36:41.926 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is C:\Applications\Java\alibaba-cloud\seata-server-1.4.2\conf\registry.conf 5 22:36:41.985 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is file.conf 6 22:36:41.985 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is C:\Applications\Java\alibaba-cloud\seata-server-1.4.2\conf\file.conf 7 22:36:42.608 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
使用带参数的命令行方式启动,格式如下:
C:\seata-server-1.4.2\bin>seata-server -h localhost -p 8092 -m db -n 1 -e test
参数说明:
-h: 主机名或 IP
-p: 端口
-m: 事务记录存储方式,支持 file、db、redis
-n:集群的节点id, 默认 1
-e: 运行环境,比如 dev、test 等,根据这个参数调用对应的 registry-*.conf