SpringCloud之Seata
1.Seata是什么?
1.1 概念:Seata 是一款开源的分布式事务解决方案,提供高性能和简单易用的分布式事务服务。
1.2 术语
(1)TC: 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
(2)TM:事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
(3)RM:资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
1.3 工作图
1.2 事务模式
(1)AT 模式
1.1.1 无侵入式的分布式事务解决方案,用户只需关注自己作为一阶段的业务SQL,Seata框架会自动生成事务进行二阶段提交和回滚操作。
1.1.2 2个阶段
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
1.1.3 以订单服务为例
图参考
(2)TCC 模式
1.2.1 TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:
Try:对业务资源的检查并预留;
Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。
Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。
1.2.2 TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。
1.2.3 TCC和AT区别
AT 模式基于 支持本地 ACID 事务 的 关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
TCC 模式,不依赖于底层数据资源的事务支持:
一阶段 prepare 行为:调用自定义 的 prepare 逻辑。
二阶段 commit 行为:调用自定义 的 commit 逻辑。
二阶段 rollback 行为:调用自定义 的 rollback 逻辑。
(3)Saga 模式
1.3.1 Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
1.3.2
1.3.3 比如,老系统,封闭的系统(无法修改,同时没有任何分布式事务引入)。或者事务参与者可能是其他公司的服务或者是遗留系统,无法改造。那么AT、XA、TCC模型将全部不能使用,这时就可以使用Saga模式。
1.3.4 Saga模式提供了异构系统的事务统一处理模型。所有的子业务都不在直接参与整体事务的处理(只负责本地事务的处理),而是全部交由了最终调用端来负责实现。而在进行总业务逻辑处理时,在某一个子业务出现问题时,则自动补偿全面已经成功的其他参与者,这样一阶段的正向服务调用和二阶段的服务补偿处理全部由总业务开发实现。
1.3.5 Saga状态机
目前Seata提供的Saga模式只能通过状态机引擎来实现,需要开发者手工的进行Saga业务流程绘制,并且将其转换为Json配置文件,而后在程序运行时,将依据子配置文件实现业务处理以及服务补偿处理,而要想进行Saga状态图的绘制,一般需要通过Saga状态机来实现。
1.3.6 状态机基本原理:
通过状态图来定义服务调用的流程并生成json定义文件
状态图中一个节点可以调用一个服务,节点可以配置它的补偿节点
状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
(3)XA 模式
1.4.1 XA模型用于解决分布式事务领域的问题,是最早的分布式事务处理方案,因为需要数据库内部也是支持XA模式的,比如MYSQL,XA模式具有强一致性的特点,因此他对数据库占用时间比较长,所以性能比较低。
1.4.2 XA模式属于两阶段提交。
第一阶段进行事务注册,将事务注册到TC中,执行SQL语句。
第二阶段TC判断无事务出错,通知所有事务提交,否则回滚。
在第一到第二阶段过程中,事务一直占有数据库锁,因此性能比较低,但是所有事务要么一起提交,要么一起回滚,所以能实现强一致性。
无论是AT模式、TCC还是SAGA,这些模式的提出,都是源于XA规范对某些业务场景无法满足
1.4.3 XA规范
1.4.3.1 描述了全局事务管理器和局部资源管理器之间的接口,XA规范的目的是允许多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。
1.4.3.2 XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。几乎所有的主流数据库都保有对XA规范的支持。
1.4.3.3 分布式事务DTP模型定义的角色如下:
AP:即应用程序,可以理解为使用DTP分布式事务的程序,例如订单服务、库存服务
RM:资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库的实例(MySql),通过资源管理器对该数据库进行控制,资源管理器控制着分支事务
TM:事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理实务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
1.4.3.4 DTP模式定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现的2PC又称为XA方案。
1.4.3.5 现在有应用程序(AP)持有订单库和库存库,应用程序(AP)通过TM通知订单库(RM)和库存库(RM),进行扣减库存和生成订单,这个时候RM并没有提交事务,而且锁定资源。
当TM收到执行消息,如果有一方RM执行失败,分别向其他RM也发送回滚事务,回滚完毕,释放锁资源
当TM收到执行消息,RM全部成功,向所有RM发起提交事务,提交完毕,释放锁资源。
1.4.3.6 分布式通信协议XA规范,具体执行流程如下所示:
第一步:AP创建了RM1,RM2的JDBC连接。
第二步:AP通知生成全局事物ID,并把RM1,RM2注册到全局事务ID
第三步:执行二阶段协议中的第一阶段prepare
第四步:根据prepare请求,决定整体提交或回滚。
1.4.3.7 但是对于XA而言,如果一个参与全局事务的资源“失联”了,那么就意味着TM收不到分支事务结束的命令,那么它锁定的数据,将会一直被锁定,从而产生死锁,这个也是Seata需要重点解决的问题。
1.4.2 在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
执行阶段:
可回滚:业务SQL操作在XA分支中进行,有资源管理器对XA协议的支持来保证可回滚
持久化:ZA分支完成以后,执行 XA prepare,同样,由资源对XA协议的支持来保证持久化
完成阶段:
分支提交:执行XA分支的commit
分支回滚:执行XA分支的rollback
1.4.3 XA存在的意义
Seata 已经支持了三大事务模式:AT\TCC\SAGA,这三个都是补偿型事务,补偿型事务处理你机制构建在 事务资源 之上(要么中间件层面,要么应用层),事务资源本身对于分布式的事务是无感知的,这种对于分布式事务的无感知存在有一个根本性的问题,无法做到真正的全局一致性。
例如一个库存记录,在补偿型事务处理过程中,用80扣减为60,这个时候仓库管理员查询数据结果,看到的是60,之后因为异常回滚,库存回滚到原来的80,那么这个时候库存管理员看到的60,其实就是脏数据,而这个中间状态就是补偿型事务存在的脏数据。
和补偿型事务不同,XA协议要求事务资源 本身提供对规范和协议的支持,因为事务资源感知并参与分布式事务处理过程中,所以事务资源可以保证从任意视角对数据的访问有效隔离性,满足全局数据的一致性。
2.Seata如何应用于项目?
安装seata及修改配置
1.1 官网下载Seata安装包
1.2 修改seata/config.txt
1.2.1 修改存储方式
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://你的IP:3306/seata?useUnicode=true
store.db.user=root
store.db.password=root
1.2.2 添加如下配置
service.vgroupMapping.demo-order=default
service.vgroupMapping.demo-storage=default
service.vgroupMapping.demo-account=default
1.3 修改seata/conf/registry.conf
1.3.1 注册中心改为nacos
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "你的IP:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
1.3.2 配置中心也改为nacos
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "你的IP:8848"
namespace = "在nacos中创建一个命名空间,用来保存seata配置,提前配置好"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
1.4 执行nacos脚本,推送seata配置到nacos
sh nacos-config.sh -h 你的nacos -p 8848 -g SEATA_GROUP -t 上面创建的命名空间 -u nacos -w nacos
创建seata需要用到的表
1.1 创建数据库
create database seata;
1.2 创建相关表
global_table: 全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID
branch_table: 分支事务表,记录每一个分支事务的 ID,分支事务操作的哪个数据库等信息
lock_table: 全局锁
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96) DEFAULT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
启动seata服务
sh seata/bin/seata-server.sh 启动成功后,监听8091端口
看下nacos中是否注册了此服务: 看到如下内容表示,seata已经安装并注册成功
项目整合seata
1.1 创建父工程
在ams-demo下创建父工程demo-seata,并修改父工程打包方式为pom
1.2 添加依赖
<dependencies>
<!-- 配置读取 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Cloud & Alibaba -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.ams</groupId>
<artifactId>common-mybatis-plus</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.ams</groupId>
<artifactId>common-base</artifactId>
<version>${ams.version}</version>
</dependency>
<dependency>
<groupId>com.ams</groupId>
<artifactId>common-web</artifactId>
<version>${ams.version}</version>
</dependency>
<!-- openfeign依赖 1. http客户端选择okhttp 2. loadbalancer替换ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!--seata 必备-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
1.3 创建订单子模块(库存、账户子模块类似)
1.3.1 在demo-seata下创建demo-order子模块
1.3.2 添加创建订单的接口
/**
* @description:创建订单控接口
*/
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@RequestMapping("/createOrder")
public R createOrder() {
orderService.createOrder();
return R.ok("订单创建成功");
}
}
1.3.3 添加创建订单的service
/**
* @description:创建订单service
*/
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final AccountFeignService accountFeignService;
private final StorageFeignService storageFeignService;
@Override
@GlobalTransactional(name = "ams-create-order",rollbackFor = Exception.class)
public R createOrder() {
Order order = Order.builder()
.count(10)
.money(100)
.productId(1L)
.status(0)
.userId(1L)
.build();
// 创建订单
save(order);
// 扣除库存
storageFeignService.decrease(order.getProductId(), order.getCount());
// 扣余额
accountFeignService.decrease(order.getUserId(), order.getMoney());
//更新订单状态
order.setStatus(1);
updateById(order);
return R.ok();
}
}
1.3.4 添加调用库存接口的feign
/**
* Description:调用库存接口的feign
*/
@FeignClient(value = "ams-demo-storage")
@RequestMapping("/storage")
public interface StorageFeignService {
@RequestMapping("/decrease")
public R decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
1.3.5 添加调用账户接口的feign
/**
* Description:调用账户接口的feign
*/
@FeignClient(value = "ams-demo-account")
@RequestMapping("/account")
public interface AccountFeignService {
@RequestMapping("/decrease")
public R decrease(@RequestParam("userId") Long userId, @RequestParam("money") Integer money);
}
1.3.6 创建启动类
/**
* @description:订单启动类
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class DemoOrderApp {
public static void main(String[] args) {
SpringApplication.run(DemoOrderApp.class, args);
}
}
1.3.7 创建bootstrap.yml
server:
port: 20003
spring:
application:
name: ams-demo-order
cloud:
nacos:
# 注册中心
discovery:
server-addr: http://你的nacos地址:8848
# 配置中心
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
shared-configs[0]:
data-id: ams-common.yaml
refresh: true
alibaba:
seata:
tx-service-group: demo-order
1.3.8 新增nacos配置:
在naocs中创建ams-demo-order.yaml配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${mysql.host}:${mysql.port}/ams_order?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: ${mysql.username}
password: ${mysql.password}
redis:
database: 0
host: ${redis.host}
port: ${redis.port}
password: ${redis.password}
cache:
# 缓存类型
type: redis
# 缓存时间(单位:ms)
redis:
time-to-live: 3600000
# 缓存null值,防止缓存穿透
cache-null-values: true
# 允许使用缓存前缀,
use-key-prefix: true
# 缓存前缀,没有设置使用注解的缓存名称(value)作为前缀,和注解的key用双冒号::拼接组成完整缓存key
key-prefix: 'admin:'
mybatis-plus:
configuration:
# 驼峰下划线转换
map-underscore-to-camel-case: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局参数设置
ribbon:
ReadTimeout: 120000
ConnectTimeout: 10000
SocketTimeout: 10000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
feign:
httpclient:
enabled: true
okhttp:
enabled: false
#Seata分布式事务配置(AT模式)
seata:
enabled: true
application-id: ${spring.application.name}
#客户端和服务端在同一个事务组
tx-service-group: demo-order
enable-auto-data-source-proxy: true
service:
vgroup-mapping:
demo-order: default
config:
type: nacos
nacos:
namespace: "填写在安装seata时创建的命名空间"
serverAddr: 你的nacos ip:8848
group: SEATA_GROUP
username: "nacos"
password: "nacos"
#服务注册到nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 你的nacos ip:8848
group: SEATA_GROUP
namespace: "public"
username: "nacos"
password: "nacos"
cluster: default
logging:
level:
spring: info
1.3.9 创建回滚表
在每个业务数据库下面创建回滚表
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
2.0.1
验证
启动是三个服务,请求:http://localhost:20003/order/createOrder,通过设置异常,观察是否全局回滚
随心所往,看见未来。Follow your heart,see night!
欢迎点赞、关注、留言,收藏及转发,一起学习、交流!