1. 概述
Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,为用户提供AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案
Seata主要由三个重要组件组成:
TC:Transaction Coordinator事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚
TM:Transaction Manager事务管理器,用于开启、提交或者回滚全局事务
RM:Resource Manager资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务
Seata执行流程如下:
- A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
- A服务的RM向TC注册分支事务,并将其纳入XID对应全局事务的管辖
- A服务执行分支事务,向数据库做操作
- A服务开始远程调用B服务,此时XID会在微服务的调用链上传播
- B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
- B服务执行分支事务,向数据库做操作
- 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
- TC协调其管辖之下的所有分支事务,决定是否回滚
Seata实现2PC与传统2PC的差别:
- 架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而 Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的。
- 两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率
2. 分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
分布式事务场景
- 单体系统访问多个数据库,一个服务需要调用多个数据库实例完成数据的增删改操作
- 多个微服务访问同一个数据库,多个服务需要调用一个数据库实例完成数据的增删改操作
- 多个微服务访问多个数据库,多个服务需要调用一个数据库实例完成数据的增删改操作
3. 分布式事务解决方案
3.1. 全局事务
基于DTP模型实现,DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model,要实现分布式事务,需要三种角色
AP:Application应用系统(微服务)
TM:Transaction Manager事务管理器(全局事务管理)
RM:Resource Manager资源管理器(数据库)
整个事务分成两个阶段
阶段一:表决阶段,所有参与者都将本事务执行预提交,并将能否成功的信息反馈发给协调者
阶段二:执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚
优点:
提高了数据一致性的概率,实现成本较低
缺点:
单点问题,事务协调者宕机
同步阻塞,延迟了提交时间,加长了资源阻塞时间
数据不一致,提交第二阶段,依然存在commit结果未知的情况,有可能导致数据不一致
3.2. 可靠消息服务
基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。假设有A和B两个系统,分别可以处理任务A和任务B。此时存在一个业务流程,需要将任务A和任务B在同一个事务中处理。就可以使用消息中间件来实现这种分布式事务。
第一步:消息由系统A投递到中间件
- 在系统A处理任务A前,首先向消息中间件发送一条消息
- 消息中间件收到后将该条消息持久化,但并不投递。持久化成功后,向A回复一个确认应答
- 系统A收到确认应答后,则可以开始处理任务A
- 任务A处理完成后,向消息中间件发送Commit或者Rollback请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了
- 如果消息中间件收到Commit,则向B系统投递消息;如果收到Rollback,则直接丢弃消息。但是如果消息中间件收不到Commit和Rollback指令,那么就要依靠”超时询问机制”。
超时询问机制
系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到发布消息便开始计时,如果到了超时没收到确认指令,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果,中间件根据三种结果做出不同反应:
- 提交:将该消息投递给系统B
- 回滚:直接将条消息丢弃
- 处理中:继续等待
第二步:消息由中间件投递到系统B
消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。
- 如果消息中间件收到确认应答后便认为该事务处理完毕
- 如果消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止
一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干预。这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂度问题。
基于可靠消息服务的分布式事务,前半部分使用异步,注重性能;后半部分使用同步,注重开发成本。
3.3. 最大努力通知
最大努力通知也被称为定期校对,其实是对第二种解决方案的进一步优化。它引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息会被下游系统消费。
第一步:消息由系统A投递到中间件
- 处理业务的同一事务中,向本地消息表中写入一条记录
- 准备专门的消息发送者不断地发送本地消息表中的消息到消息中间件,如果发送失败则重试
第二步:消息由中间件投递到系统B
- 消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行
- 当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该
事务完成 - 对于投递失败的消息,利用重试机制进行重试,对于重试失败的,写入错误消息表
- 消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费
优点:
一种非常经典的实现,实现了最终一致性。
缺点:
消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
3.4. TCC事务
TCC即为Try Confifirm Cancel,它属于补偿型分布式事务。TCC实现分布式事务一共有三个步骤:
Try: 尝试待执行的业务:这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
Confifirm: 确认执行业务:确认执行业务操作,不做任何业务检查, 只使用Try阶段预留的业务资源。通常情况下,采用TCC则认为 Confifirm阶段是不会出错的。即:只要Try成功,Confifirm一定成功。若Confifirm阶段真的出错了,需引入重试机制或人工处理。
Cancel: 取消待执行的业务:取消Try阶段预留的业务资源。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理
TCC两阶段提交与XA两阶段提交的区别:
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁
优点:
把数据库层的二阶段提交上提到了应用层来实现,规避了数据库层的2PC性能低下问题。
缺点:
TCC的Try、Confifirm和Cancel操作功能需业务提供,开发成本高。
4. Seata实现分布式事务控制
本文通过模拟电商中下单时扣减账户余额和扣减库存的过程实现分布式事务,需要提前新建3个数据库和3个微服务
4.1. 数据库准备
seata-account库
CREATE TABLE `account_info` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NULL DEFAULT NULL COMMENT '用户ID',
`total` decimal(10, 2) NULL DEFAULT NULL COMMENT '总额',
`used` decimal(10, 2) NULL DEFAULT NULL COMMENT '已用额度',
`residue` decimal(10, 2) NULL DEFAULT NULL COMMENT '剩余额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
seata-order库
CREATE TABLE `order_info` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NULL DEFAULT NULL COMMENT '用户ID',
`product_id` bigint(20) NULL DEFAULT NULL COMMENT '编码',
`count` int(11) NULL DEFAULT NULL COMMENT '数量',
`money` decimal(10, 2) NULL DEFAULT NULL COMMENT '金额',
`status` tinyint(1) NULL DEFAULT NULL COMMENT '状态 0:创建中 1:已创建',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单信息表' ROW_FORMAT = Dynamic;
seata-storage库
CREATE TABLE `storage_info` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`product_id` bigint(20) NOT NULL COMMENT '编码',
`total` int(11) NULL DEFAULT NULL COMMENT '总数',
`used` int(11) NULL DEFAULT NULL COMMENT '已用数量',
`residue` int(11) NULL DEFAULT NULL COMMENT '剩余数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_product_id`(`product_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存信息表' ROW_FORMAT = Dynamic;
在每个库中新建日志表
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;
4.2. Seata服务端安装
4.2.1. 下载安装包
从官网下载最新稳定版服务端安装包,目前最新版本为1.4.2
下载Seata配置文件,将其放在seata-server目录下
4.2.2. 新建数据库表
新建seata数据库,建表语句可以从官网获得,根据数据库类型选择对应sql
4.2.3. 修改配置信息
打开conf目录下file.conf文件
打开conf目录下registry.conf文件,需要修改registry区域和config区域两部分
config信息更改如下
打开script/config-center目录下config.txt文件
方便后面测试所用,添加如下事务group
service.vgroupMapping.seata_account_tx_group=default
service.vgroupMapping.seata_order_tx_group=default
service.vgroupMapping.seata_storage_tx_group=default
找到db区域,修改数据库连接信息
4.2.4. 修改脚本文件
打开script/config-center/nacos目录下nacos-config.sh文件,修改nacos地址为自己的
在此目录下使用Git Bash执行脚本nacos-config.sh,将配置信息同步到nacos配置中心
打开bin目录,根据不同系统执行.bat或.sh文件启动seata-server
5. SpringBoot整合Seata
新建3个springboot项目
alibaba-seata-account、alibaba-seata-order和alibaba-seata-storage
5.1. 引入核心依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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-seata</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
order微服务需要通过openfeign调用account和storage微服务,还需要引入openfeign依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
5.2. application.yml配置
下面以storage微服务为例,其他两个微服务只需要更改端口、应用名称、数据库名称和事务group即可
server:
port: 8402
spring:
application:
name: alibaba-seata-storage
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/seata-storage?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
max-active: 20
min-idle: 2
initial-size: 2
cloud:
nacos:
discovery:
server-addr: localhost:8848
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: seata_storage_tx_group
enable-auto-data-source-proxy: true
service:
vgroup-mapping:
seata_storage_tx_group: default
grouplist:
default: localhost:8091
disable-global-transaction: false
registry:
type: nacos
nacos:
application: seata-server
cluster: default
server-addr: localhost
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: localhost
data-id: seata.properties
username: nacos
password: nacos
mybatis:
mapper-locations: classpath:mapper/*.xml
5.3. 数据源配置类
Seata是通过代理数据源实现事务分支,所以需要配置数据源代理
@Configuration
public class DataSourceProxyConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
5.4. Order业务实现类
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageApiClient storageApiClient;
@Autowired
private AccountApiClient accountApiClient;
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(OrderInfo orderInfo) {
orderMapper.create(orderInfo);
storageApiClient.decrease(orderInfo.getProductId(), orderInfo.getCount());
accountApiClient.decrease(orderInfo.getUserId(), orderInfo.getMoney());
orderMapper.update(orderInfo.getUserId(), 0);
}
}
5.4. 验证
依次启动Account、Storage和Order微服务,浏览器地址输入http://localhost:8403/order/create,请求报文如下:
{
"userId": 1,
"productId": 1,
"count": 1,
"money": 50,
"status": 0
}
在执行过程中会先在undo_log表中暂存,查看数据库undo_log表会看到日志记录,执行完成后表中数据会被删除,若执行过程中发生异常,前面已执行过的SQL会回滚
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76760.html