在微服务架构里,事务管理可是个让人头疼的难题。原本单机环境下顺风顺水的本地事务,到了分布式场景就频频 “掉链子”。今天这篇文章,就从分布式事务的基础问题说起,结合 CAP、BASE 理论,再到 Seata 框架的实战应用,带大家把微服务事务管理的思路理清楚。

一、分布式事务

1.1 本地事务:单机时代的 “优等生”

本地事务就是咱们熟悉的单机事务,得严格遵守 ACID 原则:

  • 原子性(Atomicity):事务里的操作要么全成,要么全败,没有中间态。比如转账,要么钱成功转过去,要么一分没动。
  • 一致性(Consistency):事务执行完,数据库得保持完整的约束,像主键唯一、外键关联这些都不能乱。
  • 隔离性(Isolation):多个事务同时操作同一资源时,互相不能干扰,得有隔离级别来控制,比如读未提交、读已提交等。
  • 持久性(Durability):事务一旦提交,对数据库的修改就永久保存了,就算数据库崩溃,数据也不会丢。

1.2 分布式事务:微服务下的 “拦路虎”

分布式事务,简单说就是事务操作跨了多个服务或多个数据库。比如电商下单,得调用订单服务创建订单、库存服务扣减库存、账户服务扣用户余额,这三个操作分别在三个服务和三个数据库里,要让它们要么全成功,要么全失败,这就是分布式事务要解决的问题。

原本每个服务里的本地事务都能保证 ACID,但把它们串成一个 “业务整体” 时,ACID 就很难满足了 —— 万一订单创建成功,库存扣减也成功,偏偏账户扣款失败,总不能让订单生效却没扣用户钱吧?这就是分布式事务的核心痛点。

1.3 直观感受:分布式事务问题演示

咱们拿电商下单举例:

  1. 订单服务:创建订单,本地事务提交,订单状态为 “已创建”。
  2. 库存服务:扣减对应商品库存,本地事务提交,库存减少。
  3. 账户服务:扣用户余额时,突然报错(比如网络超时、余额不足),本地事务回滚,余额没扣。

这时候就出问题了:订单有了,库存少了,用户钱却没扣,数据不一致了。这就是分布式事务没处理好的典型情况。

二、搞定分布式事务的理论基础

要解决分布式事务,得先懂两个核心理论:CAP 定理和 BASE 理论。

2.1 CAP 定理:分布式系统的 “三角难题”

1998 年加州大学的 Eric Brewer 提出,分布式系统有三个核心指标,但这三个指标没法同时满足,只能三选二:

  • 一致性(Consistency):不管访问哪个节点,拿到的数据都得一样。比如两个节点原本数据都是 v0,一个改成 v1 后,另一个也得同步成 v1,否则用户访问不同节点看到的数据就不一样了。
  • 可用性(Availability):只要节点是健康的,用户访问它就得有响应,不能超时或拒绝。比如三个节点的集群,哪怕一个节点挂了,另外两个也得正常提供服务。
  • 分区容错性(Partition Tolerance):分布式系统里,节点之间可能因为网络故障断开(形成 “分区”),但整个系统还得能对外提供服务。

关键矛盾:在分布式系统里,网络故障是难免的,所以分区容错性(P)是必须要保证的。这时候就只能在一致性(C)和可用性(A)之间选一个 —— 要一致性,就得等分区恢复、数据同步完才能提供服务,期间服务不可用;要可用性,就不能等数据同步,直接提供服务,但会出现数据不一致。

2.2 BASE 理论:CAP 的 “折中方案”

BASE 理论是对 CAP 的补充,核心思路是 “退而求其次”,接受一定时间内的不一致,最终保证数据一致:

  • 基本可用(Basically Available):系统出故障时,允许损失部分可用性,比如降级(高峰期关闭非核心功能)、限流(限制部分用户访问),但核心功能得正常。
  • 软状态(Soft State):允许系统在一定时间内处于 “中间状态”,比如数据同步过程中,不同节点数据不一致,这个状态是暂时的。
  • 最终一致性(Eventually Consistent):虽然不能保证实时一致,但过一段时间(比如数据同步完成后),所有节点的数据会达成一致。

2.3 解决分布式事务的两种思路

