Seata AT 模式 —— 分布式事务

📅 2026/7/8 7:11:41
Seata AT 模式 —— 分布式事务
目录一、分布式事务问题的由来1.1 单体时代一个数据库搞定1.2 微服务时代三个数据库怎么办二、Seata 是什么2.1 四种事务模式三、AT 模式核心原理重点3.1 整体架构3.2 AT 模式执行流程一阶段3.3 AT 模式执行流程二阶段 —— 提交3.4 AT 模式执行流程二阶段 —— 回滚3.5 完整时序图四、电商场景实战4.1 环境准备undo_log 表建在每个业务库中Seata 配置Nacos 命名空间 java2601Nacos 中添加 Seata 配置4.2 依赖配置4.3 完整业务代码OrderService事务发起方 TMStockController StockService库存服务 RMUserService UserPointService用户服务 RM4.4 XID 传递Seata 自动完成五、进阶知识点5.1 全局锁机制防脏写 不太准确5.2 脏写检测机制5.3 性能优化5.4 AT vs TCC 如何选择六、常见问题排查七、面试模拟 Q1讲讲 Seata AT 模式的原理Q2Seata AT 模式的全局锁是什么为什么需要Q3undo_log 是什么存在哪里Q4Seata 的 XID 是怎么传递的Q5AT 模式和 TCC 模式怎么选Q6Seata 的 TC、TM、RM 分别是什么Q7Seata AT 模式下如果回滚时数据已经被其他事务修改了怎么办Q8GlobalTransactional 的 rollbackFor 属性默认值是什么不配置会有什么问题版本要求Seata 2.6.0 场景贯穿电商系统「下单功能」—— 库存扣减 积分扣减 订单创建的一致性保障一、分布式事务问题的由来1.1 单体时代一个数据库搞定单体应用 单个数据库 ​ BEGIN TRANSACTION INSERT INTO orders (...) -- 创建订单 UPDATE stock SET count count - 2 -- 扣减库存 UPDATE user SET points points - 200 -- 扣减积分 COMMIT ​ 一切在一个本地事务里要么全成功要么全回滚。1.2 微服务时代三个数据库怎么办微服务拆分后 ​ order-service → order_db 订单库 stock-service → stock_db 库存库 user-service → user_db 用户库 ​ 问题 订单创建成功了 ✅ 库存扣减成功了 ✅ 积分扣减失败了 ❌ → 订单和库存已经提交积分没扣数据不一致本地事务的 ACID 保证不了跨服务、跨数据库的操作这就是分布式事务要解决的问题。二、Seata 是什么SeataSimple Extensible Autonomous Transaction Architecture是阿里开源的分布式事务解决方案。2.1 四种事务模式模式原理侵入性性能适用场景AT自动生成 undo_log自动回滚零侵入高大多数业务场景推荐TCC手写 Try/Confirm/Cancel高很高资金等强一致性场景Saga长事务正向操作补偿操作中高长流程业务如审批流XA数据库层面 2PC零侵入低对一致性要求极高的场景本文重点讲AT 模式企业用得最多零代码侵入。三、AT 模式核心原理重点3.1 整体架构┌─────────────────────────────────┐ │ Seata Server (TC) │ │ 192.168.187.102:8091 │ │ │ │ global_table ← 全局事务状态 │ │ branch_table ← 分支事务状态 │ │ lock_table ← 全局锁 │ └──────────┬──────────────────────┘ │ ┌──────────────┼──────────────┐ │ │ │ 注册/汇报 注册/汇报 注册/汇报 │ │ │ ┌───────┴───┐ ┌───────┴───┐ ┌───────┴───┐ │ order-svc │ │ stock-svc │ │ user-svc │ │ (TM) │ │ (RM) │ │ (RM) │ │ │ │ │ │ │ │ undo_log │ │ undo_log │ │ undo_log │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ ┌─────┴─────┐ ┌────┴──────┐ ┌────┴──────┐ │ order_db │ │ stock_db │ │ user_db │ └───────────┘ └───────────┘ └───────────┘ ​ TM Transaction Manager事务发起者通常是入口服务 RM Resource Manager资源管理者每个参与服务 TC Transaction Coordinator事务协调者Seata Server3.2 AT 模式执行流程一阶段以「扣减库存」为例stock-service 执行 UPDATE stock SET count count - 2 WHERE product_id 100 ​ ┌──────────────────────────────────────────────┐ │ 一阶段业务执行 │ └──────────────────────────────────────────────┘ ​ 步骤 1解析 SQL │ ▼ Seata JDBC 代理 DataSource 拦截到这条 SQL │ 解析出表名stock操作UPDATEWHERE 条件 product_id100 │ ▼ 步骤 2查询前镜像Before Image │ ▼ SELECT * FROM stock WHERE product_id 100 │ 结果{product_id: 100, count: 100, name: iPhone 16} │ 保存到 undo_log 的 rollbackInfo 字段 │ ▼ 步骤 3执行业务 SQL │ ▼ UPDATE stock SET count count - 2 WHERE product_id 100 │ 数据库实际变更count 从 100 变成 98 │ ▼ 步骤 4查询后镜像After Image │ ▼ SELECT * FROM stock WHERE product_id 100 │ 结果{product_id: 100, count: 98, name: iPhone 16} │ ▼ 步骤 5生成 undo_log 并提交 │ ▼ INSERT INTO undo_log (branch_id, xid, rollback_info, ...) │ rollbackInfo {beforeImage, afterImage, SQL类型, 表名...} │ ▼ 步骤 6向 TC 注册分支事务 申请全局锁 │ ▼ TC 记录 branch_table获得全局锁 │ ▼ 步骤 7本地事务提交 undo_log 和业务 SQL 在同一个本地事务中一起提交关键点undo_log 和业务变更在同一个本地事务里提交要么都成功要么都失败。3.3 AT 模式执行流程二阶段 —— 提交所有分支事务都成功 → TC 通知各 RM 提交 ​ ┌──────────────────────────────────────────────┐ │ 二阶段提交异步 │ └──────────────────────────────────────────────┘ ​ TC ──通知──► RM各分支 ​ RM 收到后 ① 删除 undo_log异步清理不需要等 ② 释放全局锁 ​ 完成。业务数据已经提交undo_log 只是历史记录删掉即可。3.4 AT 模式执行流程二阶段 —— 回滚某个分支事务失败 → TC 通知各 RM 回滚 ​ ┌──────────────────────────────────────────────┐ │ 二阶段回滚 │ └──────────────────────────────────────────────┘ ​ TC ──通知──► RM各分支 ​ RM 收到回滚指令后 ① 根据 xid branch_id 查出 undo_log ② 取出前镜像Before Image ③ 校验数据是否被修改过后镜像 vs 当前数据 ├─ 一致 → 可以安全回滚 └─ 不一致 → 说明被其他事务改过了需要人工处理脏写 ④ 用前镜像构造反向 SQL 回滚 ​ 回滚 SQL 示例 前镜像{product_id: 100, count: 100} 业务SQLUPDATE stock SET count count - 2 → count 98 回滚SQLUPDATE stock SET count 100 WHERE product_id 100 → 恢复原值 ​ ⑤ 删除 undo_log ⑥ 释放全局锁3.5 完整时序图用户下单流程完整时序 ​ 用户服务(TM) Seata Server(TC) 订单服务(RM) 库存服务(RM) 用户服务(RM) │ │ │ │ │ │ ①开启全局事务 │ │ │ │ │─────────────────────►│ │ │ │ │ 返回全局事务XID │ │ │ │ │◄─────────────────────│ │ │ │ │ │ │ │ │ │ ②携带XID 调用创建订单 │ │ │ │ │─────────────────────────────────────────────►│ │ │ │ │ │ │ │ │ │ ③注册分支事务写undo_log│ │ │ │ │◄─────────────────────│ │ │ │ │ 返回分支事务ID │ │ │ │ │─────────────────────►│ │ │ │ │ │ │ │ │ │ ④携带XID 调用扣减库存 │ │ │ │──────────────────────────────────────────►│ │ │ │ │ │ │ │ │ ⑤注册分支事务写undo_log│ │ │ │ │◄──────────────────────────────────────────│ │ │ │ │ │ │ │ │ ⑥携带XID 调用扣减积分 │ │ │──────────────────────────────────────────────────────────────►│ │ │ │ │ │ │ │ ⑦注册分支事务写undo_log │ │ │◄──────────────────────────────────────────────────────────────│ │ │ │ │ │ │◄────────────────────────────────────────────返回结果──────────────────────────────│ │ │ │ │ │ │ 全部成功 │ │ │ │ │ ├─ 是⑧提交全局事务 │ │ │ │ │ │ ────────────────►│ 异步删除undo_log 释放锁│ │ │ │ └─ 否⑧回滚全局事务 │ │ │ │ │ ──────────────►│ 用undo_log反向SQL回滚 │ │ │ │ │ │ │ │四、电商场景实战4.1 环境准备undo_log 表建在每个业务库中-- 在 order_db、stock_db、user_db 中分别执行 CREATE TABLE IF NOT EXISTS undo_log ( id bigint NOT NULL AUTO_INCREMENT, branch_id bigint NOT NULL, xid varchar(128) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int 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 DEFAULT CHARSET utf8mb4;Seata 配置Nacos 命名空间 java2601# bootstrap.ymlorder-service spring: cloud: nacos: server-addr: 192.168.187.102:8848 username: nacos password: nacos alibaba: seata: tx-service-group: my_tx_group # 事务组名 ​ # application.yml seata: enabled: true registry: type: nacos nacos: server-addr: 192.168.187.102:8848 namespace: 824f9d83-5700-4ef6-9db9-f6034ae8a8c5 group: SEATA_GROUP username: nacos password: nacos config: type: nacos nacos: server-addr: 192.168.187.102:8848 namespace: 824f9d83-5700-4ef6-9db9-f6034ae8a8c5 group: SEATA_GROUP username: nacos password: nacosNacos 中添加 Seata 配置在 Nacos 控制台 java2601 命名空间下创建配置Data ID:service.vgroupMapping.my_tx_groupGroup:SEATA_GROUPdefaultData ID:service.default.grouplistGroup:SEATA_GROUP192.168.187.102:80914.2 依赖配置!-- Seata Spring Boot Starter -- dependency groupIdorg.apache.seata/groupId artifactIdseata-spring-boot-starter/artifactId version2.6.0/version /dependency ​ !-- Seata 集成 Nacos注册中心配置中心 -- dependency groupIdorg.apache.seata/groupId artifactIdseata-spring-registry-nacos/artifactId version2.6.0/version /dependency4.3 完整业务代码OrderService事务发起方 TMService Slf4j public class OrderService { ​ Autowired private OrderMapper orderMapper; ​ Autowired private StockFeignClient stockFeignClient; ​ Autowired private UserFeignClient userFeignClient; ​ /** * 创建订单 * * GlobalTransactional 是 Seata 的核心注解 * - name: 事务名称用于监控和日志 * - rollbackFor: 哪些异常触发回滚默认只回滚 RuntimeException * - timeoutMills: 全局事务超时时间ms */ GlobalTransactional( name create-order, rollbackFor Exception.class, timeoutMills 30000 ) public ResultOrderVO createOrder(CreateOrderDTO dto) { log.info( 开始全局事务 XID: {} , RootContext.getXID()); ​ // ---- 分支事务 1扣减库存远程调用 stock-service---- log.info(分支事务1扣减库存商品ID{}数量{}, dto.getProductId(), dto.getCount()); ResultVoid stockResult stockFeignClient.deductStock( dto.getProductId(), dto.getCount() ); if (!stockResult.isSuccess()) { throw new BusinessException(库存扣减失败 stockResult.getMessage()); } log.info(分支事务1库存扣减成功); ​ // ---- 分支事务 2扣减积分远程调用 user-service---- int points dto.getCount() * 100; log.info(分支事务2扣减积分用户ID{}积分{}, dto.getUserId(), points); ResultVoid pointResult userFeignClient.deductPoints( dto.getUserId(), points ); if (!pointResult.isSuccess()) { throw new BusinessException(积分扣减失败 pointResult.getMessage()); } log.info(分支事务2积分扣减成功); ​ // ---- 分支事务 3创建订单本地操作---- log.info(分支事务3创建订单); Order order new Order(); order.setUserId(dto.getUserId()); order.setProductId(dto.getProductId()); order.setCount(dto.getCount()); order.setTotalAmount(dto.getCount() * 999.00); // 假设单价 999 order.setStatus(OrderStatus.CREATED.getCode()); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); log.info(分支事务3订单创建成功订单ID{}, order.getId()); ​ // ---- 全部成功返回 ---- OrderVO vo new OrderVO(); BeanUtils.copyProperties(order, vo); return Result.ok(vo); ​ // 如果任何一步抛异常GlobalTransactional 会触发全局回滚 // TC 通知所有 RM 用 undo_log 还原数据 } }StockController StockService库存服务 RMRestController RequestMapping(/stock) public class StockController { ​ Autowired private StockService stockService; ​ PostMapping(/deduct) public ResultVoid deductStock(RequestParam(productId) Long productId, RequestParam(count) Integer count) { stockService.deductStock(productId, count); return Result.ok(); } } ​ Service public class StockService { ​ Autowired private StockMapper stockMapper; ​ /** * 扣减库存 * 不需要加任何 Seata 注解 * Seata 通过代理 DataSource 自动拦截 SQL自动写 undo_log */ Transactional // 本地事务即可 public void deductStock(Long productId, Integer count) { // 检查库存 Stock stock stockMapper.selectByProductId(productId); if (stock null || stock.getCount() count) { throw new BusinessException(库存不足); } // 扣减Seata 自动拦截这条 SQL记录前镜像和后镜像 stockMapper.deductStock(productId, count); } }UserService UserPointService用户服务 RMRestController RequestMapping(/user) public class UserController { ​ Autowired private UserPointService userPointService; ​ PostMapping(/points/deduct) public ResultVoid deductPoints(RequestParam(userId) Long userId, RequestParam(points) Integer points) { userPointService.deductPoints(userId, points); return Result.ok(); } } ​ Service public class UserPointService { ​ Autowired private UserPointMapper userPointMapper; ​ Transactional public void deductPoints(Long userId, Integer points) { UserPoint userPoint userPointMapper.selectByUserId(userId); if (userPoint null || userPoint.getPoints() points) { throw new BusinessException(积分不足); } userPointMapper.deductPoints(userId, points); } }4.4 XID 传递Seata 自动完成你可能会问订单服务调用库存服务时Seata 怎么知道这是同一个全局事务 ​ 答案XID全局事务ID通过 HTTP Header 自动传递 ​ order-serviceTM stock-serviceRM │ │ │ 调用时 Seata 拦截器自动添加 Header: │ │ SEATA_XID 192.168.1.100:8091:1234│ │──────────────────────────────────────►│ │ │ Seata Filter 从 Header 取出 XID │ │ 放入 RootContextThreadLocal │ │ 后续 SQL 自动关联到这个全局事务如果是异步调用线程池、MQXID 不会自动传递需要手动处理// 提交异步任务前获取 XID String xid RootContext.getXID(); executor.submit(() - { try { RootContext.bind(xid); // 手动绑定 // 业务逻辑... } finally { RootContext.unbind(); // 解绑 } });五、进阶知识点5.1 全局锁机制防脏写 不太准确场景两个事务同时修改同一条数据 ​ 事务A下单 事务B管理员手动改库存 │ │ ▼ ▼ 查询前镜像count100 查询前镜像count100 │ │ ▼ ▼ UPDATE count98 UPDATE count50 │ │ ▼ ▼ 申请全局锁 ✅ 成功 申请全局锁 ❌ 被阻塞等待事务A完成 │ │ ▼ │ 提交事务释放锁 │ │ ▼ │ 重新查询前镜像count98最新值 │ UPDATE count48 │ │ ▼ ▼ 事务A完成 事务B完成全局锁的作用防止并发事务对同一条数据的脏写。没有全局锁的话事务B用旧的前镜像回滚会覆盖事务A的修改。5.2 脏写检测机制二阶段回滚时的校验 ​ 回滚前Seata 会做一次校验 ​ SELECT * FROM stock WHERE product_id 100 ​ 取后镜像执行时的值count 98 取当前数据库值 count 95 ← 被其他事务改过了 ​ 对比98 ≠ 95 → 发生了脏写 ​ 处理策略seata.undo.logValidation 配置 - strict默认: 抛异常需要人工介入 - none: 不校验直接用前镜像覆盖有风险5.3 性能优化AT 模式的性能瓶颈在哪 ​ 每次业务 SQL 都要 ① 查一次前镜像SELECT ② 执行业务 SQLUPDATE/INSERT/DELETE ③ 查一次后镜像SELECT ④ 写一条 undo_logINSERT ​ 1 条业务 SQL → 4 次数据库操作 ​ 优化手段 ┌─────────────────────────────────────────┐ │ 1. 关闭后镜像校验能减少一次 SELECT │ │ seata.undo.logValidation false │ │ 注意牺牲了脏写检测能力 │ │ │ │ 2. 合并数据源减少分支事务数量 │ │ 如果多个操作在同一个库尽量在一个服务里 │ │ │ │ 3. 批量操作优化 │ │ Seata 对批量 INSERT 有优化会合并 undo │ │ │ │ 4. 高并发场景考虑用 TCC 模式 │ │ TCC 不需要 undo_log性能更好 │ └─────────────────────────────────────────┘5.4 AT vs TCC 如何选择AT 模式 TCC 模式 开发成本 零侵入只加注解 高手写三个接口 性能 中等多了查询undo_log 高无额外开销 一致性 最终一致 最终一致 锁粒度 行级全局锁 业务自定义 适用场景 大多数 CRUD 业务 资金、库存等高并发场景 回滚依赖 undo_log 自动回滚 补偿代码手动回滚 ​ 选择建议 ✅ 入门首选 AT 模式 ✅ 80% 的业务用 AT 就够了 ✅ 涉及资金、高频交易、对性能敏感 → TCC六、常见问题排查问题原因解决方案no available serviceSeata Server 没启动或没注册到 Nacos检查 Seata Server 状态和 Nacos 注册GlobalTransactionException: Could not found ...事务组映射配置缺失在 Nacos 添加service.vgroupMapping.xxx配置LockConflictException全局锁冲突其他事务持有锁等待重试或排查死锁UndoLogDirtyException脏写检测失败数据被其他事务改过检查并发场景考虑调整隔离策略datasource not found数据源未被 Seata 代理确认引入了seata-spring-boot-starterFeign 调用时 XID 丢失异步线程中 XID 不传递手动RootContext.bind(xid)undo_log 表报Duplicate entry同一全局事务重复回滚Seata 自动重试可忽略或检查网络七、自测小题 Q1讲讲 Seata AT 模式的原理参考答案AT 模式的核心是基于 undo_log 的自动补偿机制分为两个阶段一阶段Seata 通过代理 DataSource 拦截业务 SQL执行前先查一次前镜像保存到 undo_log执行业务 SQL 后再查一次后镜像也保存到 undo_log然后向 TC 注册分支事务并申请全局锁最后将 undo_log 和业务 SQL 在同一个本地事务中提交。二阶段提交如果所有分支都成功TC 通知各分支异步删除 undo_log 并释放全局锁。二阶段回滚如果某个分支失败TC 通知其他分支回滚分支根据 undo_log 中的前镜像构造反向 SQL 恢复数据并在回滚前校验后镜像与当前数据是否一致脏写检测最后删除 undo_log 释放锁。Q2Seata AT 模式的全局锁是什么为什么需要参考答案全局锁是 Seata 在一阶段分支事务提交时向 TC 申请的一把逻辑锁锁的粒度是行级表名 主键。需要全局锁的原因是防止脏写。比如事务 A 和事务 B 同时修改同一行数据A 先提交了B 的 undo_log 里记录的前镜像还是旧值。如果没有全局锁B 回滚时会用旧值覆盖 A 的修改造成数据错误。有了全局锁B 在一阶段就会被阻塞等 A 释放锁后再继续。Q3undo_log 是什么存在哪里参考答案undo_log 是 AT 模式的核心表记录了每条业务 SQL 执行前后的数据快照前镜像和后镜像。它存储在每个参与分布式事务的业务数据库中而不是 Seata Server 的库里。这样设计的目的是保证 undo_log 和业务数据在同一个数据库里可以在同一个本地事务中提交保证原子性。回滚时也是从同一个库里读取 undo_log构造反向 SQL 还原数据。Q4Seata 的 XID 是怎么传递的参考答案XID全局事务 ID在同步调用场景下Seata 会通过SeataFilter或SeataRestTemplateInterceptor自动将 XID 放到 HTTP 请求头SEATA_XID中传递给下游服务。下游服务收到请求后从 Header 中取出 XID 放入RootContext基于 ThreadLocal后续的 SQL 操作就会自动关联到这个全局事务。但是在异步调用场景下线程池、MQThreadLocal 中的 XID 不会自动传递需要手动调用RootContext.getXID()获取然后在异步线程中RootContext.bind(xid)绑定。Q5AT 模式和 TCC 模式怎么选参考答案AT 模式零侵入Seata 自动拦截 SQL、自动生成和回滚 undo_log开发成本低适合大多数 CRUD 业务。但代价是每次 SQL 多了前镜像查询、后镜像查询和 undo_log 写入性能有一定损耗。TCC 模式需要手写 Try预留资源、Confirm确认提交、Cancel取消补偿三个接口开发成本高但没有 undo_log 的额外开销性能更好且锁粒度可以由业务自定义控制。选择建议80% 的业务用 AT 就够了涉及资金、高频交易、对性能极其敏感的场景用 TCC。Q6Seata 的 TC、TM、RM 分别是什么参考答案TCTransaction Coordinator事务协调者独立部署的 Seata Server负责维护全局事务和分支事务的状态驱动全局事务的提交或回滚。TMTransaction Manager事务管理者发起全局事务的服务通常是入口服务通过GlobalTransactional注解标识负责开启、提交或回滚全局事务。RMResource Manager资源管理者每个参与分布式事务的服务负责管理分支事务资源向 TC 注册分支事务、报告分支状态、驱动分支的提交或回滚。在下单场景中用户服务是 TM发起方订单服务、库存服务、用户服务扣积分部分都是 RMSeata Server 是 TC。Q7Seata AT 模式下如果回滚时数据已经被其他事务修改了怎么办参考答案Seata 在回滚前会做脏写检测。它会将 undo_log 中保存的后镜像与数据库当前数据做对比如果一致说明没有被其他事务改过可以安全回滚如果不一致说明发生了脏写。默认策略strict会直接抛出UndoLogDirtyException异常需要人工介入处理。也可以配置为none跳过校验直接回滚但有数据错误的风险生产环境不建议。Q8GlobalTransactional 的 rollbackFor 属性默认值是什么不配置会有什么问题参考答案默认值是RuntimeException也就是说只对运行时异常和 Error 进行回滚受检异常Checked Exception不会触发回滚。如果业务代码中抛出的是Exception或自定义的受检异常如IOException、BusinessException extends ExceptionSeata 不会回滚导致数据不一致。所以生产中必须显式配置rollbackFor Exception.class确保所有异常都能触发回滚。下一篇Redis —— 缓存实战讲解下单场景中的库存缓存、热点商品处理、缓存一致性等。