引言

在使用 Spring Boot 开发企业应用时,我们经常会在同一个类中封装多个方法,并且希望它们都能正确地参与事务管理。但是,你是否遇到过这样的情况:明明给方法加了@Transactional注解,但事务却没有按照预期工作?特别是当同一个类中的方法互相调用时,事务行为变得令人困惑?

本文将深入探讨 Spring Boot 中同类公用方法的事务问题,通过实际案例分析原因,并提供多种解决方案。本文基于Spring Boot 2.7.x/3.x版本进行讲解,如果你使用的是早期版本,部分配置项可能略有差异。

版本兼容性提示

  • Spring Boot 1.x/2.0 早期版本:使用spring.aop.autospring.aop.proxy-target-class
  • Spring Boot 2.x 后期/3.x 版本:默认使用 CGLIB 代理,通常无需额外配置
  • Spring Framework 6.0+(Spring Boot 3.x):移除了部分过时的事务 API

问题场景再现

假设我们正在开发一个简单的银行系统,需要实现账户转账功能。先看一段常见的实现代码:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 扣减转出账户金额
        deduct(fromAccount, amount);

        // 增加转入账户金额
        deposit(toAccount, amount);
    }

    @Transactional
    public void deduct(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }

        accountMapper.updateBalance(accountNo, account.getBalance().subtract(amount));
    }

    @Transactional
    public void deposit(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        accountMapper.updateBalance(accountNo, account.getBalance().add(amount));
    }
}

看起来很合理,对吧?transfer方法有@Transactional注解,理论上如果deposit方法执行失败,整个转账操作应该回滚。但实际上,可能会出现一种情况:钱从一个账户扣除了,但没有成功存入另一个账户,导致资金丢失!

问题分析

为什么会这样呢?让我们用图来说明 Spring 事务的工作原理:

graph TB
    Client[客户端] --> Proxy[Spring AOP代理]
    Proxy --> Target[目标对象]
    Proxy -- "事务管理" --> TransactionManager[事务管理器]

Spring 的事务管理基于 AOP(面向切面编程)实现。当我们调用一个带有@Transactional注解的方法时,实际上是通过 Spring 创建的代理对象来调用的。代理对象会在调用真正的方法前后添加事务管理的逻辑。

但问题在于:当一个类的方法内部调用同类的另一个方法时,这个调用是在目标对象内部完成的,不会经过 Spring 的代理

graph TB
    subgraph "外部调用(正常工作)"
    Client1[客户端] --> Proxy1[Spring AOP代理]
    Proxy1 --> Target1[目标对象的方法A]
    end

    subgraph "内部调用(事务失效)"
    Target2[目标对象的方法A] --> |"直接调用,不经过代理"|Target3[目标对象的方法B]
    end

在我们的示例中,当外部代码调用transfer方法时,事务正常启动。但transfer内部调用deductdeposit方法时,这些调用直接在目标对象上执行,没有经过代理,所以这两个方法上的@Transactional注解被完全忽略了!

结果就是:整个转账操作运行在transfer方法的事务中,而deductdeposit方法上的事务配置不会生效。

@Transactional 注解失效的常见场景

除了同类方法调用导致的事务失效,还有其他几种常见的情况也会使@Transactional 注解失效:

1. 方法访问权限不是 public

Spring AOP 代理只支持对 public 方法应用事务。如果方法是 private、protected 或默认(包)访问级别,即使添加了@Transactional 注解,事务也不会生效。

// 事务无效 - 方法不是public
@Transactional
protected void updateAccount(Account account) {
    // 不会在事务中执行!
    accountMapper.update(account);
}

2. 异常被捕获却未重新抛出

@Transactional 默认只在遇到未检查异常(RuntimeException 及其子类)时才回滚。如果方法内部捕获了异常但没有重新抛出,Spring 无法感知异常发生,事务不会回滚。

@Transactional
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
    try {
        // 扣减转出账户金额
        deduct(fromAccount, amount);

        // 增加转入账户金额
        deposit(toAccount, amount);
    } catch (Exception e) {
        log.error("转账失败", e);
        // 异常被捕获但未重新抛出,事务不会回滚!
    }
}

3. 使用了错误的异常类型

默认情况下,@Transactional 只在遇到运行时异常时回滚,而对于检查型异常(如 IOException)则不回滚。

@Transactional
public void importData(File file) throws IOException {
    try {
        // 读取文件并导入数据
        // 假设这里抛出了IOException
    } catch (IOException e) {
        // 对于IOException,事务默认不会回滚!
        throw e;
    }
}

如果需要在检查型异常时也回滚,需要显式配置:

