java面试:Spring事务失效的场景有哪些?

Spring事务失效在java场景当中是十分常见的,在某些情况之下,我们希望被事务锁住能正常回滚的代码往往会由于代码编写的并不规范而导致失效,这就会出现编写的代码达不到我们的预期效果的问题,为了能够避免这种问题的发生,我们就需要知道哪些情况之下会发生事务的失效。

而spring的事务失效一般来说会在八种场景之下发生,接下来我们会对这八种情况进行逐一讲解。

1.抛出错误异常

Spring的事务只会检查Run Time exception和error的异常,而假设在@Transational绑定的事务当中用户选择了抛出了与之同级的异常,就会导致事务失效。

解决方案是通过@Transational的rollbackFor属性来指定除了Run Time exception以外的其他异常类也会触发事务的回滚。

演示代码:

java 复制代码
/**
 * 【问题】默认配置 - 事务失效
 * 抛出 BusinessException(受检异常)时,事务不会回滚!
 */
@Transactional
public void createUserWithDefaultConfig(String username) throws BusinessException {
    userRepository.saveUser(username);  // 已执行
    if (username.contains("invalid")) {
        throw new BusinessException("用户名非法");  // ❌ 事务不回滚!
    }
}

/**
 * 【解决方案】使用 rollbackFor 指定受检异常也触发回滚
 */
@Transactional(rollbackFor = BusinessException.class)
public void createUserWithRollbackFor(String username) throws BusinessException {
    userRepository.saveUser(username);
    if (username.contains("invalid")) {
        throw new BusinessException("用户名非法");  // ✅ 事务会回滚!
    }
}

2.直接调用本类方法

Spring的事务是由AOP也就是基于动态代理来实现的,因此会给自动装配的类的方法进行一个try catch的增强,而假设我们不基于动态代理而是直接调用本类方法,那就没有这样的增强,就会导致本类方法的事务失效。

解决方案是通过@Autowired或者getBean()来进行装配,基于动态代理获取到对象以后,就可以解决。

java 复制代码
@Service
class OrderService {
    
    /**
     * 【问题】直接调用本类方法 - 事务失效!
     */
    public void createOrderDirectCall(String orderId, double amount) {
        // 保存订单主表
        orderRepository.saveOrder(orderId, amount);
        
        // ❌ 直接调用本类方法 - this 指向原生对象,不是代理对象!
        this.saveOrderItemWithTransaction(orderId, "商品A");
    }
    
    /**
     * 被调用的内部方法 - 带有事务注解
     * 只有通过代理调用时,事务才会生效
     */
    @Transactional(rollbackFor = Exception.class)
    public void saveOrderItemWithTransaction(String orderId, String item) throws Exception {
        orderRepository.saveOrderItem(orderId, item);
        if (item.equals("商品A")) {
            throw new Exception("库存不足!");
        }
    }
}

3.多线程事务

假设我们通过多线程来调用另外一个事务方法,假设子线程出现异常,会使得主线程的事务都失效,这是由于spring的每一个线程事务的JDBC Connnection都是保存在对应的ThreadLocal上,它们无法共享同一个数据库连接。

解决方案:通过分布式事务的方式来解决。

4.异常被捕获没有抛出

Spring 事务管理器通过感知异常来决定是否回滚:

  • 方法抛出异常 → 事务管理器捕获 → 触发回滚

  • 异常被 try-catch 捕获且未抛出 → 事务管理器无感知 → 正常提交

    java 复制代码
    @Service
    class TransferService {
        
        /**
         * 【问题】异常被吞没,事务失效!
         * 
         * 结果:转出方钱已扣,转入方未收到,数据不一致!
         */
        @Transactional(rollbackFor = Exception.class)
        public void transferWithSwallowedException(Long fromUserId, Long toUserId, double amount) {
            try {
                // 1. 扣减转出账户
                accountRepository.deductBalance(fromUserId, amount);
                
                // 2. 抛出异常(如余额不足)
                if (amount > 1000) {
                    throw new RuntimeException("单笔转账限额 1000 元!");
                }
                
                // 3. 增加转入账户
                accountRepository.addBalance(toUserId, amount);
                
            } catch (Exception e) {
                // ❌ 致命错误:只记录日志,不抛出异常!
                log.error("转账异常", e);
                
                // 事务管理器感知不到异常,会提交事务!
                // 结果:转出方钱已扣,转入方未收到,数据不一致!
            }
        }
    }

    解决方案:抛出异常,Throw e 即可。

5.@Transational的应用在非public的方法上

非公共方法不应用事务,因此事务会因此失效

6.@Transational应用在final和static修饰的方法上

fianl方法无法被子类化,而static修饰的属于类而不是实例对象,无法被代理

7事务的传播行为配置错误

例如REQUIRES_NEW 滥用导致数据不一致

java 复制代码
@Service
class OrderProcessingService {
    
    @Autowired
    private InventoryService inventoryService;  // 使用 REQUIRES_NEW
    
    @Autowired
    private LogService logService;  // 使用 REQUIRES_NEW
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(String orderId, String productId, int quantity) {
        // 1. 保存订单(主事务)
        orderRepository.saveOrder(orderId, quantity * 100.0);
        
        // 2. 扣减库存(独立事务!)
        // ❌ 错误:使用 REQUIRES_NEW,与主事务分离
        inventoryService.deductStock(productId, quantity);
        
        // 3. 记录日志(独立事务!)
        // ❌ 错误:使用 REQUIRES_NEW,与主事务分离
        logService.saveLog("CREATE_ORDER", "订单: " + orderId);
        
        // 4. 模拟异常
        if (quantity > 10) {
            throw new RuntimeException("超出限制!");
        }
        // 结果:主事务回滚(订单未创建),但库存已扣、日志已记录!
    }
}

@Service
class InventoryService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)  // ❌ 错误
    public void deductStock(String productId, int quantity) {
        inventoryRepository.deductStock(productId, quantity);
    }
}

8数据库用的是MyISAM的引擎

MyISAM不支持事务。

今天的分享就到这里了,希望这篇博客能给你一些帮助,让你对关于有了解过Spring事务失效的场景有哪些的问题得到进一步的提升,在面试的时候能从容面对面试官。

相关推荐
Fate_I_C6 分钟前
Kotlin 内部类和嵌套类
java·开发语言·kotlin
We་ct21 分钟前
JS手撕:函数进阶 & 设计模式解析
开发语言·前端·javascript·设计模式·面试·前端框架
斌味代码24 分钟前
MySQL主从延迟根因诊断法
数据库
宸津-代码粉碎机26 分钟前
Spring Boot 4.0 实战技巧全解析
java·大数据·spring boot·后端·python
Makoto_Kimur28 分钟前
Java Scanner 的 ACM 常用输入模板
java·数据结构·算法
逆境不可逃28 分钟前
高频 SQL 50 题 之 连接篇 1378 1068 1581 197 1661 577 1280 570 1934
数据库·sql
0xDevNull32 分钟前
Spring 核心教程:@Component vs @Bean 深度解析
java·后端
逆境不可逃33 分钟前
【后端新手谈09】深入浅出短链接:从原理到实战开发
算法·面试·职场和发展
小碗羊肉34 分钟前
【从零开始学Java | 第三十二篇】方法引用(Method Reference)
java·开发语言
yuki_uix36 分钟前
当 reduce 遇到二维数据:从"聚合直觉"到"复合 Map"的思维跃迁
前端·javascript·面试