您现在的位置是:首页 >学无止境 >微服务之事务网站首页学无止境
微服务之事务
在分布式系统中,事务管理是一个复杂且关键的问题。由于服务被拆分为多个独立的微服务,传统的单机事务(如数据库事务)无法直接应用于跨服务的场景。因此,分布式事务解决方案应运而生。本文将介绍几种主流的分布式事务方案,包括 Seata、2PC、TCC、Saga 等.
1. 分布式事务的挑战
在分布式系统中,事务需要跨越多个服务、数据库和消息队列,面临以下挑战:
-
数据一致性:如何保证多个服务的数据在事务提交后保持一致。
-
性能开销:分布式事务通常需要额外的协调和通信,可能影响系统性能。
-
复杂性:实现分布式事务需要处理网络分区、服务宕机等异常情况。
2. 主流分布式事务方案
2.1. Seata(阿里巴巴开源)
Seata 是一款开源的分布式事务解决方案,支持 AT(自动补偿)、TCC、Saga 和 XA 模式。
方案说明:
-
AT 模式:基于两阶段提交(2PC)的改进版本,通过全局锁和本地事务实现数据一致性。
-
TCC 模式:通过 Try、Confirm、Cancel 三个阶段实现事务的最终一致性。
-
Saga 模式:通过长事务和补偿机制实现最终一致性。
-
XA 模式:基于 XA 协议的两阶段提交。
优点:
-
多模式支持:支持多种分布式事务模式,适应不同场景。
-
易用性:提供简单的 API 和注解,易于集成到 Spring Cloud 等框架。
-
高性能:AT 模式通过本地事务和全局锁优化性能。
-
社区活跃:作为阿里巴巴开源项目,Seata 拥有活跃的社区和丰富的文档。
缺点:
-
全局锁性能瓶颈:在高并发场景下,全局锁可能成为性能瓶颈。
-
复杂性:TCC 和 Saga 模式需要开发者手动实现补偿逻辑。
适用场景:
-
需要高一致性的金融、电商等场景。
-
对性能要求较高的场景(AT 模式)。
-
需要灵活选择事务模式的场景。
2.2. 2PC(两阶段提交)
2PC 是一种经典的分布式事务协议,分为准备阶段和提交阶段。
方案说明:
-
准备阶段:协调者询问所有参与者是否可以提交事务。
-
提交阶段:如果所有参与者都同意提交,协调者通知参与者提交事务;否则回滚。
优点:
-
强一致性:保证所有参与者要么全部提交,要么全部回滚。
-
简单易懂:协议逻辑清晰,易于理解。
缺点:
-
性能差:同步阻塞,事务执行时间长。
-
单点故障:协调者宕机可能导致事务阻塞。
-
数据不一致风险:在提交阶段,如果部分参与者提交失败,可能导致数据不一致。
适用场景:
-
对一致性要求极高的场景。
-
事务参与者较少的场景。
2.3. TCC(Try-Confirm-Cancel)
TCC 是一种基于补偿机制的分布式事务方案。
方案说明:
-
Try 阶段:预留资源,执行业务检查。
-
Confirm 阶段:提交事务,确认资源。
-
Cancel 阶段:回滚事务,释放资源。
优点:
-
高性能:通过资源预留减少锁冲突。
-
最终一致性:适合高并发场景。
缺点:
-
开发复杂:需要手动实现 Try、Confirm、Cancel 逻辑。
-
业务侵入性强:需要修改业务代码。
适用场景:
-
高并发、对性能要求较高的场景。
-
需要最终一致性的场景。
2.4. Saga
Saga 是一种长事务解决方案,通过事件驱动和补偿机制实现最终一致性。
方案说明:
-
正向事务:依次执行多个子事务。
-
补偿事务:如果某个子事务失败,依次执行补偿事务回滚。
优点:
-
松耦合:通过事件驱动实现服务解耦。
-
适合长事务:支持长时间运行的事务。
缺点:
-
数据不一致风险:在补偿事务执行期间,数据可能处于不一致状态。
-
开发复杂:需要设计正向事务和补偿事务。
适用场景:
-
长事务场景(如订单、物流系统)。
-
对最终一致性要求较高的场景。
2.5. 消息队列(最终一致性)
通过消息队列实现分布式事务的最终一致性。
方案说明:
-
本地事务:在本地事务中发送消息到消息队列。
-
消息消费:消费者消费消息并执行业务逻辑。
优点:
-
高性能:异步执行,减少事务阻塞。
-
松耦合:通过消息队列解耦服务。
缺点:
-
数据不一致风险:消息可能丢失或重复消费。
-
开发复杂:需要处理消息的可靠性和幂等性。
适用场景:
-
对一致性要求不高的场景。
-
异步处理的场景(如通知、日志)。
3. Seata 的使用
3.1. 部署 Seata Server
-
下载 Seata Server 并解压。
-
修改
conf/registry.conf
文件,配置数据库. -
file:(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认) db:(5.7+)高可用模式,全局事务会话信息通过db共享,相应性能差些 # 配置mysql数据库 store { mode = "db" file { ## store location dir dir = "sessionStore" } db { datasource = "dbcp" db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://101.42.40.100:3306/seata" user = "root" password = "root" }
-
conf/registry.conf
配置 nacos
type = "nacos"
nacos {
serverAddr = "localhost:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
- 新建表
-- -------------------------------- 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);
- 配置注册
-
启动 Seata Server:
sh bin/seata-server.sh
。
3.2. 集成 Seata Client
-
在 Spring Boot 项目中添加 Seata 依赖:
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.5.0</version> </dependency>
- 添加undo_log表
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT 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 AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
-
配置 Seata:
# 配置事务组 seata: enabled: true application-id: your-app-id tx-service-group: my_tx_group registry: type: nacos nacos: server-addr: localhost:8848 config: type: nacos nacos: server-addr: localhost:8848
3.3. 使用 Seata AT 模式
-
在业务方法上添加
@GlobalTransactional
注解:@GlobalTransactional public void createOrder() { // 业务逻辑 }
3.4. 使用 Seata TCC 模式
-
定义 TCC 接口:
public interface TccService { @TwoPhaseBusinessAction(name = "tryAction", commitMethod = "confirm", rollbackMethod = "cancel") boolean tryAction(BusinessActionContext context); boolean confirm(BusinessActionContext context); boolean cancel(BusinessActionContext context); }
3.5. 选型建议
-
强一致性场景:选择 Seata AT 模式 或 2PC。
-
高并发场景:选择 TCC 或 消息队列。
-
长事务场景:选择 Saga。
-
简单易用:选择 Seata,支持多种模式且易于集成。
3.6.总结
分布式事务的选型需要根据业务场景、一致性要求和性能需求来决定。Seata 作为一款功能强大且易用的分布式事务解决方案,适合大多数场景。对于特定场景,可以选择 TCC、Saga 或消息队列等方案。无论选择哪种方案,都需要结合业务特点进行合理设计和优化,以确保系统的稳定性和性能。
4. 使用笔记
4.1. seata 的三个角色
- 事务协调者 TC 维护全局和分支事务的状态,驱动全局事务提交或者回滚
- 事务管理器 TM 定义全局事务范围:开始全局事务,提交或者回滚全局事务
- 资源管理器 RM 管理分支事务的资源,与TC交谈,注册分支事务 和报告分支事务状态,驱动分支事务提交或者回滚
4.2. 模式比较
4.3. Seata AT 模式的执行流程
Seata AT 模式的核心思想是通过 两阶段提交(2PC) 来实现分布式事务的一致性。它的执行流程分为以下几个阶段:
1. 事务发起阶段
-
全局事务开始:
-
事务发起者(通常是业务服务的入口)在方法上添加
@GlobalTransactional
注解,标记这是一个全局事务。 -
Seata 框架会为该事务生成一个全局唯一的 XID(全局事务 ID),并将 XID 传播到所有参与事务的服务。
-
-
注册分支事务:
-
每个参与事务的服务(称为 RM,Resource Manager)在执行本地事务之前,会向 Seata Server(TC,Transaction Coordinator)注册一个 分支事务。
-
分支事务的注册信息包括 XID、资源 ID(如数据源)、分支事务类型(如 AT、TCC)等。
-
2. 第一阶段:执行本地事务
-
执行业务 SQL:
-
每个参与事务的服务执行自己的业务逻辑,包括对数据库的增删改操作。
-
Seata 会通过 数据源代理 拦截这些 SQL 操作,记录 前置镜像(Before Image) 和 后置镜像(After Image),用于后续的回滚操作。
-
-
提交本地事务:
-
每个服务在完成本地事务后,会向 Seata Server 报告本地事务的状态(成功或失败)。
-
如果本地事务成功,Seata 会将该分支事务的状态标记为 已提交但未完成,等待全局事务的最终提交。
-
3. 第二阶段:全局事务提交或回滚
-
全局事务提交:
-
如果所有分支事务都成功提交,Seata Server 会向所有分支事务发送 提交请求。
-
每个分支事务在收到提交请求后,会删除之前记录的前置镜像和后置镜像,完成事务的最终提交。
-
-
全局事务回滚:
-
如果任何一个分支事务失败,Seata Server 会向所有分支事务发送 回滚请求。
-
每个分支事务在收到回滚请求后,会根据之前记录的前置镜像和后置镜像,执行 反向 SQL,将数据恢复到事务开始之前的状态。
-
-
5. seata使用问题解决
5.1. Seata Server 启动失败
问题描述
在启动 Seata Server 时,可能会遇到以下错误:
-
端口被占用。
-
依赖的注册中心(如 Nacos)未启动或配置错误。
-
数据库连接失败。
解决方案
-
端口被占用:
-
检查 Seata Server 的默认端口(默认是 8091)是否被占用。
-
修改
conf/registry.conf
中的端口配置,或者停止占用端口的进程。
-
-
注册中心未启动:
-
确保注册中心(如 Nacos、Zookeeper)已正确启动。
-
检查
conf/registry.conf
中的注册中心配置是否正确。例如:registry { type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } }
-
-
数据库连接失败:
-
Seata Server 需要连接数据库来存储事务日志。确保
conf/file.conf
中的数据库配置正确:store { mode = "db" db { url = "jdbc:mysql://localhost:3306/seata" user = "root" password = "root" } }
-
确保数据库已创建,并且 Seata 所需的表已初始化(表结构在 Seata 的
script/server/db
目录下)。
-
5.2. 客户端无法连接到 Seata Server
问题描述
客户端启动时,报错提示无法连接到 Seata Server,例如:
io.seata.core.exception.RmTransactionException: can not register RM,err:can not connect to services-server
解决方案
-
检查 Seata Server 地址:
-
确保客户端配置的 Seata Server 地址正确。在
application.yml
中配置:seata: tx-service-group: my_tx_group registry: type: nacos nacos: server-addr: localhost:8848
-
-
检查注册中心:
-
确保 Seata Server 和客户端使用相同的注册中心(如 Nacos)。
-
登录注册中心的管理界面,检查 Seata Server 是否已成功注册。
-
-
网络问题:
-
确保客户端和 Seata Server 之间的网络是通的,没有防火墙或安全组限制。
-
5.3. 事务未生效
问题描述
在业务方法上添加了 @GlobalTransactional
注解,但事务未生效,例如:
-
部分服务未回滚。
-
事务未提交。
解决方案
-
检查注解是否正确:
-
确保在业务方法上正确使用了
@GlobalTransactional
注解:@GlobalTransactional public void createOrder() { // 业务逻辑 }
-
-
检查数据源代理:
-
Seata 需要通过代理数据源来管理事务。确保数据源被 Seata 正确代理:
seata: enabled: true application-id: your-app-id tx-service-group: my_tx_group
-
-
检查分支事务注册:
-
确保每个参与分布式事务的服务都正确注册了分支事务。可以通过 Seata 的日志查看是否有分支事务注册失败。
-
-
检查全局事务 ID:
-
在 Seata Server 的日志中查找全局事务 ID(XID),检查事务的执行状态。
-
5.4. 数据源代理失败
问题描述
在集成 Seata 时,数据源代理失败,导致事务无法管理。
解决方案
-
检查数据源配置:
-
确保数据源被 Seata 的
DataSourceProxy
代理。例如:@Bean public DataSource dataSource(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); }
-
-
排除冲突依赖:
-
如果项目中使用了多个数据源或连接池(如 Druid、HikariCP),确保它们的版本兼容,并且没有冲突。
-
-
检查 Seata 版本:
-
确保 Seata 的版本与 Spring Boot、Spring Cloud 的版本兼容。
-
5.5. 事务回滚失败
问题描述
在分布式事务中,部分服务回滚失败,导致数据不一致。
解决方案
-
检查回滚逻辑:
-
确保每个服务的回滚逻辑正确实现。例如,在 TCC 模式中,确保
cancel
方法能够正确回滚资源。
-
-
检查全局锁:
-
在 AT 模式中,Seata 使用全局锁来保证数据一致性。如果全局锁未正确释放,可能导致回滚失败。检查 Seata Server 的日志,查看是否有锁冲突。
-
-
检查异常传播:
-
确保业务方法中的异常能够正确传播到 Seata 框架。例如,在 Spring 中,默认只有
RuntimeException
会触发回滚。
-
5.6. 性能问题
问题描述
在高并发场景下,Seata 的性能可能成为瓶颈,例如:
-
全局锁竞争激烈。
-
事务执行时间过长。
解决方案
-
优化全局锁:
-
减少全局锁的持有时间,尽量将锁粒度细化。
-
使用 TCC 模式代替 AT 模式,减少锁冲突。
-
-
异步化处理:
-
将非核心业务逻辑异步化,减少事务的执行时间。
-
-
调整 Seata 配置:
-
根据业务场景调整 Seata 的线程池和连接池配置。
-
5.7. Seata 日志问题
问题描述
Seata 的日志输出过多或过少,难以排查问题。
解决方案
-
调整日志级别:
-
在
logback.xml
或log4j2.xml
中调整 Seata 的日志级别:<logger name="io.seata" level="DEBUG" />
-
-
查看 Seata Server 日志:
-
Seata Server 的日志位于
logs/seata/
目录下,可以通过日志排查问题。
-