目录
一、分布式事务问题的由来
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 命名空间 java2601)
Nacos 中添加 Seata 配置
4.2 依赖配置
4.3 完整业务代码
OrderService(事务发起方 TM)
StockController + StockService(库存服务 RM)
UserService + UserPointService(用户服务 RM)
4.4 XID 传递(Seata 自动完成)
五、进阶知识点
5.1 全局锁机制(防脏写) 不太准确!!!
5.2 脏写检测机制
5.3 性能优化
5.4 AT vs TCC 如何选择
六、常见问题排查
七、面试模拟 🎯
Q1:讲讲 Seata AT 模式的原理?
Q2:Seata AT 模式的全局锁是什么?为什么需要?
Q3:undo_log 是什么?存在哪里?
Q4:Seata 的 XID 是怎么传递的?
Q5:AT 模式和 TCC 模式怎么选?
Q6:Seata 的 TC、TM、RM 分别是什么?
Q7:Seata AT 模式下,如果回滚时数据已经被其他事务修改了怎么办?
Q8:@GlobalTransactional 的 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 是什么
Seata(Simple 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 Server)
3.2 AT 模式执行流程(一阶段)
以「扣减库存」为例:
stock-service 执行 UPDATE stock SET count = count - 2 WHERE product_id = 100 ┌──────────────────────────────────────────────┐ │ 一阶段(业务执行) │ └──────────────────────────────────────────────┘ 步骤 1:解析 SQL │ ▼ Seata JDBC 代理 DataSource 拦截到这条 SQL │ 解析出:表名=stock,操作=UPDATE,WHERE 条件 product_id=100 │ ▼ 步骤 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} 业务SQL:UPDATE stock SET count = count - 2 → count = 98 回滚SQL:UPDATE 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.yml(order-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_GROUP
default
Data ID:service.default.grouplistGroup:SEATA_GROUP
192.168.187.102:8091
4.2 依赖配置
<!-- Seata Spring Boot Starter --> <dependency> <groupId>org.apache.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>2.6.0</version> </dependency> <!-- Seata 集成 Nacos(注册中心+配置中心) --> <dependency> <groupId>org.apache.seata</groupId> <artifactId>seata-spring-registry-nacos</artifactId> <version>2.6.0</version> </dependency>4.3 完整业务代码
OrderService(事务发起方 TM)
@Service @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 Result<OrderVO> createOrder(CreateOrderDTO dto) { log.info("=== 开始全局事务 XID: {} ===", RootContext.getXID()); // ---- 分支事务 1:扣减库存(远程调用 stock-service)---- log.info("分支事务1:扣减库存,商品ID:{},数量:{}", dto.getProductId(), dto.getCount()); Result<Void> 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); Result<Void> 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(库存服务 RM)
@RestController @RequestMapping("/stock") public class StockController { @Autowired private StockService stockService; @PostMapping("/deduct") public Result<Void> 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(用户服务 RM)
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserPointService userPointService; @PostMapping("/points/deduct") public Result<Void> 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-service(TM) stock-service(RM) │ │ │ 调用时 Seata 拦截器自动添加 Header: │ │ SEATA_XID = "192.168.1.100:8091:1234"│ │──────────────────────────────────────►│ │ │ Seata Filter 从 Header 取出 XID │ │ 放入 RootContext(ThreadLocal) │ │ 后续 SQL 自动关联到这个全局事务
如果是异步调用(线程池、MQ),XID 不会自动传递,需要手动处理:
// 提交异步任务前获取 XID String xid = RootContext.getXID(); executor.submit(() -> { try { RootContext.bind(xid); // 手动绑定 // 业务逻辑... } finally { RootContext.unbind(); // 解绑 } });
五、进阶知识点
5.1 全局锁机制(防脏写) 不太准确!!!
场景:两个事务同时修改同一条数据 事务A(下单) 事务B(管理员手动改库存) │ │ ▼ ▼ 查询前镜像:count=100 查询前镜像:count=100 │ │ ▼ ▼ UPDATE count=98 UPDATE count=50 │ │ ▼ ▼ 申请全局锁 ✅ 成功 申请全局锁 ❌ 被阻塞(等待事务A完成) │ │ ▼ │ 提交事务,释放锁 │ │ ▼ │ 重新查询前镜像:count=98(最新值) │ UPDATE count=48 │ │ ▼ ▼ 事务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) ② 执行业务 SQL(UPDATE/INSERT/DELETE) ③ 查一次后镜像(SELECT) ④ 写一条 undo_log(INSERT) = 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 service | Seata Server 没启动或没注册到 Nacos | 检查 Seata Server 状态和 Nacos 注册 |
GlobalTransactionException: Could not found ... | 事务组映射配置缺失 | 在 Nacos 添加service.vgroupMapping.xxx配置 |
LockConflictException | 全局锁冲突,其他事务持有锁 | 等待重试或排查死锁 |
UndoLogDirtyException | 脏写检测失败,数据被其他事务改过 | 检查并发场景,考虑调整隔离策略 |
datasource not found | 数据源未被 Seata 代理 | 确认引入了seata-spring-boot-starter |
| Feign 调用时 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 释放锁。
Q2:Seata AT 模式的全局锁是什么?为什么需要?
参考答案:
全局锁是 Seata 在一阶段分支事务提交时,向 TC 申请的一把逻辑锁,锁的粒度是行级(表名 + 主键)。
需要全局锁的原因是防止脏写。比如事务 A 和事务 B 同时修改同一行数据,A 先提交了,B 的 undo_log 里记录的前镜像还是旧值。如果没有全局锁,B 回滚时会用旧值覆盖 A 的修改,造成数据错误。有了全局锁,B 在一阶段就会被阻塞,等 A 释放锁后再继续。
Q3:undo_log 是什么?存在哪里?
参考答案:
undo_log 是 AT 模式的核心表,记录了每条业务 SQL 执行前后的数据快照(前镜像和后镜像)。它存储在每个参与分布式事务的业务数据库中,而不是 Seata Server 的库里。
这样设计的目的是保证 undo_log 和业务数据在同一个数据库里,可以在同一个本地事务中提交,保证原子性。回滚时也是从同一个库里读取 undo_log,构造反向 SQL 还原数据。
Q4:Seata 的 XID 是怎么传递的?
参考答案:
XID(全局事务 ID)在同步调用场景下,Seata 会通过SeataFilter或SeataRestTemplateInterceptor自动将 XID 放到 HTTP 请求头(SEATA_XID)中传递给下游服务。下游服务收到请求后,从 Header 中取出 XID 放入RootContext(基于 ThreadLocal),后续的 SQL 操作就会自动关联到这个全局事务。
但是在异步调用场景下(线程池、MQ),ThreadLocal 中的 XID 不会自动传递,需要手动调用RootContext.getXID()获取,然后在异步线程中RootContext.bind(xid)绑定。
Q5:AT 模式和 TCC 模式怎么选?
参考答案:
AT 模式零侵入,Seata 自动拦截 SQL、自动生成和回滚 undo_log,开发成本低,适合大多数 CRUD 业务。但代价是每次 SQL 多了前镜像查询、后镜像查询和 undo_log 写入,性能有一定损耗。
TCC 模式需要手写 Try(预留资源)、Confirm(确认提交)、Cancel(取消补偿)三个接口,开发成本高,但没有 undo_log 的额外开销,性能更好,且锁粒度可以由业务自定义控制。
选择建议:80% 的业务用 AT 就够了,涉及资金、高频交易、对性能极其敏感的场景用 TCC。
Q6:Seata 的 TC、TM、RM 分别是什么?
参考答案:
TC(Transaction Coordinator):事务协调者,独立部署的 Seata Server,负责维护全局事务和分支事务的状态,驱动全局事务的提交或回滚。
TM(Transaction Manager):事务管理者,发起全局事务的服务(通常是入口服务),通过
@GlobalTransactional注解标识,负责开启、提交或回滚全局事务。RM(Resource Manager):资源管理者,每个参与分布式事务的服务,负责管理分支事务资源,向 TC 注册分支事务、报告分支状态、驱动分支的提交或回滚。
在下单场景中:用户服务是 TM(发起方),订单服务、库存服务、用户服务(扣积分部分)都是 RM,Seata Server 是 TC。
Q7:Seata AT 模式下,如果回滚时数据已经被其他事务修改了怎么办?
参考答案:
Seata 在回滚前会做脏写检测。它会将 undo_log 中保存的后镜像与数据库当前数据做对比,如果一致说明没有被其他事务改过,可以安全回滚;如果不一致,说明发生了脏写。
默认策略(strict)会直接抛出UndoLogDirtyException异常,需要人工介入处理。也可以配置为none跳过校验直接回滚,但有数据错误的风险,生产环境不建议。
Q8:@GlobalTransactional 的 rollbackFor 属性默认值是什么?不配置会有什么问题?
参考答案:
默认值是RuntimeException,也就是说只对运行时异常和 Error 进行回滚,受检异常(Checked Exception)不会触发回滚。
如果业务代码中抛出的是Exception或自定义的受检异常(如IOException、BusinessException extends Exception),Seata 不会回滚,导致数据不一致。
所以生产中必须显式配置rollbackFor = Exception.class,确保所有异常都能触发回滚。
下一篇:Redis —— 缓存实战,讲解下单场景中的库存缓存、热点商品处理、缓存一致性等。