【SpringCloud】使用Seata实现分布式事务
目录
- 一、Seata 框架的需求背景
- 二、Seata 事务模式与架构
- 2.1 Seata 组成
- 2.2 Seata 事务模式
- 三、Seata 实战演示
- 3.1 部署 Seata Server
- 3.1.1 下载 Seata Server
- 3.1.2 更改 Seata Server 配置
- 3.1.3 创建 Seata Server 所需的数据库、数据库表
- 3.1.4 启动 Seata Server
- 3.2 Seata AT 模式分布式事务数据一致性演示
- 3.2.1 添加依赖
- 3.2.2 在项目中配置 Seata
- 3.2.2 项目中使用 Seata AT 模式
一、Seata 框架的需求背景
一般情况下,在 Java 项目中如果存在对一个数据库实例的一系列操作的,为了避免程序执行过程中出现异常,我们通常会使用一个事务包裹住这一系列数据库操作,从而保证这一组操作的数据安全。我们通常使用Spring工具箱中的工具来实现本地事务(例如 @Transactional
注解提供的声明式事务功能等)。
但是,在微服务架构下,一个微服务可能存在多个实例,或者说一个微服务实例在运行期间可能同时对多个数据库实例进行CRUD。在这种分布式应用场景下,使Spring工具箱内的工具就失效了。我们需要借助分布式事务来保证分布式应用场景下的数据一致性。Seata 提供了分布式事务完备的解决方案,针对不同的分布式事务场景有不同的模式。
二、Seata 事务模式与架构
2.1 Seata 组成
Seata 框架由三个组件组成,分别是事务协调器(Transaction Coordinator,简称TC),事务管理器(Transaction Manager,简称TM),资源管理器(Resource Manager,简称RM)。
- 事务协调器(TC):是一个中心化的事务协调者的角色,主要用来协调全局事务的提交和回滚,并管理全局事务与分支事务的状态。是一个需要独立部署的服务,也就是我们下面要进行部署的 Seata Server;
- 事务管理器(TM):主要用来发起一个全局事务,对一个全局事务是提交,还是回滚做出决议。在我们实际的项目中,
- 资源管理器(RM):操作分支事务进行提交或者回滚,并向事务协调器(TC)上报分支事务的状态。
以上图为例来介绍下 Seata 的架构。 事务协调器(TC,又被叫做 Seata Server)是需要被独立部署的,在AT 模式下,TM 和 RM 则是在相应的微服务中引入依赖,添加相应的配置,在需要发起全局事务的方法上添加相应的注解即可。
- 上图在 Business 微服务模块中发起一个全局事务,并将该全局事务注册到 TC 上;
- Business 微服务模块调用了 Stock 微服务和 Order 微服务,Stock 微服务和 Order 微服务都属于该全局事务下的分支事务,这2个被调用的微服务执行本地事务并生成相应的回滚日志到undolog数据库表中,执行完成之后(无论成功或失败)也会向 TC 上报分支事务执行情况。
- 在 Order 微服务中又调用了 Account 微服务,又开启了一个分支事务,Account 微服务执行本地事务并生成相应的回滚日志到undolog数据库表中,执行完毕之后向 TC 上报自己所在的分支事务执行状态。
- Business 微服务对全局事务的执行结果进行决议,如果所有的分支事务都执行成功,则提交全局事务,否则,会根据undolog数据库表中记录的回滚日志,对所有执行成功的分支事务进行回滚。
2.2 Seata 事务模式
模式类型 | 数据一致性 | 性能 | 业务侵入性 | 适用场景 |
---|---|---|---|---|
AT 模式 | 弱一致性 | 高 | 无侵入 | 大多数分布式事务场景,要求无须改造业务代码,快速接入时 |
XA 模式 | 强一致性 | 差 | 无侵入 | 想要迁移到 Seata 平台基于 XA 协议的老应用,使用 XA 模式将更平滑 |
Saga 模式 | 最终一致性 | 很高 | 有侵入,需要实现状态机及补偿代码 | 业务流程长、业务流程多,或者遗留系统服务,无法提供TCC要求的3个接口 |
TCC 模式 | 弱一致性 | 很高 | 有侵入,需要实现try,confirm,cancel 3个接口 | 适用于核心系统等对性能有很高要求的场景 |
三、Seata 实战演示
下面演示 AT 模式下的 Seata 使用。分为2各部分:部署 Seata Server,分布式事务的数据一致性。
3.1 部署 Seata Server
3.1.1 下载 Seata Server
在 Seata 的 Github 主页的 Release 页面下载对应版本的二进制压缩包。
3.1.2 更改 Seata Server 配置
默认的 Seata Server 无法运行,需要更改配置,这里的 Seata Server 将使用 MySQL 数据库作为数据源,使用 Nacos 作为服务发现、注册中心。
首先,在Seata Server 的conf 目录下找到文件 file.conf.example
复制一份,并将文件名改为 file.conf
,打开该文件进行更改,改动主要集中在 store
节点:
store {
## store mode: file、db
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "druid"
## 配置的数据库类型
dbType = "mysql"
## 数据库连接驱动
driverClassName = "com.mysql.jdbc.Driver"
## 数据库连接 URL
url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
## 数据库连接账号密码
user = "fuyou"
password = "fuyou3323"
## 数据库连接配置
minConn = 5
maxConn = 50
## Seata Server在数据库表中存放全局事务锁信息的表名
globalTable = "global_table"
## Seata Server在数据库表中存放分支事务锁信息的表名
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
以上配置的主要更改点是,把mode
字段取值改为db
,然后就是数据库相关的配置,设置数据库类型,数据库驱动,数据库连接的URL,数据库的账号密码,以及 Seata Server 存储全局锁及表所的数据库表名。
接下来接着更改 registry.conf
文件里的配置,内容大概如下:
registry {
# 支持 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "myGroup"
namespace = "public"
cluster = "default"
username = ""
password = ""
}
}
因为我们使用 Nacos 进行服务的注册发现,因此我们将Seata Server也注册到 Nacos 上进行服务管理和使用(当然,你也可以选择其他类型的注册中心)。上述配置主要是配置了 注册中心的类型,Seata Server的服务名,Nacos的地址、服务组等信息。
3.1.3 创建 Seata Server 所需的数据库、数据库表
我们在上面配置了 Seata Server 中使用MySQL数据库进行存储,并在 URL 中配置了 Seata Server 使用的数据库库名为 seata。Seata Server(TC)在运行期间,需要管理协调全局事务和分支事务,在这个过程需要把一些信息存在数据库中,例如XID(全局事务ID),分支事务ID等信息。
为了避免资源的竞争,Seata也会为资源加锁,避免不同的全局事务之间抢占资源。例如,一个分支事务只有在获取到全局锁和本地锁的情况下才可以提交成功对应的分支事务,并将对应的回滚日志写到数据库中。全局锁和本地锁的相关信息就存在名为 seata
的数据库的 lock_table
表中。
-- 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 = 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,
`gmt_modified` DATETIME,
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(96),
`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 = utf8mb4;
本地创建库表情况如下:
除了 Seata Server 要使用的数据库及数据库表以外,还需要在引入 Seata 依赖的 client 侧的微服务所使用的数据库内,创建一个数据库表undo_log
,这个就是 Seata 的回滚日志表,每个执行成功的分支事务都需要将该分支对应的回滚日志写入到该表中,如果全局事务回滚时,每个分支事务就根据分支事务的回滚日志进行恢复。
CREATE TABLE IF NOT EXISTS `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
3.1.4 启动 Seata Server
在解压好的Seata Server 目录内,切换到 /bin
目录下,运行 Seata Server 的启动脚本。
启动成功后,效果大致如下:
PS :
1. 因为 Seata 要注册到 Nacos 上,所以在启动Seata Server之前务必先确保Nacos服务在线(你也可以选择其他类型的注册中心,因为我使用的是Nacos,所以需要确保Nacos在线);
2. 尽量确保 seata server 的/lib
目录内依赖的mysql 驱动的版本与本地安装的数据库版本一致,或者相近,否则可能会有报错;
3.2 Seata AT 模式分布式事务数据一致性演示
3.2.1 添加依赖
接下来我们要演示 Seata 的 AT 模式下可以保证分布式事务的数据一致性。对于 Seata 来说微服务侧是 client 端,我们首先需要先添加 Seata 依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3.2.2 在项目中配置 Seata
在需要使用Seata的每个微服务的 application.yaml
文件中添加 Seata 的配置项:
spring:
cloud:
alibaba:
seata:
tx-service-group: seata-server-group
seata:
application-id: order
registry:
type: nacos
nacos:
application: seata-server
server-addr: localhost:8848
namespace: dev
group: myGroup
cluster: default
service:
vgroup-mapping:
seata-server-group: default
3.2.2 项目中使用 Seata AT 模式
因为 Seata 的 AT 模式是无侵入的,我们只需要在开启分布式事务的方法上添加 @GlobalTransactional
注解即可。示例代码如下:
@DeleteMapping("order")
@GlobalTransactional(name = "business", rollbackFor = Exception.class)
public void deleteOrder(@RequestParam("orderId") Long orderId) {
customerService.deleteOrder(orderId);
}
@Override
@Transactional
public void deleteOrder(Long orderId) {
stockService.recoverStock(orderId);
orderService.deleteOrder(orderId);
// 执行到此处显式抛出异常
throw new RuntimeException("Branch transaction exception.");
}
在以上演示代码的 deleteOrder
方法中,调用了 stockService
服务的 deleteOrder
,此处是一个 openFeign 的远程调用。不属于本地的事务。如果调用该方法后,还发现订单未删除,说明分布式事务生效了,该全局事务下的所有分支事务都进行了回滚。
我在本地新下了一个订单,系统生成的订单ID 为 8.
然后调用上述代码中提供的订单删除接口。
再次查询订单ID 为 8 的订单,发现并该订单还在。
以上就是 seata 框架 AT 模式下的演示。Seata 的 AT模式对业务无侵入,不用对项目代码进行改造,只需要在对应位置处加上相应注解即可。改造工作量很小。