Spring @Transactional 失效的 7 种真实场景 + 修法

标了 @Transactional 测试也好好的,生产却出现部分写入。本文 7 种事务失效场景:同类内部调用 / 非 public / 异常被吞 / checked exception / MyISAM / 多数据源 / Propagation 配错。每个附复现代码 + 3 种修法。

有个 service 方法标了 @Transactional,跑测试也好好的,上生产之后出现部分写入 —— 余额扣了但订单没创建,或者订单创了但日志没写。Spring 事务"看起来生效但实际没"是 Java 工程师每年都要踩一遍的坑。本文把我们这两年遇到的 7 种失效场景写下来,每个附复现代码 + 正确写法。

前置:Spring @Transactional 的本质

Spring 的 @TransactionalAOP 代理实现:

  1. Spring 启动时扫描所有 @Transactional 的 bean
  2. 给这个 bean 生成一个代理对象(JDK 动态代理或 CGLIB 子类)
  3. 代理对象在方法调用前后开启 / 提交 / 回滚事务
  4. 注入到其他 bean 的是代理对象,不是原对象

所有失效场景的根源都是:方法调用没经过代理对象

失效 1:同类内部方法调用

@Service
public class OrderService {

    public void create(Order order) {
        save(order);
        deductInventory(order);
    }

    @Transactional
    public void save(Order order) {     // ← 这个 @Transactional 不会生效
        orderRepo.save(order);
        // 抛异常时,期望回滚,但实际不会
        if (order.getAmount() < 0) {
            throw new IllegalArgumentException();
        }
    }
}

外部调用 create():Spring 代理拦截到了 create,但 createsave() 是用 this.save() 调用的 —— 这个 this 是原对象,不是代理对象,根本没经过 AOP 拦截器,事务注解失效。

修法 1:把方法拆到另一个 bean

@Service
public class OrderService {
    @Autowired private OrderTransactionalService txService;

    public void create(Order order) {
        txService.save(order);      // 跨 bean 调用,走代理
        txService.deductInventory(order);
    }
}

@Service
public class OrderTransactionalService {
    @Transactional
    public void save(Order order) { ... }

    @Transactional
    public void deductInventory(Order order) { ... }
}

修法 2:注入自己的代理

@Service
public class OrderService {
    @Autowired private OrderService self;       // 注入自己的代理(同一个 bean,但是是代理对象)

    public void create(Order order) {
        self.save(order);                       // 调用代理,事务生效
    }

    @Transactional
    public void save(Order order) { ... }
}

修法 3:用 AopContext 拿当前代理

@EnableAspectJAutoProxy(exposeProxy = true)    // 启动类加这个
public class App { ... }

@Service
public class OrderService {
    public void create(Order order) {
        ((OrderService) AopContext.currentProxy()).save(order);
    }

    @Transactional
    public void save(Order order) { ... }
}

失效 2:方法不是 public

@Service
public class OrderService {
    @Transactional
    private void save(Order order) {        // ← private 方法事务不生效
        orderRepo.save(order);
    }

    @Transactional
    protected void deduct(Long uid) {       // ← protected 也不生效
        // ...
    }

    @Transactional
    void process(Order order) {             // ← package private 不生效
        // ...
    }
}

Spring AOP 的默认实现(JDK 代理 + CGLIB)只能拦截 public 方法。private 方法编译期就被链接,代理拦不到。protected 和 package-private 同样不行(虽然 CGLIB 技术上能但 Spring 默认不拦)。

修法:把方法改成 public,或者用 AspectJ 编译期织入(项目里很少这么用)。

失效 3:异常被吞掉

@Service
public class OrderService {

    @Transactional
    public void create(Order order) {
        orderRepo.save(order);
        try {
            inventoryService.deduct(order);     // 抛 BusinessException
        } catch (Exception e) {
            log.error("deduct failed", e);
            // 异常被吞,事务不会回滚,订单已经入库
        }
    }
}

Spring 事务回滚靠抛出未捕获异常。catch 住等于告诉 Spring "一切正常",事务提交。

修法:

@Transactional
public void create(Order order) {
    orderRepo.save(order);
    try {
        inventoryService.deduct(order);
    } catch (BusinessException e) {
        log.error("deduct failed", e);
        // 主动标记当前事务回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        // 或者重新抛
        throw e;
    }
}

失效 4:抛 checked exception 但没声明 rollbackFor

@Transactional
public void create(Order order) throws IOException {     // checked exception
    orderRepo.save(order);
    sendNotification(order);    // 抛 IOException
}

Spring 默认只对 RuntimeExceptionError 回滚!checked exception(IOException / SQLException 等)默认不回滚。

修法:

@Transactional(rollbackFor = Exception.class)       // 对所有异常回滚
public void create(Order order) throws IOException {
    orderRepo.save(order);
    sendNotification(order);
}

// 或者只针对特定异常
@Transactional(rollbackFor = {IOException.class, BusinessException.class})
public void create(Order order) { ... }

团队规范:所有 @Transactional 一律加 rollbackFor = Exception.class,避免漏。

失效 5:数据库引擎不支持事务

-- MyISAM 不支持事务,所有事务声明都是无效的
CREATE TABLE orders (
    id INT PRIMARY KEY,
    amount DECIMAL(10,2)
) ENGINE=MyISAM;       -- ← 这就是坑

-- 必须用 InnoDB
CREATE TABLE orders (
    id INT PRIMARY KEY,
    amount DECIMAL(10,2)
) ENGINE=InnoDB;