基于 CAP 和 BASE 理论,解决分布式事务主要有两种方向:

  • CP 模式:追求强一致性。各个子事务执行完后不提交,等待协调者通知,要么一起提交,要么一起回滚。但等待期间服务处于弱可用状态,性能较差。
  • AP 模式:追求高可用性。各个子事务先执行并提交,允许暂时的数据不一致,之后通过补偿机制(比如回滚、重试)恢复数据,最终达成一致。

不管哪种模式,都需要一个 “事务协调者(TC)” 来协调各个子事务(分支事务)的状态,比如通知哪个分支提交、哪个分支回滚。

三、Seata:分布式事务的 “好帮手”

Seata 是 2019 年蚂蚁金服和阿里巴巴一起开源的分布式事务解决方案,支持多种事务模式,易用性和性能都不错。

3.1 Seata 的核心架构

Seata 里有三个关键角色,分工明确:

  • TC(Transaction Coordinator):事务协调者,是核心。负责维护全局事务和分支事务的状态,协调所有分支事务提交或回滚。
  • TM(Transaction Manager):事务管理器。定义全局事务的范围(比如哪个方法是全局事务入口),发起全局事务的开始、提交或回滚。
  • RM(Resource Manager):资源管理器。管理分支事务的资源(比如数据库连接),跟 TC 通信,注册分支事务、报告分支事务状态,还得执行 TC 下达的提交或回滚指令。

Seata 支持四种事务模式,咱们重点说最常用的 XA 和 AT 模式:

  • XA 模式:强一致性,基于两阶段提交,无代码侵入,但性能一般。
  • AT 模式:最终一致性,也是两阶段,但一阶段就提交事务,性能更好,无代码侵入(Seata 默认模式)。
  • TCC 模式:最终一致性,需要手写补偿逻辑(Try、Confirm、Cancel),有代码侵入。
  • SAGA 模式:适合长事务,也需要手写补偿逻辑,有代码侵入。

3.2 部署 TC 服务:先搭好 “协调中心”

TC 是 Seata 的核心,得先部署好。步骤很简单:

  1. 下载 Seata Server:去 Seata 官网(http://seata.io/)下载 Seata Server 包,比如 1.4.2 版本。
  2. 解压包:解压后会有 bin(启动脚本)、conf(配置文件)、lib(依赖)等文件夹。
  3. 修改配置文件:重点改 conf 目录下的registry.conf,让 TC 注册到 Nacos(方便微服务发现):
registry {
  # 注册中心类型,选nacos
  type = "nacos"
  nacos {
    # TC注册到Nacos的服务名,自定义
    application = "seata-tc-server"
    # Nacos地址,本地就是127.0.0.1:8848
    serverAddr = "127.0.0.1:8848"
    # Nacos分组,默认DEFAULT_GROUP
    group = "DEFAULT_GROUP"
    # Nacos用户名密码,默认nacos/nacos
    username = "nacos"
    password = "nacos"
    # 集群名,比如SH(上海)
    cluster = "SH"
  }
}

config {
  # 配置读取方式,也从Nacos读
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    # 配置文件的DataID
    dataId = "seataServer.properties"
  }
}
  1. 在 Nacos 配置 Seata Server 参数:登录 Nacos 控制台,新建配置,DataID 为seataServer.properties,Group 为SEATA_GROUP,配置内容主要是数据存储(用数据库存全局事务、分支事务信息):
# 数据存储方式,选db(数据库)
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
# MySQL驱动,注意版本,8.x用cj.jdbc.Driver
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 数据库连接地址,seata库需要自己创建
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
# 全局事务表、分支事务表、锁表名(默认即可)
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.lockTable=lock_table
# 其他配置(日志、传输方式等)
server.undo.logSaveDays=7
transport.serialization=seata
transport.compressor=none

  1. 创建 Seata 数据库和表:在 MySQL 里创建seata库,然后执行 SQL 创建global_table(全局事务表)、branch_table(分支事务表)、lock_table(锁表),SQL 脚本如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- 分支事务表
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` tinyint(4) NULL DEFAULT NULL,
  `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime(6) NULL DEFAULT NULL,
  `gmt_modified` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`branch_id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- 全局事务表
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `timeout` int(11) NULL DEFAULT NULL,
  `begin_time` bigint(20) NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`xid`) USING BTREE,
  INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
  INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- 锁表
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
  `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;
  1. 启动 TC 服务:进入 bin 目录,双击seata-server.bat(Windows)或执行./seata-server.sh(Linux)。也可以指定端口,比如seata-server.bat -p 9000。启动成功后,去 Nacos 服务列表能看到seata-tc-server,说明 TC 部署好了。