@Transactional(rollbackFor = Exception.class)
public void importData(File file) throws IOException {
    // 现在任何异常都会导致回滚
}

4. 代理类型与方法调用的兼容性

Spring 可以使用两种代理机制:JDK 动态代理(基于接口)和 CGLIB 代理(基于类)。代理类型会影响事务行为:

graph TB
    subgraph "Spring代理机制"
    JDK[JDK动态代理<br>需要接口] --- CGLIB[CGLIB代理<br>直接代理类]
    end

    subgraph "影响因素"
    Config[spring.aop.proxy-target-class=true/false] --> |影响| ProxyChoice[代理选择]
    Interface[是否实现接口] --> |影响| ProxyChoice
    end
  • 如果 Service 实现了接口,默认情况下 Spring 使用 JDK 动态代理
  • 如果 Service 没有实现接口,或配置了proxy-target-class=true,Spring 使用 CGLIB 代理
  • CGLIB 不能代理 final 类和方法
// 如果使用final修饰,事务将失效
public final class FinalAccountService {

    @Transactional
    public void transfer(...) { ... }
}

// 或final方法
public class AccountService {

    @Transactional
    public final void transfer(...) { ... }
}

解决方案

方案一:使用自我注入

一个简单的解决方案是让 Service 注入自己:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountService self; // 注入自己的代理对象,而非目标对象

    @Transactional
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 通过self调用,走代理
        self.deduct(fromAccount, amount);
        self.deposit(toAccount, amount);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deduct(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }

        accountMapper.updateBalance(accountNo, account.getBalance().subtract(amount));
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deposit(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        accountMapper.updateBalance(accountNo, account.getBalance().add(amount));
    }
}

实现细节与注意事项

  • Spring 通过@Autowired注入的是代理对象而非目标对象本身,因此self调用会经过代理,从而激活事务管理
  • 此方法在默认的单例作用域(@Scope("singleton"))下工作良好
  • 如果 Service 配置为原型作用域(@Scope("prototype")),可能引发循环依赖问题,Spring 会抛出BeanCurrentlyInCreationException异常
  • 确保被调用的方法是public的,因为 Spring 无法代理非公开方法,即使通过 self 调用也会失效
  • 该方法简单直接,对现有代码结构改动最小,但增加了对象引用复杂度

方案二:使用 AopContext 获取代理对象

Spring 提供了一种方式来获取当前代理:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        AccountService proxy = (AccountService) AopContext.currentProxy();
        proxy.deduct(fromAccount, amount);
        proxy.deposit(toAccount, amount);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deduct(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }

        accountMapper.updateBalance(accountNo, account.getBalance().subtract(amount));
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deposit(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        accountMapper.updateBalance(accountNo, account.getBalance().add(amount));
    }
}

实现细节与限制

  • 必须在启动类或配置类上添加@EnableAspectJAutoProxy(exposeProxy = true)
  • 或在 application.properties 中设置(根据 Spring Boot 版本有所不同):

    # Spring Boot 2.0+
    spring.aop.proxy-target-class=true
    
    # Spring Boot 1.x
    spring.aop.auto=true
    spring.aop.proxy-target-class=true
  • 该方法仅适用于通过 Spring AOP 代理调用的上下文中
  • 不能在非 Spring 管理的线程或对象中使用,否则会抛出IllegalStateException
  • exposeProxy = true会略微影响性能,因为 Spring 需要在 ThreadLocal 中维护当前代理对象
  • 如果在使用线程池的情况下,ThreadLocal 可能导致内存泄漏,需要注意清理

方案三:方法重构,调整设计以符合事务边界

重新设计方法结构,明确事务边界:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    // 公开API,包含完整事务
    @Transactional
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 内部直接实现业务逻辑,不再调用有事务注解的其他方法
        // 扣减转出账户
        Account fromAcc = accountMapper.findByAccountNo(fromAccount);
        if (fromAcc.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }
        accountMapper.updateBalance(fromAccount, fromAcc.getBalance().subtract(amount));

        // 增加转入账户
        Account toAcc = accountMapper.findByAccountNo(toAccount);
        accountMapper.updateBalance(toAccount, toAcc.getBalance().add(amount));
    }

    // 仅供外部直接调用的独立操作,有独立事务
    @Transactional
    public void deduct(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }
        accountMapper.updateBalance(accountNo, account.getBalance().subtract(amount));
    }

    // 仅供外部直接调用的独立操作,有独立事务
    @Transactional
    public void deposit(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        accountMapper.updateBalance(accountNo, account.getBalance().add(amount));
    }

    // 或者使用protected限制内部方法的可见性
    /*
    // 这些方法不再需要事务注解,仅供内部使用
    protected void internalDeduct(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }
        accountMapper.updateBalance(accountNo, account.getBalance().subtract(amount));
    }

    protected void internalDeposit(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        accountMapper.updateBalance(accountNo, account.getBalance().add(amount));
    }
    */
}