这年头 MyISAM 已经罕见,但 H2 测试库 / 部分内存数据库默认不开事务支持。

失效 6:多数据源切换

@Transactional
public void crossDb() {
    primaryRepo.save(orderA);          // 主库:DataSource 1
    secondaryRepo.save(orderB);        // 从库:DataSource 2
    // Spring 默认单 DataSource 事务,跨数据源不能保证一致
}

修法:

  1. 避免跨库写:架构层面解决最稳
  2. 用 JTA 分布式事务:Atomikos / Narayana,性能很差
  3. Saga 模式:每库本地事务 + 失败补偿,业务复杂但性能好
// Spring 多数据源:不同方法用不同事务管理器
@Transactional(transactionManager = "primaryTxManager")
public void saveToA(Order o) { ... }

@Transactional(transactionManager = "secondaryTxManager")
public void saveToB(Order o) { ... }

// 跨库一致性自己保证(消息补偿 / Saga)
public void crossDbSaga(Order a, Order b) {
    try {
        saveToA(a);
    } catch (Exception e) {
        return;
    }
    try {
        saveToB(b);
    } catch (Exception e) {
        revertA(a);     // 补偿
    }
}

失效 7:Propagation 配错

@Service
public class OuterService {
    @Autowired private InnerService inner;

    @Transactional
    public void outer() {
        inner.inner();   // 期望加入外层事务
    }
}

@Service
public class InnerService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)    // 新开独立事务
    public void inner() {
        // ...
    }
}

REQUIRES_NEW 让 inner 开新事务,outer 暂停。如果 outer 抛异常,outer 回滚,但 inner 已经提交。可能不是你想要的。

7 种 Propagation 简要:

REQUIRED         默认。有就加入,没就新建            ← 绝大多数情况
REQUIRES_NEW    永远新建独立事务                    ← 日志记录、强制提交场景
SUPPORTS         有就加入,没就非事务执行
NOT_SUPPORTED    永远非事务执行(挂起外层)
MANDATORY        必须有外层,没有抛异常
NEVER             必须没外层,有就抛异常
NESTED           嵌套事务(SavePoint)

调试事务的方法

# application.properties 开 Spring 事务日志
logging.level.org.springframework.transaction=DEBUG
logging.level.org.springframework.orm.jpa=DEBUG

# 看 SQL 实际执行
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# 正常事务日志输出:
[main] DEBUG o.s.t.i.TransactionInterceptor - Getting transaction for [OrderService.create]
[main] DEBUG o.h.SQL - INSERT INTO orders ...
[main] DEBUG o.s.t.i.TransactionInterceptor - Completing transaction for [OrderService.create]
[main] DEBUG o.s.t.i.TransactionInterceptor - Transaction committed

# 失效时日志缺失:
[main] DEBUG o.s.t.i.TransactionInterceptor - Getting transaction for [OrderService.create]
[main] DEBUG o.h.SQL - INSERT INTO orders ...
# ← 这里直接没了下面的 Completing 日志,说明内部调用没经过代理

正确的事务模板

@Service
public class OrderService {

    @Autowired private OrderRepository    orderRepo;
    @Autowired private InventoryRepository invRepo;
    @Autowired private EventPublisher      publisher;

    /**
     * 创建订单 + 扣库存(同一本地事务)
     * 规则:rollbackFor=Exception.class,确保所有异常都回滚
     *      propagation=REQUIRED(默认),加入外层事务
     *      timeout=5,5 秒不完成自动回滚,避免长事务锁表
     */
    @Transactional(rollbackFor = Exception.class, timeout = 5)
    public Order create(CreateOrderCmd cmd) {
        // 1. 幂等检查
        if (orderRepo.existsByIdempotencyKey(cmd.getIdempotencyKey())) {
            return orderRepo.findByIdempotencyKey(cmd.getIdempotencyKey());
        }

        // 2. 创建订单
        Order order = new Order(cmd);
        orderRepo.save(order);

        // 3. 扣库存(乐观锁)
        int updated = invRepo.deductWithVersion(cmd.getSku(), cmd.getQty());
        if (updated == 0) {
            throw new BusinessException("inventory_changed");
        }

        // 4. 发事件(注意:这个事件要在事务提交后才发,否则可能消费者拿到了订单 id 但 DB 还没提交)
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    publisher.publish(new OrderCreatedEvent(order.getId()));
                }
            }
        );
        return order;
    }
}

这套模板有几个亮点:

  • rollbackFor = Exception.class 兜底
  • timeout = 5 防长事务
  • 事务内幂等检查
  • 用乐观锁防并发扣库存
  • 事件用 afterCommit 确保事务真的提交了再发

事务 review 7 问

  1. 方法是 public 吗?
  2. 是不是被同类内部调用?
  3. 异常有没有被吞?
  4. 有没有 rollbackFor = Exception.class?
  5. 数据源支持事务吗?(InnoDB,不是 MyISAM)
  6. 是不是跨数据源?
  7. Propagation 是不是默认 REQUIRED?(除非有特殊需求)

把这 7 问贴到 PR 模板里,Spring 事务类 bug 直接降到一年遇到 1-2 次。代价 0,收益高。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Kafka 消费幂等 + Offset 管理:2 年踩过的 7 个真实坑

2026-5-19 10:48:34

技术教程

MongoDB 副本集 primary 切换丢了 12 个订单的复盘:writeConcern 必须 majority

2026-5-19 10:52:51

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索