3.3 微服务集成 Seata:让服务 “接入” 事务协调

每个需要参与分布式事务的微服务(比如订单服务、库存服务、账户服务),都要集成 Seata,步骤都一样:

  1. 引入 Seata 依赖:在 pom.xml 里加依赖,注意排除低版本的seata-spring-boot-starter,用 1.4.2 版本:
<!-- Seata依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <!-- 排除低版本的seata-spring-boot-starter -->
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 引入1.4.2版本的seata-spring-boot-starter -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

  1. 配置 TC 地址:在 application.yml 里配置 Seata,让微服务能找到 TC:
seata:
  registry:
    # TC注册中心类型(nacos)
    type: nacos
    nacos:
      # Nacos地址
      server-addr: 127.0.0.1:8848
      # 服务名(和TC注册的一致)
      application: seata-tc-server
      # 分组(和TC一致)
      group: DEFAULT_GROUP
      # 用户名密码
      username: nacos
      password: nacos
      # 集群名(和TC一致)
      cluster: SH
  # 事务组名称(自定义,比如seata-demo)
  tx-service-group: seata-demo
  service:
    # 事务组和集群的映射(seata-demo对应SH集群)
    vgroup-mapping:
      seata-demo: SH

微服务找 TC 的逻辑很简单:通过 Nacos 的 namespace、group、application(服务名)、cluster(集群名)这四个信息,定位到 TC 实例。

四、实战:Seata 的 XA 和 AT 模式

4.1 XA 模式:强一致性的 “代表”

XA 模式是基于 X/Open 组织定义的 XA 规范,主流数据库(MySQL、Oracle)都支持,核心是两阶段提交。

4.1.1 两阶段提交:分两步走的 “协调”

XA 模式的核心是两阶段提交,正常情况和异常情况处理不一样:

  • 正常情况
    1. 一阶段(准备阶段):TC 通知所有 RM 执行本地事务,但不提交。RM 执行完后,告诉 TC “我准备好了”(成功)或 “我失败了”。
    2. 二阶段(提交阶段):TC 看所有 RM 都准备好,就通知大家提交事务;只要有一个 RM 失败,就通知所有人回滚。
  • 异常情况:比如一阶段有个 RM 执行失败,TC 直接触发二阶段回滚,所有 RM 都回滚本地事务。
4.1.2 Seata 的 XA 模型:简化但核心不变

Seata 对 XA 模式做了封装,适配自己的架构:

  • 一阶段(RM 的活):RM 注册分支事务到 TC → 执行本地业务 SQL(但不提交) → 告诉 TC 执行状态。
  • 二阶段(TC 和 RM 的活):TC 检查所有分支事务状态 → 全成功就通知 RM 提交,有失败就通知 RM 回滚 → RM 执行提交或回滚。
4.1.3 XA 模式的优缺点:强一致但性能一般
  • 优点
    1. 强一致性,严格满足 ACID 原则,数据不会乱。
    2. 数据库原生支持,不用改业务代码,无侵入。
  • 缺点
    1. 一阶段会锁定数据库资源(比如行锁),得等二阶段结束才释放,性能差,高并发场景 hold 不住。
    2. 依赖关系型数据库,非关系型数据库用不了。
4.1.4 实现 XA 模式:简单配置就行

Seata 已经自动装配了 XA 模式,不用写额外代码,两步搞定:

  1. 开启 XA 模式:在每个微服务的 application.yml 里加配置,指定数据源代理模式为 XA:
seata:
  data-source-proxy-mode: XA
  1. 标记全局事务入口:在发起全局事务的方法上加@GlobalTransactional注解。比如订单服务的创建订单方法:
@Service
public class OrderServiceImpl implements OrderService {
    // 注入库存服务、账户服务
    @Autowired
    private InventoryFeignClient inventoryFeignClient;
    @Autowired
    private AccountFeignClient accountFeignClient;
    
    // 加@GlobalTransactional,标记这是全局事务入口
    @GlobalTransactional
    @Override
    public void createOrder(Order order) {
        // 1. 创建订单(本地事务)
        orderMapper.insert(order);
        // 2. 调用库存服务扣库存(远程事务)
        inventoryFeignClient.deduct(order.getProductId(), order.getCount());
        // 3. 调用账户服务扣余额(远程事务)
        accountFeignClient.deduct(order.getUserId(), order.getMoney());
    }
}

  1. 重启测试:重启所有微服务,不管中间哪个步骤失败,所有事务都会回滚,数据保持一致。

