找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3259

积分

0

好友

422

主题
发表于 3 天前 | 查看: 16| 回复: 0

昨天有朋友在面试时被问到 @Transactional 注解在哪些情况下会失效,一时没能答上来。这确实是日常开发中一个容易踩坑的点。今天,我们就来系统地梳理一下 @Transactional 相关的知识点,并重点分析导致其失效的几种常见场景。

@Transactional 注解大家应该都不陌生,它是 Spring 框架中用于声明式事务管理的关键注解,能确保方法内多个数据库操作具备原子性——要么全部成功,要么全部回滚。然而,如果不了解其工作原理和使用细节,你很可能会发现事务并没有按照预期工作。接下来,我们先从事务的基础概念讲起。

一、Spring 事务管理简介

在系统开发中,事务管理至关重要。Spring 提供了完善的事务管理机制,主要分为编程式事务声明式事务两种。

  • 编程式事务:指在代码中手动管理事务的提交、回滚等操作。这种方式代码侵入性强。
    try {
        //TODO something
         transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw new InvoiceApplyException(“异常失败”);
    }
  • 声明式事务:基于 AOP(面向切面编程)实现,将具体业务逻辑与事务处理代码解耦,代码侵入性低,因此在实际开发中更为常用。声明式事务有两种配置方式:一是基于 XML 配置文件,另一种就是使用 @Transactional 注解。
        @Transactional
        @GetMapping(“/test”)
        public String test() {
            int insert = cityInfoDictMapper.insert(cityInfoDict);
        }

二、@Transactional 注解详解

1. 注解作用域

@Transactional 可以作用于接口类方法

  • 作用于类:表示该类的所有 public 方法都使用相同的事务属性配置。
  • 作用于方法:如果类和方法都配置了 @Transactional,方法上的配置会覆盖类上的配置。
  • 作用于接口不推荐。如果将注解标注在接口上,并且 Spring AOP 使用了 CGLib 动态代理(例如,代理类没有实现接口),将导致 @Transactional 注解失效。

下面是一个同时使用类级别和方法级别注解的例子:

@Transactional
@RestController
@RequestMapping
public class MybatisPlusController {
    @Autowired
    private CityInfoDictMapper cityInfoDictMapper;

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(“/test”)
    public String test() throws Exception {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setParentCityId(2);
        cityInfoDict.setCityName(“2”);
        cityInfoDict.setCityLevel(“2”);
        cityInfoDict.setCityCode(“2”);
        int insert = cityInfoDictMapper.insert(cityInfoDict);
        return insert + “”;
    }
}

2. 注解核心属性

  • propagation(传播行为):默认值为 Propagation.REQUIRED

    • REQUIRED:如果当前存在事务,则加入该事务;否则新建一个事务。
    • SUPPORTS:如果当前存在事务,则加入;否则以非事务方式运行。
    • MANDATORY:必须在一个已有的事务中运行,否则抛异常。
    • REQUIRES_NEW:总是新建一个事务,如果当前存在事务,则将其挂起。
    • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将其挂起。
    • NEVER:以非事务方式运行,如果当前存在事务,则抛异常。
    • NESTED:如果当前存在事务,则在嵌套事务内执行;否则行为同 REQUIRED
  • isolation(隔离级别):默认值为 Isolation.DEFAULT,即使用底层数据库的默认隔离级别。其他选项包括 READ_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE

  • timeout(超时时间):单位为秒,默认值为 -1(永不超时)。超过时间限制事务未完成则自动回滚。

  • readOnly(是否只读):默认 false。设为 true 可优化读取操作。

  • rollbackFor/noRollbackFor(回滚/不回滚的异常类型):用于指定触发回滚或不触发回滚的异常类数组。

三、@Transactional 失效的六大场景分析

了解了基础知识后,我们进入正题,结合代码分析 @Transactional 注解为何会“莫名其妙”地失效。

场景一:注解应用于非 public 方法

如果 @Transactional 注解应用在 protectedprivate 或包级可见的方法上,事务将不会生效

Spring AOP事务拦截流程图

失效原因在于 Spring AOP 代理的机制。如上图所示,TransactionInterceptor(事务拦截器)在调用目标方法前后进行拦截。在决定是否启用事务时,会检查目标方法的修饰符。相关源码片段如下:

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don‘t allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}

此方法会检查目标方法是否为 public,如果不是则直接返回 null,导致无法获取事务配置信息。

注意:此时事务虽然无效,但程序不会抛出任何异常,这很容易在不知不觉中导致数据不一致问题。

场景二:propagation 属性设置不当

