常见的分布式事务解决方案:
1、seata阿里分布式事务框架
2、消息队列
3、saga
4、XA
他们都有一个共同点,都是”两阶段(2PC)”。两阶段是指完成整个分布式事务,划分成两个步骤完成,实际上这四种常见的分布式事务解决方案,分别对应着分布式事务的四种模式:AT、TCC、Saga、XA;四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出;每种模式都有他的适用场景,同样每个模式也都诞生有各自的代表产品,而这些代表产品,可能就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC)。
在看具体实现之前,先说下分布式事务的理论基础。
分布式事务理论基础
解决分布式事务,也有相应的规范和协议。分布式事务相关的协议有2PC、3PC。
由于三阶段提交协议3PC非常难实现,目前市面主流的分布式事务解决方案都是2PC协议。
有些文章分析2PC时,几乎都会用TCC两阶段的例子,第一阶段try,第二阶段完成confirm或cancel。其实2PC并不是专为实现TCC设计的,2PC具有普适性,目前绝大多数分布式事务解决方案都是以两阶段提交协议2PC为基础的。
两阶段提交协议
顾名思义,分为两个阶段:Prepare和Commit。
AT模式(Auto Tran
saction
)
AT模式是一种
无侵入
的分布式事务解决方案。
阿里seata框架,实现了该模式。
在AT模式下,用户只需关注自己的业务sql,用户的业务sql作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。
AT模式如何做到对业务的无侵入:
一阶段:在一阶段,Seata会拦截业务sql,首先解析sql语义,找到业务sql要更新的业务数据,在业务数据被更新之前,将其保存成before image,然后执行业务sql更新业务数据,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交:二阶段如果是提交的话,因为业务sql在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删除,完成数据清理即可。
二阶段回滚:二阶段如果是回滚的话,Seata就需要回滚一阶段已执行的业务sql,还原业务数据。回滚方式便是用before image 还原业务数据,但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据。如果不一致,就说明有脏写,出现脏写就需要转人工处理。
AT模式的一阶段,二阶段提交和回滚都是由Seata框架自动生成,用户只需编写业务sql,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案。
Seata
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)。
官网:
Seata
源码:
http://github.com/seata/seata
官方Demo:
GitHub – seata/seata-samples: seata-samples
Seata的三大角色
在Seata的架构中,一共有三大角色:
TC(Transaction Coordinator)- 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM(Transaction Manager)- 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM(Resource Manager)- 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC为单独部署的Server服务端,TM和RM为嵌入到应用中的Client客户端。
在Seata中,一个分布式事务的生命周期如下:
-
TM请求TC开启一个全局事务。TC会生成一个XID作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
-
RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联。
-
TM请求TC告诉XID对应的全局事务是进行提交还是回滚。
-
TC驱动RM们将XID对应的自己的本地事务进行提交还是回滚。
设计亮点
相比其他分布式事务框架,Seata架构的亮点主要有几个:
-
应用层基于sql解析实现了自动补偿,从而最大程度的降低业务侵入性。
-
将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚。
-
通过全局锁实现了写隔离和读隔离。
存在的问题
性能损耗
一条update的sql,则需要全局事务xid获取(与TC通讯)、before image(解析sql,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要运行一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写sql都会增加这么多开销,粗略估计会增加5倍响应时间。
性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个成功率是有多高,或者说分布式事务失败需要回滚的有多少比例?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
全局锁
Seata快速开始
Seata Server(TC)环境搭建
:
Server端存储模式(store.mode)支持三种
:
file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)
db:高可用模式,全局事务会话信息通过db共享,相应性能差些
redis:Seata-Server1.3及以上版本支持,性能较高。存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置
我们使用DB模式,是将数据存在数据库中。
db存储模式+Nacos高可用集群部署
步骤一:下载安装包
步骤二:建表(仅db)
创建数据库seata-server
打开官网,找到资源目录
,
或者使用下面的文件:
-- -------------------------------- 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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- 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 = utf8mb4;
-- 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),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
全局事务会话信息由3块内容构成,全局事务–>分支事务–>全局锁,对应表global_table、branch_table、lock_table。
步骤三:修改
conf中file.conf中的
store.mode
属性,
修改数据库连接
步骤
四
:修改Server端配置注册中心
步骤五:
修改Server端配置中心
同样在registry.conf文件中修改注册中心为nacos
步骤
六
:下载seata服务端源码
https://github.com/seata/seata/archive/refs/tags/v1.4.0.zip
步骤
七
: 修改config.txt文件
发现默认的数据源时file
步骤
八
:
配置事务分组
配置事务分组,要与客户端配置的事务分组一致。
对应的客户端配置
#事务分组配置
seata.tx-service-group=my_test_tx_group
#指定事务分组至集群映射关系(等号右侧的集群名需要与Seata-server注册到Nacos的cluster保持一致)
seata.service.vgroup-mapping.my_test_tx_group=default
步骤
九
: 将config.txt文件中的配置导入到Nacos配置中心
打开
seata\seata-1.4.0\seata-1.4.0\script\config-center\nacos
文件夹
运行nacos-config.sh,前提1、先运行Nacos;2、装了Git
步骤十:启动SeataServer
启动Seata Server,双击seata-server.bat
Seata Client
(项目代码)
环境搭建
-
启动seata server服务端,seata server使用nacos作为注册中心和配置中心(上一步已完成)
-
微服务整合seata
-
添加pom依赖
<!–seata–>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
-
各微服务对应数据库中添加undo_log表
-
在配置文件修改事务分组
-
在application.yml文件中配置seata 的注册中心和配置中心
spring: datasource: url: jdbc:mysql://localhost:3306/tw?characterEncoding=UTF-8&useSSL=false&useUnicode=true&serverTimezone=UTC username:username password: password driver-class-name: com.mysql.jdbc.Driver profiles: # 当前环境,在真实项目中,一般分为多个环境,dev为开发环境 active: dev application: name: 项目名 cloud: alibaba: seata: #配置事务分组 tx-service-group: my_test_tx_group #注册中心 registry: #配置seata的注册中心为nacos,告诉seata client怎么去访问seata server type: nacos nacos: #seata server的注册地址 server-addr: localhost:8848 #seata server的服务名,默认:seata-server,若没有修改则可以不配置 application: seata-server username: username password: password #seata server所在的组,默认:SEATA_GROUP,若没有修改则可以不配置 group: SEATA_GROUP #配置中心 config: #配置seata的配置中心为nacos type: nacos nacos: #seata server的注册地址 server-addr: localhost:8848 #seata server所在的组,默认:SEATA_GROUP,若没有修改则可以不配置 group: SEATA_GROUP username: nacos password: nacos nacos: config: # 注册中心地址 server-addr: localhost:8848 # 配置文件后缀,即配置文件格式 file-extension: yaml # 命名空间,在后续nacos配置中会出现该参数是如何获取的 namespace: dcda1c6d-95b3-4581-96de-cc70583f228f //nacos命名空间 discovery: # 注册中心地址 server-addr: localhost:8848 #关闭驼峰 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #mybatis mybatis: type-aliases-package: com.tm.pojo #所有pojo类所在包的路径 mapper-locations: classpath:mapper/*.xml #mapper映射文件 configuration: map-underscore-to-camel-case: true #支持驼峰映射
-
在相应的服务
中,要远程调用的方法上
添加@GlobalTransactional注解
注:如果出现
就将所有配置文件中的127.0.0.1改为localhost即可,我改过,所以有问题,修改为localhost就好了
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
子项目:
<dependency>
<groupId>
com.alibaba.cloud
</groupId>
<artifactId>
spring-cloud-starter-alibaba-seata
</artifactId>
</dependency>