设计权衡与注意事项

  • 优点:避免了事务传播问题,设计更清晰,性能开销最小
  • 缺点:可能导致代码重复,降低复用性
  • 如果内部方法确实需要在其他地方复用,考虑:

    1. 使用protected修饰符限制方法仅在同包或子类中可见
    2. 明确区分"外部 API 方法"(有事务)与"内部实现方法"(无事务)
    3. 在文档中清晰标注方法的事务行为
  • 注意:使用protectedprivate方法时,即使通过代理调用,这些方法上的@Transactional注解也会被忽略

方案四:使用编程式事务管理

灵活控制事务边界,适用于复杂场景:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 创建事务模板
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);

        // 可以动态设置事务属性
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        // 执行事务
        transactionTemplate.execute(status -> {
            try {
                // 执行转账操作
                deduct(fromAccount, amount);
                deposit(toAccount, amount);
                return null;
            } catch (Exception e) {
                // 可以精细控制回滚决策
                status.setRollbackOnly();
                throw e;
            }
        });
    }

    // 这些方法不需要事务注解,由编程式事务控制
    private void deduct(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }
        accountMapper.updateBalance(accountNo, account.getBalance().subtract(amount));
    }

    private void deposit(String accountNo, BigDecimal amount) {
        Account account = accountMapper.findByAccountNo(accountNo);
        accountMapper.updateBalance(accountNo, account.getBalance().add(amount));
    }

    // 示例:动态决定事务行为的场景
    public void conditionalTransfer(String fromAccount, String toAccount, BigDecimal amount, boolean priority) {
        TransactionTemplate template = new TransactionTemplate(transactionManager);

        // 根据业务参数动态设置事务属性
        if (priority) {
            // 高优先级交易使用更高隔离级别
            template.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
            template.setTimeout(30); // 更长超时
        } else {
            template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
            template.setTimeout(10);
        }

        template.execute(status -> {
            // 执行业务逻辑
            Account fromAcc = accountMapper.findByAccountNo(fromAccount);
            if (fromAcc.getBalance().compareTo(amount) < 0) {
                throw new InsufficientBalanceException("余额不足");
            }
            accountMapper.updateBalance(fromAccount, fromAcc.getBalance().subtract(amount));

            Account toAcc = accountMapper.findByAccountNo(toAccount);
            accountMapper.updateBalance(toAccount, toAcc.getBalance().add(amount));

            return null;
        });
    }
}

编程式事务的优势场景

  • 需要动态决定事务属性(隔离级别、超时、传播行为)
  • 复杂的事务边界控制(部分操作提交或回滚)
  • 精细的异常处理策略(某些异常回滚,某些不回滚)
  • 与声明式事务混合使用的场景
  • 性能关键场景(避免 AOP 代理带来的反射开销)

事务传播特性详解

了解 Spring 事务的传播特性对解决这类问题至关重要:

graph TB
    subgraph "事务传播特性对比"
    REQUIRED["REQUIRED(默认): 使用现有事务,无则创建"]
    REQUIRES_NEW["REQUIRES_NEW: 总是创建新事务,挂起现有事务"]
    NESTED["NESTED: 在现有事务内创建嵌套事务(依赖保存点)"]
    SUPPORTS["SUPPORTS: 支持现有事务,无则非事务执行"]
    NOT_SUPPORTED["NOT_SUPPORTED: 不支持事务,挂起现有事务"]
    MANDATORY["MANDATORY: 必须有事务,无则抛异常"]
    NEVER["NEVER: 禁止事务,有则抛异常"]
    end

