【JAVA Spring面经】Spring 事务失效情况

文章目录

  • 前言
  • [1. 方法非 public](#1. 方法非 public)
  • [2. 内部 this 调用](#2. 内部 this 调用)
  • [3. 异常被 catch 且未重新抛出](#3. 异常被 catch 且未重新抛出)
  • [4. rollbackFor 未指定受检异常](#4. rollbackFor 未指定受检异常)
  • [5. 数据库引擎不支持事务](#5. 数据库引擎不支持事务)
  • [6. 多线程调用](#6. 多线程调用)
  • [7. 传播属性配置不当](#7. 传播属性配置不当)
  • [8. 未被 Spring 管理的类](#8. 未被 Spring 管理的类)

前言

 Spring 事务基于 AOP 动态代理实现,核心流程是:当调用被 @Transactional 标注的方法时,实际调用的是代理对象的方法。代理对象会在方法执行前开启事务,在方法正常结束后提交事务,在方法抛出特定异常(默认 RuntimeException 或 Error)时回滚事务。

 假设有一个 Service 原始类

java 复制代码
@Service
public class UserServiceImpl implements UserService {
    @Transactional
    @Override
    public void createUser(User user) {
        userMapper.insert(user);   // ← 核心业务:插入数据库
    }
}

 Spring 在启动时,会为 UserServiceImpl 生成一个代理类,大概内容如下(简化版):

java 复制代码
// 这是 Spring 动态生成的代理类(伪代码,实际更复杂)
public class UserServiceProxy implements UserService {
    // 持有原始对象
    private UserService target;

    @Override
    public void createUser(User user) {
        // -------- 秘书的"前置工作"(AOP 前置通知)--------
        TransactionStatus status = transactionManager.begin();  // 1. 开启事务
        try {
            // -------- 老板干正事(调用原始对象的方法)--------
            target.createUser(user);   // 2. 实际业务:插入数据库

            // -------- 秘书的"善后工作"(AOP 后置通知)--------
            transactionManager.commit(status);  // 3. 提交事务
        } catch (Exception e) {
            transactionManager.rollback(status); // 4. 异常回滚
            throw e;
        }
    }
}

 当我们调用 userService.createUser(user) 时,实际执行的是代理对象的 createUser 方法,而不是原始对象的方法。 原始对象的方法被包裹在代理之中,整个流程受到了事务管理的保护。

1. 方法非 public

 Spring AOP 只能拦截 public 方法,如果目标方法是 protected、private 或默认(包)可见,代理无法拦截,事务自然不生效。

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Transactional
    // 错误:非 public 方法
    void updateUser(User user) {
        userMapper.updateById(user);
        throw new RuntimeException("测试回滚");
    }
}

 此时抛出的异常不会导致事务回滚,数据库数据仍然被更新。

  • 解决方案将方法修饰符改为 public 即可

2. 内部 this 调用

 在同一类中,一个无事务的方法通过 this 调用另一个带 @Transactional 的方法,调用的是原始对象而不是代理对象,事务注解被忽略。

java 复制代码
@Service
public class OrderService {
    public void placeOrder(Order order) {
        // this 调用,绕过了代理
        this.createOrder(order);
    }

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        throw new RuntimeException("测试回滚");
    }
}
  • 解决方案1:从 Spring 容器中注入自身的代理对象,用代理调用事务方法。
java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderService self;  // 注入自己的代理

    public void placeOrder(Order order) {
        self.createOrder(order);
    }

    @Transactional
    public void createOrder(Order order) { ... }
}
  • 解决方案2:通过 AopContext.currentProxy() 获取代理对象。

先在启动类或配置类上启用暴露代理:

java 复制代码
@EnableAspectJAutoProxy(exposeProxy = true)
java 复制代码
public void placeOrder(Order order) {
    ((OrderService) AopContext.currentProxy()).createOrder(order);
}

3. 异常被 catch 且未重新抛出

 @Transactional 默认只在抛出 RuntimeException 或 Error 时才回滚。如果异常被 catch 块捕获并且没有再次抛出,Spring 根本不知道发生了异常,事务会正常提交。

java 复制代码
@Transactional
public void transfer() {
    try {
        accountMapper.debit();
        int i = 1 / 0;          // 抛出 ArithmeticException
        accountMapper.credit();
    } catch (Exception e) {
        log.error("transfer error", e);
        // 异常被吞,事务未感知,仍然提交
    }
}
  • 解决方案:在 catch 块中重新抛出运行时异常。或者手动标记事务回滚。
java 复制代码
} catch (Exception e) {
    log.error("transfer error", e);
    throw new RuntimeException(e);
}
java 复制代码
} catch (Exception e) {
    log.error("transfer error", e);
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

4. rollbackFor 未指定受检异常

 如果业务抛出的异常是受检异常(Exception 而非 RuntimeException),默认情况下 Spring 不会回滚。

java 复制代码
@Transactional
public void createFile() throws IOException {
    fileService.write();
    throw new IOException("文件写入失败"); // 受检异常,默认不回滚
}
  • 解决方案:明确指定 rollbackFor = Exception.class,让任何异常都触发回滚。
java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createFile() throws IOException { ... }

5. 数据库引擎不支持事务

 MySQL 的 MyISAM 引擎不支持事务,若表使用该引擎,@Transactional 无论如何配置都不会生效。

  • 解决方案:将表的存储引擎改为 InnoDB(支持事务和行级锁)。
java 复制代码
ALTER TABLE `user` ENGINE = InnoDB;

6. 多线程调用

 Spring 事务通过 ThreadLocal 绑定数据库连接,事务范围限定在同一个线程内。如果在事务方法内部启动新线程执行数据库操作,新线程中的操作不在当前事务中。

java 复制代码
@Transactional
public void process() {
    userMapper.update();
    new Thread(() -> {
        orderMapper.insert();  // 在另一个线程,与当前事务无关
    }).start();
    throw new RuntimeException("回滚");
}
  • 解决方案:避免在事务中开启多线程。若必须使用多线程,可考虑编程式事务或分布式事务(如 Seata)来协调多个数据库操作。

7. 传播属性配置不当

 @Transactional 的 propagation 属性控制事务的传播行为。例如默认 REQUIRED 会加入当前事务,但如果设为 NOT_SUPPORTED(非事务执行)或 NEVER(不能有事务),会导致事务不生效。

java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void update() {
    userMapper.update();    // 不在事务中执行
}
  • 解决方案:根据业务需求选择合适的传播属性,大多数情况使用默认 REQUIRED。

8. 未被 Spring 管理的类

 只有被 Spring 容器管理的 Bean 才能享受 AOP 代理和事务功能。如果类没有通过 @Service、@Component 等注解注册,或者通过 new 手动创建对象,@Transactional 将无效。

java 复制代码
// 未加 @Service 等注解,不在 Spring 容器中
public class NotManagedService {
    @Transactional
    public void doSomething() { ... }
}
  • 解决方案:确保类被 Spring 扫描并管理,例如添加 @Service。
java 复制代码
java 复制代码
相关推荐
Irene19912 小时前
Oracle PL/SQL Developer 版本差异(11g、14)实战总结
java·开发语言
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第191题】【08_Kafka篇】第7题:消息队列的优缺点
java·消息队列·系统设计·分布式架构·技术选型
用户446139430272 小时前
Spring Boot 接入大模型:从配置到实战的完整指南
java
青石路2 小时前
如果写Redis序列化与读Redis序列化不一致,你觉得会发生什么
java·redis
煎饼学大模型2 小时前
架构决定上限:Skill 知识架构的三次重构实践
java·重构·架构·skill
llwszx2 小时前
【Java/Go后端手撸原生Agent(第五篇):多工具并行调用 + BashTool执行引擎 + Judge证据链升级】
java·后端·golang·状态机·pydantic·agnet·llm-as-judge
奈何不吃鱼2 小时前
【SpringBoot】业务多线程四大实战场景
java·spring boot·多线程
问商十三载2 小时前
2026法律行业AI引擎生成式优化怎么优化?三层专业加固提排名,零成本提33%引用率附适配表
java·前端·人工智能·算法
野生风长2 小时前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
最后的最后、7962 小时前
SQLi-Labs 六关 SQL 注入过关笔记
数据库·笔记·sql