Seata AT 分布式事务全解析

一、前言

在微服务架构下,业务会被拆分为多个独立服务,网约车系统便是典型代表:订单服务、账户服务、司机服务 各司其职。乘客下单流程需要跨多个服务完成数据变更,传统单机 @Transactional 无法保证跨服务、跨数据库的数据一致性,极易出现「订单创建成功、用户余额已冻结,但司机匹配失败」这类数据不一致问题,进而引发资损、客诉。

本文基于网约车项目(Spring Cloud Alibaba + Seata AT 模式),结合真实业务代码、脏读问题、服务重启异常场景,全方位讲解 Seata 工作原理、落地实践、问题排查与生产优化。

技术栈

  • 框架:Spring Cloud Alibaba、OpenFeign
  • 分布式事务:Seata 1.4+ AT 模式
  • 数据库:MySQL InnoDB
  • 业务场景:网约车乘客下单、账户冻结、司机接单

二、业务场景与分布式事务痛点

2.1 核心业务流程

乘客发起打车下单,串行执行三大跨服务操作:

  • 订单服务:创建出行订单,写入订单库;
  • 账户服务:冻结乘客账户余额,预扣费用;
  • 司机服务:匹配司机,并更新司机接单状态。

2.2 现存问题

  • 数据不一致
    任意环节出现异常(代码报错、网络中断),会出现部分服务执行成功、部分失败,造成资金与订单数据错乱。
  • 中间态脏读
    Seata AT 一阶段会直接提交本地事务,数据提前落库,高并发下查询接口会读到未最终提交 / 回滚的中间数据。
  • 服务重启异常
    事务执行过程中,订单、账户、司机服务甚至 Seata 服务端重启,如何保证事务不丢失、数据最终一致?
  • 锁粒度问题
    业务使用 order_id 作为业务唯一编号(仅唯一约束、非主键),Seata 自动锁机制失效。

三、Seata 基础概念与 AT 模式原理

3.1 Seata 三大核心组件

Seata 分布式事务由三个角色协同工作,所有微服务统一对接事务协调器:

  • TC(Transaction Coordinator)事务协调器
    独立部署的服务端,全局事务总指挥。持久化存储全局事务状态、分支事务状态、全局锁信息,负责下发提交 / 回滚指令。
  • TM(Transaction Manager)事务管理器
    运行在业务服务中,标记全局事务的起点和终点 ,对应注解 @GlobalTransactional,本文中订单服务为 TM。
  • RM(Resource Manager)资源管理器
    每个微服务都是 RM,管理本地数据库资源,记录回滚快照、执行本地提交 / 回滚、上报分支状态。本文订单、账户、司机服务均为 RM。

3.2 AT 模式两阶段执行流程

AT 模式是对传统 2PC 的优化,一阶段直接提交本地事务,性能远优于 XA,也是微服务主流选择。

阶段一:Prepare(准备阶段)

  1. TM 向 TC 申请全局事务,TC 生成全局唯一 ID XID,该 ID 通过 Feign 请求头全链路透传;
  2. 每个 RM 执行业务 SQL 前,记录数据快照到 undo_log 回滚日志表;
  3. 执行业务 SQL,并直接提交本地 MySQL 事务,数据落库;
  4. RM 向 TC 上报分支执行状态,同时为业务行(订单、账户)申请全局排他锁

阶段二:Commit / Rollback(决议阶段)

  1. 全局提交 :TC 收到所有分支执行成功,向所有 RM 下发提交指令。RM 删除本地 undo_log、释放全局锁,事务正常结束。
  2. 全局回滚 :任意分支执行失败,TC 立即下发回滚指令。RM 读取 undo_log 快照,生成反向 SQL 恢复数据,删除日志并释放锁。

3.3 跨服务事务一致性核心保障

  1. XID 全链路透传:Feign 拦截器自动传递 XID,将整条调用链绑定为同一个全局事务;
  2. TC 统一调度 :集中收集所有分支状态,一荣俱荣,一损俱损
  3. undo_log 本地快照:每个服务独立维护回滚数据,跨服务回滚无需远程调用;
  4. 全局行锁:事务未结束前锁定业务行,防止并发脏读、脏写。

四、项目环境与基础配置

4.1 Maven 依赖

所有微服务统一引入 Seata 依赖:

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| xml <!-- Spring Cloud Alibaba Seata --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> |

