【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 复制代码
相关推荐
知行合一。。。4 小时前
RAG--03--Milvus基本用法
数据库·oracle·milvus
2601_962071574 小时前
【Java报错已解决】org.springframework.beans.factory.BeanCreationException
java·开发语言
TDengine (老段)4 小时前
TDengine 线程模型 — 网络、调度、执行
大数据·数据库·物联网·制造·时序数据库·tdengine·涛思数据
zww89491115 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·eclipse
上学的小垃圾5 小时前
01-数据库系统概述
数据库
二进制漫游记6 小时前
FastAPI项目集成 Qdrant向量数据库+阿里云Embedding完整实战(工具封装+业务调用)
数据库·python·阿里云·embedding
2501_928996226 小时前
GPT-4o换DeepSeek迁移成本多少?中科热备解析API聚合平台技术账本
前端·数据库·人工智能
zww89491117 小时前
匿名树洞系统开发实战:从需求分析到部署指南
java·eclipse
泡干脆面就番茄8 小时前
MySQL_子查询_分页查询与联合查询详解
数据库·mysql
黑马程序员毕设9 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设