REQUIRED、REQUIRES_NEW 与 NESTED 的关键区别

  1. REQUIRED(默认)

    • 行为:如果存在事务,则加入该事务;如果不存在,则创建新事务
    • 回滚影响:内外方法任一异常,整体回滚
    • 适用场景:大多数业务操作,要求操作整体成功或失败
  2. REQUIRES_NEW

    • 行为:总是创建一个新事务,如果存在事务,则将已有事务挂起
    • 回滚影响:内部事务与外部事务完全独立

      • 内部方法异常回滚不影响外部事务
      • 外部方法异常回滚不影响已提交的内部事务
    • 适用场景:

      • 记录操作日志(无论业务成功失败)
      • 业务操作与通知操作解耦(如发送消息)
      • 独立的计费或统计操作
    • 注意:每个新事务需要单独的数据库连接,高并发场景下可能导致连接池耗尽
  3. NESTED

    • 行为:如果存在事务,则创建一个嵌套事务(依赖数据库 SavePoint 机制);如不存在,则行为同 REQUIRED
    • 回滚影响:

      • 内部方法异常仅回滚嵌套事务(至 SavePoint),外部事务可继续执行
      • 外部方法异常会导致内部嵌套事务一起回滚
    • 适用场景:

      • "尝试性"操作,失败不影响主流程
      • 可部分回滚的业务场景
    • 重要局限性

      • 仅支持 JDBC 事务管理器,不支持 JTA 等分布式事务
      • 在微服务或分布式架构中可能不适用
      • 依赖数据库对保存点(SavePoint)的支持,部分 NoSQL 数据库不支持
      • 相比完整回滚,保存点回滚性能更好(只回滚部分操作,日志量更小)

在银行转账示例中的选择:

  • 如果要求转账操作必须整体成功或失败:使用REQUIRED(所有方法共享同一事务)
  • 如果deductdeposit需要作为独立操作(可单独提交):使用REQUIRES_NEW
  • 如果希望扣款成功后,即使存款失败也保留扣款记录:使用NESTED(但实际金融系统通常不允许这种不一致状态)

实际案例分析

