随着互联网项目的发展,分布式架构的项目显示出了独特的独特的魅力。但是分布式事务问题在分布式架构项目中越显重要。
2019年1月阿里巴巴团队发起了开源项目Fescar(Fast & Easy Commit And Rollback )与社区共建分布式事务解决方案。Fescar的愿景是:让分布式事务可以像本地事务一样,简单和高效。
随着Fescar社区发展,蚂蚁金服加入Fescar社区,并在0.4.0版本中贡献了TCC模式。
为了打造更加中立、更开放、生态更丰富的分布式事务解决方案。经社区成员投票,对Fescar升级,命名为Seata,意味着 Simple Extensible Autonomous Transaction Architecture,简单的可扩展自治事务体系结构。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0FTo0urB-1661433194406)(images/seata1.png)]](https://1000bd.com/contentImg/2023/10/25/132530439.png)
目前Seata源码托管在github中。
https://github.com/seata/seata/
Seata中文官网地址。
http://seata.io/zh-cn/
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eIBSKEnd-1661433194407)(images/seata2.png)]](https://1000bd.com/contentImg/2023/10/25/132530467.png)
维护全局和分支事务的状态,驱动全局事务提交或回滚。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0jY643i7-1661433194408)(images/seata3.png)]](https://1000bd.com/contentImg/2023/10/25/132530369.png)
分布式事务存在两大理论依据:CAP定理和BASE理论。
CAP定理是指在一个分布式系统中Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),最多同时满足其中两个,三者不可兼得。
在分布式系统中所有节点的状态是一样的。
在集群中一部分节点出现故障后,整个集群是否还能响应客户端请求。
以实际效果而言,分区相当于对操作的时限要求。如果系统不能在一定时限内达到数据一致性,就意味着发生了分区的情况,此时就必须在A和C中做选择。
是指Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。 BASE理论是对CAP中一致性和可用性权衡的结果,是基于CAP演化而来的。
BASE理论核心思想:即使无法做到强一致性,每个应用都可以根据自身业务特点,采用适当的方式达到最终一致性。
是指在分布式系统中出现不可知故障的时候,允许损失部分可用性。此处要注意:损失部分可用性,不代表整个系统不可用。
例如:
是指系统中数据允许存在中间状态(软状态),并认为这个状态是不影响系统的可用性的。通俗解释:允许分布式节点之间存在同步延迟。
例如:
允许整个系统中数据在经过一定时间后,最终能达到整个系统的一致性。但是这个时间绝对不可以过长。
强一致性要求系统接收请求后,整个系统必须达到一致性效果,才会响应结果。
最终一致性是弱一致性的特例。满足最终一致性的系统在响应给用户结果时整个系统可能是没有达到一致性的,但是最终一定会达到一致性效果的。
总体是基于二阶段提交协议演变而来。
两个全局事务tx1和tx2,分别对a表的m字段进行更新操作,m的初始值1000。
tx1先开始,开启本地事务,拿到本地锁,更新操作m=1000-100=900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。tx2后开始,开启本地事务,拿到本地锁,更新操作m=900-100=800。本地事务提交前,尝试拿该记录的全局锁,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o3NxPgLe-1661433194410)(images/seata4.png)]](https://1000bd.com/contentImg/2023/10/25/132530228.png)
tx1二阶段全局提交,释放全局锁。tx2拿到全局锁提交本地事务。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oj9jeXph-1661433194411)(images/seata5.png)]](https://1000bd.com/contentImg/2023/10/25/132530585.png)
如果tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果tx2仍在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支的回滚会一直重试,直到tx2的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
因为整个过程全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写的问题。
在数据库本地事务隔离级别读已提交(ReadCommitted)或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交(ReadUncommitted)。
如果应用在特定场景下,必需要求全局的读已提交,目前Seata的方式是通过SELECT FOR UPDATE语句的代理。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCkqHL4P-1661433194412)(images/seata6.png)]](https://1000bd.com/contentImg/2023/10/25/132530318.png)
SELECTFORUPDATE语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚SELECTFORUPDATE语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
出于总体性能上的考虑,Seata目前的方案并没有对所有SELECT语句都进行代理,仅针对FORUPDATE的SELECT语句。
以一个具体示例来说明整个 AT 分支的工作过程。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yykTCfpS-1661433194413)(images/seata7.png)]](https://1000bd.com/contentImg/2023/10/25/132530591.png)
AT分支事务要执行的SQL:
update product set name = 'GTS' where name = 'TXC';
过程:
select id, name, since from product where name = 'TXC';
得到前镜像数据:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0X7CYNM-1661433194414)(images/seata8.png)]](https://1000bd.com/contentImg/2023/10/25/132530468.png)
3. 执行业务 SQL:更新这条记录的 name 为 'GTS'。
4. 查询后镜像:根据前镜像的结果,通过主键定位数据。
select id, name, since from product where id = 1;
得到后镜像:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-II2Srefl-1661433194416)(images/seata9.png)]](https://1000bd.com/contentImg/2023/10/25/132530583.png)
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"
}
update product set name = 'TXC' where id = 1;
UNDO_LOG Table:不同数据库在类型上会略有差别,不同的版本在表格字段上会略有差别,最终以官方源码为准。
以 MySQL 为例:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3I7AZI7-1661433194417)(images/seata10.png)]](https://1000bd.com/contentImg/2023/10/25/132530237.png)
TCC适用于任何需要做分布式事务的场景。
由于事务回滚和事务提交的逻辑需要由程序员编写,所以适用AT模式的事务交给AT进行处理,其他不支持ACID或不支持JDBC连接的数据库(数据存储工具)可以使用TCC模式。
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r0DHGLhH-1661433194418)(images/seata11.png)]](https://1000bd.com/contentImg/2023/10/25/132530235.png)
TCC 模式,不依赖于底层数据资源的事务支持:
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
长事务:运行时间比较长,长时间未提交的事务,也可以称之为大事务。这类事务往往会造成大量的阻塞和锁超时,容易造成主从延迟,要尽量避免使用长事务。
理论基础:Hector & Kenneth 发表论⽂ Sagas (1987)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQLGvBeF-1661433194419)(images/seata12.png)]](https://1000bd.com/contentImg/2023/10/25/132530224.png)
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EO9KVVi0-1661433194420)(images/seata13.png)]](https://1000bd.com/contentImg/2023/10/25/132530292.png)
可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
分支提交:执行 XA 分支的 commit
分支回滚:执行 XA 分支的 rollback
XA模式和AT模式代码实现可以说几乎是一样的。唯一的区别是在代码中需要配置数据源代理。
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
docker pull seataio/seata-server:1.4.2
Seata Server必须连接一个MySQL数据库来保存分布式事务管理过程中的数据。
在MySQL中创建数据库Database。命名为seata(任意命名)。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PWd8Lvxj-1661433194421)(images/seata14.png)]](https://1000bd.com/contentImg/2023/10/25/132530610.png)
执行下述脚本,创建表格。
建议根据具体版本,从官方源码中找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;
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
docker exec -it seata sh
cd /seata-server/resources
此配置文件用于配置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 ..