【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 复制代码
相关推荐
xmjd msup1 小时前
mysql的分区表
数据库·mysql
MeAT ITEM2 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
salipopl2 小时前
Spring Boot 整合 Druid 并开启监控
java·spring boot·后端
dovens2 小时前
PostgreSQL 中进行数据导入和导出
大数据·数据库·postgresql
IOT.FIVE.NO.12 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库
ShiJiuD6668889992 小时前
JSP Cookie和Session
java·开发语言
Rick19932 小时前
mysql 慢查询怎么快速定位
android·数据库·mysql
geNE GENT2 小时前
Spring Boot 实战篇(四):实现用户登录与注册功能
java·spring boot·后端
952368 小时前
MyBatis
后端·spring·mybatis