4.2 AT 模式:性能更好的 “最终一致” 方案

AT 模式是 Seata 的默认模式,也是两阶段,但解决了 XA 模式资源锁定时间长的问题,性能更好,而且也是无代码侵入。

4.2.1 Seata 的 AT 模型:一阶段就提交

AT 模式的核心是 “一阶段提交,二阶段补偿”,步骤更灵活:

  • 一阶段(RM 的活)
    1. RM 注册分支事务到 TC。
    2. 执行 SQL 前,先保存数据快照(也就是修改前的数据,存在undo_log表)。
    3. 执行业务 SQL,直接提交本地事务,释放数据库资源。
    4. 告诉 TC 执行状态。
  • 二阶段(分情况)
    • 提交:TC 通知所有 RM 删除自己的undo_log(快照没用了)。
    • 回滚:TC 通知所有 RM,根据undo_log里的快照,把数据恢复到修改前。
4.2.2 AT 和 XA 的区别:核心在 “提交时机”

两者最大的区别就三点:

  1. 提交时机:XA 一阶段不提交,锁定资源;AT 一阶段直接提交,释放资源,性能更好。
  2. 回滚方式:XA 依赖数据库原生回滚;AT 靠undo_log快照恢复。
  3. 一致性:XA 是强一致;AT 是最终一致。
4.2.3 脏写问题:AT 模式的 “小插曲”

AT 模式一阶段就提交事务,释放了数据库锁,可能出现 “脏写”。比如:

  • 事务 1:扣用户余额 10 元,一阶段提交,余额从 100→90,释放 DB 锁。
  • 事务 2:紧接着扣用户余额 10 元,一阶段提交,余额从 90→80,释放 DB 锁。
  • 此时事务 1 突然需要回滚,根据快照(余额 100)恢复,就会把事务 2 的修改覆盖,变成 100,这就是脏写。

解决办法:Seata 引入了 “全局锁”。在 RM 释放 DB 锁前,必须先拿到全局锁 —— 只有持有全局锁的事务,才能修改数据。比如上面的场景:

  • 事务 1 执行完,要释放 DB 锁前,先拿全局锁,拿到后才释放 DB 锁。
  • 事务 2 想执行,得先拿 DB 锁(能拿到),但拿全局锁时发现被事务 1 持有,就会重试(默认 30 次,间隔 10ms),重试失败就回滚。
  • 事务 1 回滚时,因为持有全局锁,能正常恢复数据,不会覆盖事务 2 的修改(因为事务 2 已经回滚了)。
4.2.4 AT 模式的优缺点:性能和一致的平衡
  • 优点
    1. 一阶段提交,释放资源快,性能比 XA 好很多。
    2. 全局锁解决脏写,保证读写隔离。
    3. 无代码侵入,快照和回滚都由框架自动做。
  • 缺点
    1. 两阶段之间是软状态,数据会有暂时不一致,最终才一致。
    2. 生成快照会消耗一点性能,但比 XA 好。
4.2.5 实现 AT 模式:加张表就行

AT 模式需要undo_log表(存快照)和lock_table(存全局锁),步骤如下:

  1. 创建 undo_log 表:每个微服务对应的数据库里,都要创建undo_log表(lock_table在部署 TC 时已经创建了),SQL 如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `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;

SET FOREIGN_KEY_CHECKS = 1;
  1. 开启 AT 模式:Seata 默认就是 AT 模式,所以 application.yml 里可以不配置,也可以显式指定:
seata:
  data-source-proxy-mode: AT # 默认值,可省略

  1. 标记全局事务入口:和 XA 模式一样,在发起全局事务的方法上加@GlobalTransactional注解。
  2. 重启测试:重启服务后,不管哪个步骤失败,Seata 会自动根据undo_log回滚数据,最终保持一致,而且性能比 XA 好。

总结

微服务事务管理的核心是在 “一致性” 和 “可用性” 之间找平衡:

  • 要强一致,选 Seata 的 XA 模式,简单无侵入,但性能一般。
  • 要高可用和性能,选 Seata 的 AT 模式(默认),最终一致,无侵入,适合大多数场景。

Seata 的优势在于易用性 —— 不用写复杂的补偿逻辑,配置一下、加个注解就能用,大大降低了分布式事务的实现成本。大家可以根据自己的业务场景,选择合适的事务模式。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