如果错误地配置了以下三种事务传播行为,事务将不会在异常时回滚:

  • TransactionDefinition.PROPAGATION_SUPPORTS:支持当前事务,不存在则以非事务运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式执行,挂起当前事务。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式执行,存在事务则抛异常。

这属于配置错误导致的失效,需要根据业务逻辑谨慎选择传播行为。合理使用事务传播机制是构建健壮后端 & 架构的关键之一。

场景三:rollbackFor 属性设置错误

Spring 事务默认只对未检查异常(unchecked exceptions,即 RuntimeException 及其子类)Error 进行回滚。对于已检查异常(checked exceptions,即 Exception 的子类但非 RuntimeException 子类),默认不会触发回滚。

Java异常类继承关系图

如果你在事务方法中抛出了一个自定义的已检查异常,并期望事务回滚,就必须通过 rollbackFor 属性来指定。

// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)

Spring 会判断抛出的异常是否为 rollbackFor 指定异常或其子类,从而决定是否回滚。

场景四:同一类内的方法调用

这是非常高频的一个错误场景。考虑以下情况:类中有方法 A 和方法 B,方法 A 未声明事务,方法 B 声明了 @Transactional。当方法 A 内部直接调用方法 B 时,方法 B 的事务不会生效

//@Transactional  // 注意:A方法本身没有事务注解
    @GetMapping(“/test”)
    private Integer A() throws Exception {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setCityName(“2”);
        /**
         * B 插入字段为 3的数据
         */
        this.insertB(); // 内部调用
        /**
         * A 插入字段为 2的数据
         */
        int insert = cityInfoDictMapper.insert(cityInfoDict);
        return insert;
    }

    @Transactional()
    public Integer insertB() throws Exception {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setCityName(“3”);
        cityInfoDict.setParentCityId(3);
        return cityInfoDictMapper.insert(cityInfoDict);
    }

原因:Spring 的事务管理是通过 AOP 代理实现的。只有被代理对象调用的方法,事务拦截器才能介入。当通过 this.insertB() 进行内部调用时,调用的是目标对象自身的方法,而非代理对象的方法,因此事务增强逻辑被绕过。

场景五:异常被 catch 捕获并“吞掉”

这是导致事务失效的最常见原因之一。

    @Transactional
    private Integer A() throws Exception {
        int insert = 0;
        try {
            CityInfoDict cityInfoDict = new CityInfoDict();
            cityInfoDict.setCityName(“2”);
            cityInfoDict.setParentCityId(2);
            /**
             * A 插入字段为 2的数据
             */
            insert = cityInfoDictMapper.insert(cityInfoDict);
            /**
             * B 插入字段为 3的数据
             */
            b.insertB(); // 假设B方法内部抛出了异常
        } catch (Exception e) {
            e.printStackTrace(); // 仅仅打印,未重新抛出!
        }
        return insert;
    }

如果 b.insertB() 抛出了异常,但被 A() 方法内的 try-catch 捕获并处理(没有重新抛出),那么事务将不会回滚。这是因为 Spring 事务的回滚触发机制依赖于异常是否传播到事务拦截器。

更棘手的情况是,你可能看到 UnexpectedRollbackException 异常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

这通常发生在嵌套方法调用且传播行为为 REQUIRED 时。内层方法(如insertB)抛异常,标记事务需要回滚(rollback-only)。但外层方法(A)捕获了该异常,并试图正常提交事务。这种“一个要回滚,一个要提交”的矛盾状态导致了 UnexpectedRollbackException

最佳实践:在事务方法中,除非有特殊需求,否则不要轻易捕获异常。如果必须捕获,请确保在 catch 块中抛出 RuntimeException 或通过 @Transactional(rollbackFor=Exception.class) 指定的异常。

场景六:数据库引擎不支持事务

这种情况相对少见,但需知晓。事务能否生效,底层数据库引擎的支持是根本前提。以常用的 MySQL 为例,其默认的 InnoDB 引擎是支持事务的。但如果表使用了 MyISAM 引擎,那么即便代码层面配置了 @Transactional,事务也完全不会生效,因为 MyISAM 引擎本身不支持事务。

总结

@Transactional 注解使用简单,但背后的机制和细节不少。希望通过本文对6种失效场景的剖析,能帮助你更深刻地理解 Java 中声明式事务的工作方式,在开发中有效避坑。事务一致性是数据准确性的基石,务必重视。

在实际工作中遇到其他疑难杂症,也欢迎到云栈社区与更多开发者交流探讨。




上一篇:Spring事务管理三剑客:@Transactional、TransactionTemplate与TransactionManager实战选型
下一篇:Spring事务@Transactional注解的12种常见失效原因全面解析与避坑指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 11:29 , Processed in 0.776648 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表