【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 复制代码
相关推荐
这个DBA有点耶4 小时前
NULL不是空——数据库里最反直觉的设计,90%新人踩过的坑
数据库·mysql·代码规范
karry_k5 小时前
MyBatis批量insert-select踩坑:useGeneratedKeys=true 可能让PostgreSQL返回大量插入结果
java·后端
karry_k5 小时前
PostgreSQL 在 MyBatis 中执行正常 SQL 失效:一次 DELETE USING 踩坑记录
java·后端
这个DBA有点耶6 小时前
AI写的SQL跑崩了生产库,这锅谁背?
数据库·人工智能·程序员
镜舟科技7 小时前
Databricks 再提 LTAP,AI 时代的数据底座为何重回大一统叙事?
数据库·架构·agent
Databend8 小时前
从湖仓升级为 Agent 时代的数据控制面,Snowflake 和 Databricks 有哪些布局
大数据·数据库·agent
SamDeepThinking8 小时前
从源码到代码:MyBatis-Flex 与 MyBatis-Plus 的逐项对比
java·后端·程序员
ClouGence11 小时前
SQL Server CDC 能放到 Always On 备库读吗?一文讲透原理与实践
数据库·sql server
她的男孩11 小时前
Spring Boot 接 Flowable 工作流:用 3 个注解搭一个请假审批流程
java·后端·架构
荣码13 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python