@Transactional在微服务跨服务调用中必然失效,因其仅限当前JVM内生效;HTTP/gRPC调用会断开事务上下文,下游服务开启独立事务,无法保证原子性,必须禁止跨服务大事务,改用本地事务+消息表等最终一致性方案。

微服务架构下,MySQL大事务跨服务调用根本不是“怎么避免”的问题,而是“必须禁止”的硬边界——只要事务跨越服务边界,就已脱离 MySQL 本地事务能力范围,强撑只会积累隐性故障。
为什么@Transactional传播到下游服务会失效
Spring 的 @Transactional 只在当前 JVM 进程内生效,一旦调用走 HTTP/gRPC 到另一个服务,事务上下文就彻底断开。下游服务启动的是全新数据库连接和独立事务,你看到的“一起提交”,其实是两个毫无协调的本地事务各自成功,中间没有任何原子性保障。
常见错误现象:
- 订单服务开启事务后调用库存服务接口,库存扣减成功但订单创建失败,最终出现“库存已扣、订单无记录”
- 日志里
Transaction rolled back because it has been marked as rollback-only,但数据库里部分数据已落库 - 使用 Feign +
@Transactional,以为加了注解就安全,实际只是给本地方法加锁,对远程调用零约束
替代方案:用本地事务 + 消息表兜底一致性
把跨服务写入拆成“本库落盘 + 异步通知”,靠消息表与业务表同事务来保底,比幻想分布式事务更可靠。
实操要点:
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
- 消息表(如
outbox)必须与业务表(如orders)在同一数据库、同一事务块中插入,用BEGIN/COMMIT包裹 - 字段至少含:
id、topic(如"order_created")、payload(JSON)、status("pending")、created_at - 消费者用
SELECT ... FOR UPDATE SKIP LOCKED拉取未发送消息,更新status = "sent"后再发 MQ;失败则按next_retry_at指定时间重试 - 禁止在消息发送逻辑里做耗时操作(如调用第三方 API),否则会卡住整个消息轮询线程
哪些场景看似要跨事务,其实可以重构绕开
很多所谓“必须强一致”的需求,本质是业务流程设计没分层,把状态推进和数据写入混在一起。
可落地的重构方向:
- 支付成功后更新订单状态 → 改为:支付服务本地记
payment_log,再发payment_succeeded消息,订单服务消费后更新自身orders.status - 用户注册后自动创建默认地址 → 地址服务提供幂等接口
upsert_default_address(user_id),注册服务异步调用,不参与主事务 - 定时任务批量更新多服务数据 → 改用 CDC(如 Debezium)监听 binlog,各服务自行订阅变更,避免集中式调度引发长事务
最容易被忽略的坑:事务超时与连接泄漏叠加
一个跨服务调用卡在下游响应上,@Transactional(timeout = 30) 会触发回滚,但上游连接池可能没及时释放,下游服务又因等待超时不断重试——两头都在耗连接,很快触达 max_connections 上限。
必须同步做三件事:
- 所有远程调用显式设超时:
feign.client.config.default.connectTimeout=3000、readTimeout=5000 - 数据库连接池设
pool.max_lifetime=1800000(30 分钟),避免连接长期存活后被 MySQL 主动踢掉 - 上线前执行
SHOW PROCESSLIST,重点看State为Sleep且Time> 180 的连接,它们大概率是被卡住的事务残留

















