【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 复制代码
相关推荐
swordbob6 小时前
MySQL字符集陷阱:从Oracle迁移踩坑到utf8mb4强制规范
数据库·sql
snow@li6 小时前
Java:理解 Gradle / 后端项目的管家 / 打包SpringBoot 应用 / 完成编译、下载依赖、运行测试、打包 JAR/WAR / 速查表
java
牛油果子哥q6 小时前
【C++ STL string 】C++ STL string 终极精讲:底层原理、内存机制、全套API、深浅拷贝、易错坑点与工程实战规范
数据库·c++
十五年专注C++开发6 小时前
MySql中各种功能用sql语句实现总结
数据库·sql·mysql
云烟成雨TD6 小时前
Spring AI 1.x 系列【52】可观测集成 SkyWalking
人工智能·spring·skywalking
云烟成雨TD6 小时前
Spring AI 1.x 系列【57】动态工具发现:Tool Search Tool
java·人工智能·spring
数据库小学妹6 小时前
AI时代数据库怎么选?多模融合、数据统一存储与选型实战指南
数据库·人工智能·经验分享·ai
zfoo-framework6 小时前
[修改代码使用]codex官方app中使用中转(不需要cc-switch) 1.config.toml 2.sk方式登录
java
Albert Edison6 小时前
【Redis】Centos7.9 安装 Redis 5 教程
数据库·redis·缓存
逍遥德7 小时前
MQTT教程详解-05.SpringBoot集成mqtt client 性能分析
java·spring boot·spring·mt