Spring Cloud Alibaba Seata:分布式事务
一、Seata简介
1. Seata的由来
随着互联网项目的发展,分布式架构的项目显示出了独特的独特的魅力。但是分布式事务问题在分布式架构项目中越显重要。
2019年1月阿里巴巴团队发起了开源项目Fescar(Fast & Easy Commit And Rollback )与社区共建分布式事务解决方案。Fescar的愿景是:让分布式事务可以像本地事务一样,简单和高效。
随着Fescar社区发展,蚂蚁金服加入Fescar社区,并在0.4.0版本中贡献了TCC模式。
为了打造更加中立、更开放、生态更丰富的分布式事务解决方案。经社区成员投票,对Fescar升级,命名为Seata,意味着 Simple Extensible Autonomous Transaction Architecture,简单的可扩展自治事务体系结构。
目前Seata源码托管在github中。
https://github.com/seata/seata/
Seata中文官网地址。
http://seata.io/zh-cn/
2. 官方对Seata的解释
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
3. Seata术语
3.1TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
3.2 TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
3.3 RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
4. Seata的发展历程图
二、分布式事务理论依据
分布式事务存在两大理论依据:CAP定理和BASE理论。
1. CAP定理(分布式一致性定理)
CAP定理是指在一个分布式系统中Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),最多同时满足其中两个,三者不可兼得。
1.1 一致性(C Consistency)
在分布式系统中所有节点的状态是一样的。
1.2 可用性(A Availability)
在集群中一部分节点出现故障后,整个集群是否还能响应客户端请求。
1.3 分区容错性(P Partition tolerance)
以实际效果而言,分区相当于对操作的时限要求。如果系统不能在一定时限内达到数据一致性,就意味着发生了分区的情况,此时就必须在A和C中做选择。
2. BASE理论
是指Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。 BASE理论是对CAP中一致性和可用性权衡的结果,是基于CAP演化而来的。
BASE理论核心思想:即使无法做到强一致性,每个应用都可以根据自身业务特点,采用适当的方式达到最终一致性。
2.1 基本可用(BA Basically Available)
是指在分布式系统中出现不可知故障的时候,允许损失部分可用性。此处要注意:损失部分可用性,不代表整个系统不可用。
例如:
- 可以增加响应时间。由之前0.5秒,在出现故障的时候变成1~2秒。
- 由于一些特殊原因,使网站访问流量激增,为了保证整个系统的稳定性,部分访问者可能被引导到降级页面中。
2.2 软状态(S Soft state)
是指系统中数据允许存在中间状态(软状态),并认为这个状态是不影响系统的可用性的。通俗解释:允许分布式节点之间存在同步延迟。
例如:
- 在Eureka集群中数据同步时就存在软状态。
2.3 最终一致性(E Eventually consistent)
允许整个系统中数据在经过一定时间后,最终能达到整个系统的一致性。但是这个时间绝对不可以过长。
强一致性要求系统接收请求后,整个系统必须达到一致性效果,才会响应结果。
最终一致性是弱一致性的特例。满足最终一致性的系统在响应给用户结果时整个系统可能是没有达到一致性的,但是最终一定会达到一致性效果的。
三、Seata支持的事务模式
1. Seata AT模式
1.1 适用场景
- 适用于支持本地ACID事务的关系型数据库。
- 对于Java应用适用于通过JDBC可以访问的数据库。
- 具体数据库:MySQL、Oracle、PostgreSQL、TiDB
1.2 总体实现
总体是基于二阶段提交协议演变而来。
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:提交异步化,尽可能保证性能高。回滚通过一阶段日志进行反向补偿。
1.3 写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
1.3.1 一个示例来说明
两个全局事务tx1和tx2,分别对a表的m字段进行更新操作,m的初始值1000。
tx1先开始,开启本地事务,拿到本地锁,更新操作m=1000-100=900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。tx2后开始,开启本地事务,拿到本地锁,更新操作m=900-100=800。本地事务提交前,尝试拿该记录的全局锁,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁。
tx1二阶段全局提交,释放全局锁。tx2拿到全局锁提交本地事务。
如果tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果tx2仍在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支的回滚会一直重试,直到tx2的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
因为整个过程全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写的问题。
1.4 读隔离
在数据库本地事务隔离级别读已提交(ReadCommitted)或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交(ReadUncommitted)。
如果应用在特定场景下,必需要求全局的读已提交,目前Seata的方式是通过SELECT FOR UPDATE语句的代理。
SELECTFORUPDATE语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚SELECTFORUPDATE语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
出于总体性能上的考虑,Seata目前的方案并没有对所有SELECT语句都进行代理,仅针对FORUPDATE的SELECT语句。
1.5 具体示例
以一个具体示例来说明整个 AT 分支的工作过程。
1.5.1 业务表
1.5.2 要执行的业务逻辑
AT分支事务要执行的SQL:
update product set name = 'GTS' where name = 'TXC';
1.5.3 一阶段
过程:
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
得到前镜像数据:
3. 执行业务 SQL:更新这条记录的 name 为 'GTS'。
4. 查询后镜像:根据前镜像的结果,通过主键定位数据。
select id, name, since from product where id = 1;
得到后镜像:
5. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
{
"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"
}
- 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
- 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
- 将本地事务提交的结果上报给 TC。
1.5.4 二阶段-回滚
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
1.5.5 二阶段-提交
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
1.6 附录
1.6.1 回滚日志表
UNDO_LOG Table:不同数据库在类型上会略有差别,不同的版本在表格字段上会略有差别,最终以官方源码为准。
以 MySQL 为例:
2. Seata TCC模式
2.1 适用场景
TCC适用于任何需要做分布式事务的场景。
由于事务回滚和事务提交的逻辑需要由程序员编写,所以适用AT模式的事务交给AT进行处理,其他不支持ACID或不支持JDBC连接的数据库(数据存储工具)可以使用TCC模式。
2.2 总体实现
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
- 二阶段 commit 行为:调用自定义的 commit 逻辑。
- 二阶段 rollback 行为:调用自定义的 rollback 逻辑。
3. Seata Saga 模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
长事务:运行时间比较长,长时间未提交的事务,也可以称之为大事务。这类事务往往会造成大量的阻塞和锁超时,容易造成主从延迟,要尽量避免使用长事务。
理论基础:Hector & Kenneth 发表论⽂ Sagas (1987)
3.1 适用场景
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
3.2 优势
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
3.3 缺点
- 不保证隔离性
4. Seata XA 模式
4.1 适用场景
- 支持XA 事务的数据库。
- Java 应用,通过 JDBC 访问数据库。
4.2 总体实现
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
4.2.1 执行阶段
可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
4.2.2 完成阶段
分支提交:执行 XA 分支的 commit
分支回滚:执行 XA 分支的 rollback
4.3 代码实现
XA模式和AT模式代码实现可以说几乎是一样的。唯一的区别是在代码中需要配置数据源代理。
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
四、基于Docker 安装Seata Server
1. 拉取镜像
docker pull seataio/seata-server:1.4.2
2. 准备数据库
Seata Server必须连接一个MySQL数据库来保存分布式事务管理过程中的数据。
2.1 创建Database
在MySQL中创建数据库Database。命名为seata(任意命名)。
2.2 创建表格
执行下述脚本,创建表格。
建议根据具体版本,从官方源码中找sql脚本,源码网址: https://github.com/seata/seata/blob/1.4.2/script/server/db/mysql.sql 。
-- -------------------------------- 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(128),
`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. 创建Seata Server容器
docker run -d --name seata -p 8091:8091 -e SEATA_IP=192.168.8.128 -e SEATA_PORT=8091 seataio/seata-server:1.4.2
4. 编辑Seata Server配置文件
4.1 连接Seata Server容器
docker exec -it seata sh
4.2 进入配置目录
cd /seata-server/resources
4.3 编辑registry.conf配置文件
此配置文件用于配置Seata Server连接的注册中心和存储。指定使用Nacos作为注册中心。
vi registry.conf
配置文件(部分内容)内容如下:
registry {
# file ...nacos ...eureka...redis...zk...consul...etcd3...sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.8.128:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file...nacos ..