假设我们在开发一个订单系统,需要处理下单、库存扣减和支付流程:

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryMapper inventoryMapper;

    @Autowired
    private MessageService messageService;

    @Autowired
    private OrderService self; // 自我注入解决方案

    @Transactional
    public void createOrder(OrderDTO orderDTO) {
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(orderDTO.getUserId());
        order.setProductId(orderDTO.getProductId());
        order.setQuantity(orderDTO.getQuantity());
        order.setAmount(orderDTO.getAmount());
        order.setStatus("CREATED");
        order.setCreateTime(new Date());
        orderMapper.insert(order);

        // 2. 扣减库存 - 通过self调用以确保事务生效
        try {
            self.reduceInventory(orderDTO.getProductId(), orderDTO.getQuantity());
        } catch (Exception e) {
            throw new OrderException("库存不足,下单失败");
        }

        // 3. 发送通知消息(使用REQUIRES_NEW确保消息发送不受主事务影响)
        try {
            self.sendOrderNotification(order.getId());
        } catch (Exception e) {
            // 消息发送失败不影响订单创建
            log.error("通知发送失败", e);
        }

        // 4. 模拟可能的异常
        if (new Random().nextInt(10) < 3) { // 30%的概率发生异常
            throw new RuntimeException("系统异常,下单失败");
        }
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void reduceInventory(Long productId, Integer quantity) {
        Inventory inventory = inventoryMapper.findByProductId(productId);
        if (inventory.getStock() < quantity) {
            throw new InsufficientStockException("库存不足");
        }

        inventoryMapper.reduceStock(productId, quantity);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendOrderNotification(Long orderId) {
        // 即使主事务回滚,消息也已发送(REQUIRES_NEW)
        Order order = orderMapper.findById(orderId);
        messageService.sendOrderCreationMessage(order.getUserId(), order.getProductId(), order.getAmount());

        // 记录通知日志
        NotificationLog log = new NotificationLog();
        log.setOrderId(orderId);
        log.setType("ORDER_CREATED");
        log.setContent("订单创建成功,金额:" + order.getAmount());
        log.setSendTime(new Date());
        messageService.saveNotificationLog(log);
    }

    @Transactional(propagation = Propagation.NESTED)
    public void processBonusPoints(Long userId, BigDecimal orderAmount) {
        // 尝试性操作:增加用户积分
        // 如果此方法失败,主事务可以继续(回滚到保存点)
        int points = orderAmount.multiply(new BigDecimal("10")).intValue(); // 10倍积分
        UserPoints userPoints = userPointsService.findByUserId(userId);
        userPoints.setPoints(userPoints.getPoints() + points);
        userPoints.setUpdateTime(new Date());
        userPointsService.updatePoints(userPoints);
    }
}

在这个扩展示例中:

  • reduceInventory使用REQUIRED,确保与主事务同步(订单创建失败,库存也会回滚)
  • sendOrderNotification使用REQUIRES_NEW,确保消息发送操作独立提交(即使订单创建失败)
  • processBonusPoints使用NESTED,允许积分处理失败而不影响订单创建(主流程优先)

性能分析与对比

不同事务解决方案对性能的影响:

解决方案性能影响量化分析
自我注入每次调用增加约 5-10%反射开销,内存占用略增(额外代理对象引用)
AopContext中低需要在 ThreadLocal 中存储代理引用,每次调用增加查找开销,约 10-15%额外开销
方法重构最佳无额外开销,直接方法调用是最优性能选择
编程式事务避免了反射开销,但需要手动创建 TransactionTemplate,性能接近方法重构
REQUIRES_NEW每个新事务需要额外数据库连接和事务日志开销,高并发时可能导致连接池耗尽
NESTED中高需要数据库支持保存点(SavePoint),有额外 I/O 开销,但低于 REQUIRES_NEW

高并发场景建议

  1. 避免过度使用REQUIRES_NEW,每个新事务需单独获取数据库连接
  2. 大型批处理操作考虑使用更粗粒度事务边界
  3. 使用连接池监控,观察峰值连接使用情况,根据实际负载调整连接池大小
  4. 对性能关键路径,考虑方法重构或编程式事务而非代理方案

量化示例
在一个典型的 Web 应用中,使用REQUIRES_NEW创建 100 个内部事务,与使用单一REQUIRED事务相比:

  • 响应时间:可能增加 50-200%
  • 数据库连接使用:增加 5-10 倍
  • 事务日志大小:增加 3-5 倍

如何排查事务问题

当遇到事务不按预期工作时,可以通过以下方法快速定位问题:

1. 开启 Spring 事务相关日志

application.propertiesapplication.yml中添加:

# Spring Boot 2.x/3.x 日志配置
logging.level.org.springframework.transaction=DEBUG
logging.level.org.springframework.orm.jpa=DEBUG  # 如果使用JPA
logging.level.org.springframework.jdbc=DEBUG     # 如果使用JDBC

通过观察日志,可以确认:

  • 事务是否按预期创建:寻找Creating new transaction with name [类名.方法名]
  • 事务是否正确提交/回滚:寻找Committing JDBC transactionRolling back JDBC transaction
  • 是否存在事务属性配置问题:如Participating in existing transaction表示加入现有事务

2. 使用 TransactionSynchronizationManager 检查事务状态

在关键方法中添加代码检查当前是否在事务中:

@Transactional
public void someMethod() {
    boolean inTransaction = TransactionSynchronizationManager.isActualTransactionActive();
    String txName = TransactionSynchronizationManager.getCurrentTransactionName();
    log.info("当前方法是否在事务中: {}, 事务名称: {}", inTransaction, txName);

    // 业务逻辑...
}

3. 排查步骤

  1. 首先检查方法访问修饰符是否为public
  2. 确认异常是否为运行时异常,或已配置rollbackFor
  3. 验证异常是否被捕获但未重新抛出
  4. 检查是否存在同类方法调用(事务失效的主要原因)
  5. 确认 Service 类是否被代理,使用AopUtils.isAopProxy(this)检查

4. 数据库事务分析

如果程序中事务配置正确但仍有问题,可能是数据库层面的问题:

-- MySQL查看当前活动事务
SHOW ENGINE INNODB STATUS;

-- 查看是否有长时间运行的事务
SELECT * FROM information_schema.innodb_trx
WHERE trx_started < NOW() - INTERVAL 10 SECOND;

-- 检查隔离级别
SELECT @@transaction_isolation;

建议:事务问题往往涉及多个维度,同时从应用层和数据库层进行排查可以更快定位问题。

总结

问题原因解决方案适用场景
同类方法事务失效内部调用不经过 Spring 代理自我注入(self)简单场景,不想改变现有代码结构
使用 AopContext需要在多处获取代理对象
方法重构代码设计初期,追求性能
编程式事务复杂事务逻辑,需要精细控制
方法权限问题非 public 方法修改为 public保证事务可以被 Spring 正确代理
异常处理不当异常被捕获未重新抛出重新抛出异常或使用 TransactionAspectSupport确保 Spring 能感知事务失败
代理类型限制final 类或方法避免使用 final保证 Spring 能生成代理类
事务传播特性选择业务需求不同REQUIRED大多数场景,保持数据一致性
REQUIRES_NEW需要独立事务,不受外部事务影响
NESTED需要部分回滚能力,主流程优先(仅 JDBC)

理解 Spring 事务的工作原理对于开发高质量的企业应用至关重要。透彻掌握事务传播特性的区别,以及各种解决方案的优缺点,将帮助你设计出更可靠、更高性能的事务处理逻辑。

建议:在设计时先明确事务边界和业务需求,选择合适的传播特性,权衡性能与代码复杂度,才能避免意外的数据不一致问题。事务管理不是事后修复的问题,而是需要在设计阶段就考虑清楚的架构决策。


异常君
1 声望1 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!