4.2 全局配置(application.yml)

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| yaml spring: cloud: seata: # 事务组名称,所有服务必须保持一致 tx-service-group: default_tx_group service: # Seata TC 服务地址 vgroup-mapping: default_tx_group: default grouplist: default: 127.0.0.1:8091 client: # 全局锁配置,解决脏读问题 lock: lock-retry-times: 10 # 锁重试次数 lock-retry-interval: 200 # 锁重试间隔(ms) |

4.3 数据库前置准备

每个业务数据库必须创建 undo_log 回滚日志表(Seata AT 模式必填):

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sql 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |

|------------------------------------------------------------------------|
| 业务说明:项目中 order_info 表 id 为自增主键,order_id 为唯一约束(非主键) ,后续需要手动指定锁 Key。 |

五、核心代码落地实现

5.1 远程调用 Feign 接口

账户服务 Feign

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @FeignClient("account-service") public interface AccountFeign { @PostMapping("/account/freeze") boolean freezeBalance(@RequestParam("userId") Long userId, @RequestParam("money") Integer money); } |

司机服务 Feign

|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @FeignClient("driver-service") public interface DriverFeign { @PostMapping("/driver/match") boolean matchDriver(@RequestParam("orderId") String orderId); } |

5.2 下游服务实现

账户服务

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @RestController public class AccountController { @Resource private AccountService accountService; @PostMapping("/account/freeze") public boolean freezeBalance(@RequestParam Long userId, @RequestParam Integer money) { return accountService.freezeUserBalance(userId, money); } } @Service class AccountService { @Transactional(rollbackFor = Exception.class) public boolean freezeUserBalance(Long userId, Integer money) { System.out.println("账户服务:冻结用户" + userId + " 金额:" + money); // 模拟数据库更新余额逻辑 return true; } } |

司机服务

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @RestController public class DriverController { @Resource private DriverService driverService; @PostMapping("/driver/match") public boolean matchDriver(@RequestParam String orderId) { return driverService.matchDriver(orderId); } } @Service class DriverService { @Transactional(rollbackFor = Exception.class) public boolean matchDriver(String orderId) { System.out.println("司机服务:为订单" + orderId + " 匹配司机"); // 模拟异常,用于测试全局回滚 // int a = 1 / 0; return true; } } |

5.3 订单服务(全局事务入口)

由于 order_id 仅唯一约束、非主键 ,Seata 无法自动解析锁字段,必须手动指定 lockKey

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @Data public class Order { private Long id; private String orderId; private Long userId; private Integer amount; private Integer status; // 0-中间态 1-有效 9-作废 private LocalDateTime createTime; } @Service public class OrderService { @Resource private AccountFeign accountFeign; @Resource private DriverFeign driverFeign; @Resource private OrderMapper orderMapper; /** * 全局事务入口 * lockKey:手动锁定唯一字段 order_id,格式=表名:字段名:入参 */ @GlobalTransactional(rollbackFor = Exception.class, lockKey = "order_info:order_id:#{#order.orderId}") public Order createOrder(Order order) { // 1. 写入订单(中间态,对外查询不可见) order.setStatus(0); orderMapper.insert(order); System.out.println("订单服务:创建中间态订单 orderId = " + order.getOrderId()); // 2. 远程调用账户服务冻结余额 accountFeign.freezeBalance(order.getUserId(), order.getAmount()); // 3. 远程调用司机服务匹配司机 driverFeign.matchDriver(order.getOrderId()); // 4. 全部执行成功,更新为有效订单 order.setStatus(1); orderMapper.updateStatus(order); return order; } } |

5.4 订单查询接口(解决脏读,全局锁 + 手动指定锁 Key)

AT 模式一阶段数据已落库,会产生中间态脏读,查询接口通过 @GlobalLock 加全局锁,同时手动绑定 order_id:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @Service public class OrderQueryService { @Resource private OrderMapper orderMapper; /** * 全局锁查询:防止读取事务中间态脏数据 * 手动指定 lockKey 锁定唯一字段 order_id */ @GlobalLock(lockKey = "order_info:order_id:#{#orderId}") @Transactional(rollbackFor = Exception.class) public Order getOrder(String orderId) { // 业务过滤:只查询有效订单,双重兜底防脏数据 return orderMapper.selectByOrderIdAndStatus(orderId, 1); } } |

5.5 对外接口

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java @RestController @RequestMapping("/order") public class OrderController { @Resource private OrderService orderService; @Resource private OrderQueryService queryService; @PostMapping("/create") public Object createOrder(@RequestBody Order order) { try { return orderService.createOrder(order); } catch (Exception e) { return "下单失败:" + e.getMessage(); } } @GetMapping("/get") public Object getOrder(@RequestParam String orderId) { return queryService.getOrder(orderId); } } |

六、核心问题解决方案实战

6.1 问题 1:AT 模式脏读问题

成因

AT 一阶段直接提交本地事务,数据提前落库,查询接口会读到事务未完成的中间数据。

双重解决方案(网约车生产最优方案)

  1. Seata 全局锁 :查询接口添加 @GlobalLock,请求 TC 行级锁,事务未完成则等待重试;
  2. 业务状态过滤 :订单新增默认状态 0(中间态),事务全部成功后才改为 1(有效态),所有查询强制过滤中间状态。

6.2 问题 2:order_id 为唯一约束、非主键,锁失效

成因

Seata 自动锁机制仅识别主键,无法解析普通唯一索引字段。

解决方案

放弃自动解析,在 @GlobalTransactional 和 @GlobalLock 上手动指定 lockKey

|-----------------------------------|
| java lockKey = "表名:唯一字段名:#{方法入参}" |

该方式精准锁定业务唯一编号,不受主键限制。

6.3 问题 3:事务执行中服务重启

结合 Seata 持久化机制,分三种场景说明:

场景 1:TM(订单服务)重启

  • 订单服务重启后丢失 XID 上下文;
  • TC 定时扫描长时间未结束的事务,检测到 TM 失联;
  • TC 主动下发全局回滚指令;
  • 所有 RM 根据本地 undo_log 回滚数据、释放全局锁。
    结果:数据完全一致,下单失败,支持前端重试。

场景 2:RM(账户 / 司机服务)重启

  • 下游服务重启后重新注册到 TC;
  • TC 校验分支事务状态,继续执行二阶段(提交 / 回滚);
  • RM 读取本地 undo_log 完成收尾操作。
    结果 :事务自动续跑,数据最终一致;重启期间 order_id 全局锁保持占用,防止脏写。

场景 3:TC(Seata 服务端)重启

  • TC 所有事务状态、锁信息持久化在 MySQL;
  • 重启后自动加载未完成事务,继续调度;
  • 生产环境建议 TC 集群部署,避免单点故障。

6.4 问题 4:锁残留与超时

极端网络 + 服务重启会导致全局锁无法正常释放,出现 LockConflictException:

  • 配置锁重试次数与间隔,避免无限阻塞;
  • 开启 TC 自动清理超时锁(默认 30s);
  • 增加监控告警,长期锁占用及时人工介入。

七、测试验证

7.1 正常流程测试

请求下单接口,订单、账户、司机服务全部执行成功:

  1. 订单状态更新为 1,undo_log 日志删除;
  2. 全局锁释放;
  3. 查询接口正常读取有效订单。

7.2 异常回滚测试

打开司机服务模拟异常代码 int a = 1 / 0:

  1. 订单、账户服务一阶段执行完成;
  2. 司机服务报错,上报 TC 分支失败;
  3. TC 下发全局回滚;
  4. 订单恢复为中间态 / 删除,账户余额解冻,所有 undo_log 清理,锁释放。

7.3 脏读测试

在事务执行中途调用查询接口:

  1. 查询接口触发全局锁,进入重试等待;
  2. 事务完成、锁释放后,查询才执行;
  3. 无法读取中间态数据。

八、生产环境总结与最佳实践

8.1 架构规范

  1. 优先使用 Seata AT 模式,代码侵入低、性能高,适配网约车高并发订单场景;
  2. TC 必须集群部署 + 数据库持久化,杜绝单点故障;
  3. undo_log 与业务库同实例,禁止手动删除。

8.2 锁使用规范

  1. 业务编号若为唯一约束非主键 ,必须手动配置 lockKey;
  2. 核心查询接口统一添加 @GlobalLock,配合业务状态过滤双重防脏读;
  3. 合理配置锁重试参数,避免高并发下超时。

8.3 异常兜底规范

  1. 核心流程拆分中间状态,从业务层屏蔽脏数据;
  2. 增加定时任务,扫描超时中间态订单,自动作废并解冻资金;
  3. 监控 undo_log、全局锁、事务异常,及时告警。

8.4 服务容灾

充分利用 Seata 重试、恢复机制,服务重启后事务自动续跑 / 回滚,保证微服务容灾能力。

九、结语

Seata AT 模式凭借低侵入、高性能、易落地的特性,成为 Spring Cloud 微服务分布式事务的首选方案。在网约车这类对数据一致性、资金安全要求极高的场景中,不仅要掌握基础使用,还要吃透原理、应对脏读、服务重启、锁失效等线上真实问题。