Seata-分布式事务解决方案
第一节:分布式事务
1.介绍:
在分布式系统中,每个微服务有多个相同的节点集群,相应的功能也被拆分了多个部分,比如下单功能,他的流程可能是
用户点击下单->跳转订单服务->进入支付服务->获取支付结果->进入商品库存服务扣减库存->下单成功
就像上面的流程,一个下单功能可能涉及到订单服务,支付服务,商品管理服务,等多个服务.由于这些服务都是分开部署,操作的数据库也不是相同的数据库(分库分表情况).
这时,原本的事务就不适用了,一旦其中一个失败或者说,发生并发情况,如:一个商品只有一个库存,同时有多个用户下单,这时如果使用默认事务,逻辑一般对应者串行化,除非进行锁操作,否则在mysql默认的事务情况下,可能会发生超卖的情况.或者说你实现乐观锁.
这时候,就需要一个专门的服务管理全局的事务,他的作用就是保证所有操作要么成功要么失败,不会有一部分成功的情况.同时,他也要保证分布式系统某个服务崩溃,这时就会导致数据丢失,那次事务就算在崩溃的服务重启后也无法找回数据的情况下,可以保证系统数据正确也就是保存丢失的那部分数据.
seata就是阿里开源的分布式框架,解决分布式事务问题.
2.原则
CAP介绍:
CAP原则又叫CAP定理,同时又被称作布鲁尔定理(Brewer’s theorem),指的是在一个分布式系统中,不可能同时满足以下三点
-
一致性(Consistency): 在分布式事务中,保证每个修改后的结果,都实时同步到每个节点中,保证用户获取的数据都是最新的.
-
可用性(Availability): 保证在每个请求在向未崩溃的服务发送请求时都能获取到成功的结果(可以正常响应)
-
分区容忍性(Partition tolerance):微服务集群情况下,集群中单个节点崩溃,不影响整个服务的正常使用,只要在这个微服务集群没有全部崩溃情况下,可以保证这个服务可以继续使用
意思就是只能同时满足:
CA:每个请求都可以获取最新的结果
PA:每个请求可以正常获取结果
CP:满足原子和分区容错,也就是说,要放弃可用。当系统被分区,为了保证原子性,必须放弃可用性,让服务停用。
为什么不能同时满足CAP?
C原则要求最新的数据必须所有的服务同时更新,P原则要求在一个微服务集群没有全部崩溃的情况下,保证这个微服务可用.显然CP这两个原则在都有可用性的情况下是互斥的,在一个服务与其他服务连接断开后新修改的数据无法更新到所有的服务中,但是这个服务是可用的请求请求数据不是全部都是全新的,违反C原则这时,除非整个微服务集群全部等待修复,否则不能使用这个微服务集群;P原则确是一个服务节点有问题不影响整个功能使用,所以不可能满足所有的CAP
选择居中的解决方案
这里首先介绍一致性:
-
强一致性:数据更新后,所有请求获取的数据都是最新的
-
弱一致性:数据更新后,所有请求都会趋向于最新的数据
所以我们可以舍弃强一致性,而使用弱一致性
这样满足低配版CAP
3.分布式事务协议
3.1二阶段提交:
二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理.
-
准备阶段:进行所有业务操作
-
提交阶段:当所有操作全部成功,或者有一个失败时进行
第一阶段:
-
向所有需要进行操作的服务,发送事务操作内容
-
记录操作前的数据形成回滚日志,和正在进行的事务操作形成事务日志放入事务日志中
-
如果全部操作都成功,发送给所有参与的服务"成功",否则发送"失败"
第一阶段基本进行所有操作获取每个服务事务的执行情况,然后进行决定这次事务是提交还是回滚.
第二阶段:
-
如果全部成功就发送成功消息,通知所有设及本次事务操作的服务进行提交操作,失败就全部进行回滚操作
-
成功情况下,参与事务的服务在完成操作后,释放事务资源.失败的话,根据阶段一写入的回滚日志,进行回滚
-
参与事务发送ack消息(就是完成操作消息)给分布式事务管理服务.
-
分布式事务服务在一定时间收到所有ack消息后进行释放全局锁
缺陷
- 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
- 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
3.2三阶段提交
三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。
- 在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
第一阶段(cancommit): 询问参与服务是否可以进行事务操作(也算是一种校验阶段,判断本次操作是否可以正常进行)
3PC的CanCommit
阶段其实和2PC的准备阶段很像。协调者向参与者发送commit
请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- 事务询问 协调者向所有参与者发出包含事务内容的
canCommit
请求,询问是否可以提交事务,并等待所有参与者答复。 - 响应反馈 参与者收到
canCommit
请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
第二阶段PreCommit:
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit
操作。根据响应情况,有以下两种可能。
假如所有参与者均反馈 yes,协调者预执行事务。
- 发送预提交请求 :协调者向参与者发送
PreCommit
请求,并进入准备阶段 - 事务预提交 :参与者接收到
PreCommit
请求后,会执行事务操作,并将undo
和redo
信息记录到事务日志中(但不提交事务) - 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
- 发送中断请求 :协调者向所有参与者发送
abort
请求。 - 中断事务 :参与者收到来自协调者的
abort
请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
第三阶段(doCommit) 该阶段进行真正的事务提交,也可以分为以下两种情况。
执行成功:
- 在收到参与服务器ack消息后,分布式管理服务发送真正的提交指令
- 在参与服务收到提交指令后,进行事务提交,并释放事务资源
- 参与事务提交成功后发送成功ack消息给分布式事务管理服务成功消息
- 分布式事务管理服务收到成功消息后进行释放全局锁
执行失败:
任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务(执行失败)
- 发送中断请求 如果协调者处于工作状态,向所有参与者发出 abort 请求
- 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
- 反馈结果 参与者完成事务回滚之后,向协调者反馈ACK消息
- 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
注意:
这里,在分布式管理服务崩溃时,默认会提交事务,因为cancommit阶段,所有的事务都认为可以执行成功给分布式管理服务发送的都是yes,所以默认提交.
**优点:**相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。简单的说就是完善了二阶段协议在分布式服务自身出现问题后,所有依赖他服务都会卡死,不释放资源问题,依赖的服务可以自身处理一些事情.
缺点:数据不一致问题依然存在,当在参与者收到 preCommit
请求后等待 doCommit
指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。意思就是比如我参与者有一些接收到了提交指令,然后还有一些参与者没收到,那么数据就会不一致
4.分布式事务解决方案分布式事务解决方案
- TCC
- 全局消息
- 基于可靠消息服务的分布式事务
- 最大努力通知
事务补偿(TCC)
TCC方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作
-
第一阶段
Try(尝试):主要是对业务系统做检测及资源预留 (加锁,锁住资源)
-
第二阶段
本阶段根据第一阶段的结果,决定是执行confirm还是cancel
Confirm(确认):执行真正的业务执行业务,释放锁
Cancle(取消):是预留资源的取消出问题,释放锁
意思就是try阶段先进行事务内容执行,前置条件执行,前置条件执行成功就成功,否则失败,如果成功confirm,否则cancle
在try阶段成功,那么我们认为一定可以comfirm成功,因为内容是一致的,这中间库存会一直锁住,不给其他服务使用
所以我们认为这种补偿机制可行
优点:
-
从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
-
方案轻量,容易实现。
缺点:
- 与具体的业务场景绑定,耦合性强,不可公用。
- 消息数据与业务数据同库,占用业务系统资源。
- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
总结:
TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:
- 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群
但是TCC对于不同的业务需要进行不同的设计,如商品库存,表字段添加冻结字段,try阶段进行冻结库存数增加,也就是tcc是通过业务设计实现,无法进行通用
本地消息表
本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免”业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证 2 个系统事务的数据一致性。
后续待定…
第二节:Seata
1.介绍Seata
阿里分布式事务解决方案中间件支持常见的分布式解决方案如:AT,TCC,XA等
seata主要有三个组件:TM(事务管理),TC(事务协调),RM(分支事务)
他主要依赖回滚日志表,这些表都需要在涉及全局事务操作的数据库,自己手动创建,然后在seata的配置文件中配置相应内容,
表:unlog
UNDO_LOG
必须在每个业务数据库中创建,用于保存回滚操作数据- 当全局提交时,
UNDO_LOG
记录直接删除 - 当全局回滚时,将现有数据撤销,还原至操作前的状态
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2.AT模式详解
AT模式运行机制
AT模式的特点就是对业务无入侵式,整体机制分二阶段提交
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
Seata具体实现步骤
- TM端使用
@GlobalTransaction
进行全局事务开启、提交、回滚 - TM开始RPC调用远程服务
- RM端
seata-client
通过扩展DataSourceProxy
,实现自动生成UNDO_LOG
与TC
上报 - TM告知TC提交/回滚全局事务
- TC通知RM各自执行
commit/rollback
操作,同时清除undo_log
seata配置
下载中心 (seata.io)seata下载地址
seata有三个配置文件需要进行配置
1.conf/file.conf
将mode="file"
改为mode="db"
db部分配置mysql相关信息
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `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`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
注:1.0版本后没有sql脚本
2.修改registry.conf
文件
这个文件内容分为registry
和config
2部分,为了方便,我们使用之前学过的nacos
注册中心,你也可以采用默认的file
方式
- 将
registry
和config
部分的由type="file"
都换成type="nacos"
(注意是2处地方) - 将2处的
nacos
配置根据实际情况需要调整参数,本机启动nacos
则不需要调整
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5kvKUsN-1627883488569)(C:\Users\Administrator\Desktop\md\asset\1601305025041.0d30a885.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EDSEWNRD-1627883488569)(C:\Users\Administrator\Desktop\md\asset\1601305065064.ef45e7fa.png)]
nacos {
application = "seata-server"
serverAddr = "nacos.it235.com:80"
group = "SEATA_GROUP"
namespace = "343f2aa2-1a42-43ea-b078-33ab7d58bd6a"
cluster = "default"
username = "nacos"
password = "nacos"
}
4.修改conf/logback.xml
的文件(可选操作)
将${user.home}
改为具体的seata
目录,我这里是F:\hliedu\cloud\seata-server-1.3.0\seata
,那么配置如下
<property name="LOG_HOME" value="F:\hliedu\cloud\seata-server-1.3.0\seata\logs"/>
代码编写
代码逻辑就是使用springcloud Feign 调用,然后调用的方法上添加@GloblaTroncatopnal注解,feign调用的方法需要有事务注解
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
......
<properties>
<springboot.verison>2.4.2.RELEASE</springboot.verison>
<java.version>1.8</java.version>
<mybatis.version>2.1.5</mybatis.version>
<tk-mapper.version>4.1.5</tk-mapper.version>
<seata.version>1.3.0</seata.version>
</properties>
......
<!--父模块中添加依赖-->
<dependencyManagement>
<dependencies>
<!--Mybatis通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${tk-mapper.version}</version>
</dependency>
<!--SpringCloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring Alibaba Cloud-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--子模块order-service和storage-service的pom中添加nacos和seata依赖-->
<dependencies>
<!--nacos注册中心和配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--移除掉该starter中自带的依赖,该依赖版本较低-->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--单独添加seata 1.3.0的依赖-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>10.2.3</version>
</dependency>
<!--Mybatis通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
server:
port: 6770
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: seata_test
password: 'seata1234abcd!'
url: jdbc:mysql://rm-bp17dq6iz79761b8fxo.mysql.rds.aliyuncs.com:3306/it235_order?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
cloud:
nacos:
discovery:
server-addr: nacos.it235.com:80
register-enabled: true
namespace: f46bbdaa-f11e-414f-9530-e6a18cbf91f6
config:
server-addr: nacos.it235.com:80
enabled: true
file-extension: yaml
namespace: f46bbdaa-f11e-414f-9530-e6a18cbf91f6
seata:
enabled: true
application-id: ${spring.application.name}
# 事务群组(可以每个应用独立取名,也可以使用相同的名字),要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应
tx-service-group: ${spring.application.name}-tx-group
config:
type: nacos
# 需要和server在同一个注册中心下
nacos:
namespace: f46bbdaa-f11e-414f-9530-e6a18cbf91f6
serverAddr: nacos.it235.com:80
# 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
username: "nacos"
password: "nacos"
registry:
type: nacos
nacos:
# 需要和server端保持一致,即server在nacos中的名称,默认为seata-server
application: seata-server
server-addr: nacos.it235.com:80
group: SEATA_GROUP
namespace: f46bbdaa-f11e-414f-9530-e6a18cbf91f6
username: "nacos"
password: "nacos"
mybatis:
mapperLocations: classpath:mapper/*.xml