一、前言
在微服务架构下,业务会被拆分为多个独立服务,网约车系统便是典型代表:订单服务、账户服务、司机服务 各司其职。乘客下单流程需要跨多个服务完成数据变更,传统单机 @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(准备阶段)
- TM 向 TC 申请全局事务,TC 生成全局唯一 ID XID,该 ID 通过 Feign 请求头全链路透传;
- 每个 RM 执行业务 SQL 前,记录数据快照到 undo_log 回滚日志表;
- 执行业务 SQL,并直接提交本地 MySQL 事务,数据落库;
- RM 向 TC 上报分支执行状态,同时为业务行(订单、账户)申请全局排他锁。
阶段二:Commit / Rollback(决议阶段)
- 全局提交 :TC 收到所有分支执行成功,向所有 RM 下发提交指令。RM 删除本地 undo_log、释放全局锁,事务正常结束。
- 全局回滚 :任意分支执行失败,TC 立即下发回滚指令。RM 读取 undo_log 快照,生成反向 SQL 恢复数据,删除日志并释放锁。
3.3 跨服务事务一致性核心保障
- XID 全链路透传:Feign 拦截器自动传递 XID,将整条调用链绑定为同一个全局事务;
- TC 统一调度 :集中收集所有分支状态,一荣俱荣,一损俱损;
- undo_log 本地快照:每个服务独立维护回滚数据,跨服务回滚无需远程调用;
- 全局行锁:事务未结束前锁定业务行,防止并发脏读、脏写。
四、项目环境与基础配置
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 一阶段直接提交本地事务,数据提前落库,查询接口会读到事务未完成的中间数据。
双重解决方案(网约车生产最优方案)
- Seata 全局锁 :查询接口添加 @GlobalLock,请求 TC 行级锁,事务未完成则等待重试;
- 业务状态过滤 :订单新增默认状态 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,undo_log 日志删除;
- 全局锁释放;
- 查询接口正常读取有效订单。
7.2 异常回滚测试
打开司机服务模拟异常代码 int a = 1 / 0:
- 订单、账户服务一阶段执行完成;
- 司机服务报错,上报 TC 分支失败;
- TC 下发全局回滚;
- 订单恢复为中间态 / 删除,账户余额解冻,所有 undo_log 清理,锁释放。
7.3 脏读测试
在事务执行中途调用查询接口:
- 查询接口触发全局锁,进入重试等待;
- 事务完成、锁释放后,查询才执行;
- 无法读取中间态数据。
八、生产环境总结与最佳实践
8.1 架构规范
- 优先使用 Seata AT 模式,代码侵入低、性能高,适配网约车高并发订单场景;
- TC 必须集群部署 + 数据库持久化,杜绝单点故障;
- undo_log 与业务库同实例,禁止手动删除。
8.2 锁使用规范
- 业务编号若为唯一约束非主键 ,必须手动配置 lockKey;
- 核心查询接口统一添加 @GlobalLock,配合业务状态过滤双重防脏读;
- 合理配置锁重试参数,避免高并发下超时。
8.3 异常兜底规范
- 核心流程拆分中间状态,从业务层屏蔽脏数据;
- 增加定时任务,扫描超时中间态订单,自动作废并解冻资金;
- 监控 undo_log、全局锁、事务异常,及时告警。
8.4 服务容灾
充分利用 Seata 重试、恢复机制,服务重启后事务自动续跑 / 回滚,保证微服务容灾能力。
九、结语
Seata AT 模式凭借低侵入、高性能、易落地的特性,成为 Spring Cloud 微服务分布式事务的首选方案。在网约车这类对数据一致性、资金安全要求极高的场景中,不仅要掌握基础使用,还要吃透原理、应对脏读、服务重启、锁失效等线上真实问题。