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

3259

积分

0

好友

422

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

接手新的代码仓库时,最让人头疼的往往是那些看似简单却埋着深坑的基础组件使用。就比如 @Transactional 这个注解,在很多项目中其用法可谓是五花八门,相当一部分配置甚至直接失效,完全起不到回滚的作用。

下意识地在涉及数据库操作的方法上添加 @Transactional 是个好习惯。但问题在于,很多开发者只是“加上去了事”,只要功能正常跑通,很少有人会专门去验证在异常情况下事务是否真的能正确回滚。这个注解用起来简单,却总能在你意想不到的地方“摆你一道”。本文将这些问题归纳为三类:不必要不生效不回滚,并通过具体的代码示例逐一剖析。

复杂的Java JSON处理代码示例

不必要

1. 无需事务的业务

在纯查询或仅进行HTTP请求等无写操作的方法上使用 @Transactional 注解,虽然通常不会引发错误,但从编码规范和技术严谨性的角度看,属于画蛇添足,建议移除。

@Transactional
public String testQuery() {
    standardBak2Service.getById(1L);
    return "testB";
}

2. 事务范围过大

有些开发者为了省事,直接将 @Transactional 注解标注在类或抽象类上。这会导致一个后果:该类(或其子类)中的所有方法都将被事务管理。这不仅增加了不必要的性能开销,也让代码逻辑变得不清晰。最佳实践是按需使用,只为真正需要事务管理的方法添加注解。

@Transactional
public abstract class BaseService {
}

@Slf4j
@Service
public class TestMergeService extends BaseService{

    private final TestAService testAService;

    public String testMerge() {
        testAService.testA();
        return "ok";
    }
}

需要注意的是,方法级别的事务配置会覆盖类级别的配置。例如,即使在类上配置了只读事务,方法上的 @Transactional 注解也会启用读写事务。

@Transactional(readOnly = true)
public class TestMergeService {

    private final TestBService testBService;
    private final TestAService testAService;

    @Transactional
    public String testMerge() {
        testAService.testA();
        testBService.testB();
        return "ok";
    }
}

不生效

3. 方法权限问题

切记,不要将 @Transactional 注解用于 private 方法!

@Transactional 的实现依赖于 Spring AOP 的动态代理机制。而 private 方法无法被代理,因此基于代理的AOP增强对其无效,事务自然也不会生效。

@Transactional
private String testMerge() {
    testAService.testA();
    testBService.testB();
    return "ok";
}

那么,如果在一个已开启事务的 public 方法内部调用一个 private 方法,事务会生效吗?答案是:会生效。因为事务的边界是那个 public 方法,其内部的调用都在同一个事务上下文中。

@Transactional
public String testMerge() throws Exception {
    ccc();
    return "ok";
}

private void ccc() {
    testAService.testA();
    testBService.testB();
}

4. 被 final、static 修饰的方法

原因与上述类似,被 finalstatic 修饰的方法上使用 @Transactional 同样不会生效。

  • static 方法属于类而非实例,代理机制无法拦截静态方法。
  • final 方法无法被子类重写,事务逻辑无法插入。
@Transactional
public static void b() {
}

@Transactional
public final void b() {
}

5. 同类内部方法调用问题

注意,这是 @Transactional 失效的重灾区!

网上常见的说法是:同类内部方法调用不会经过代理,因此事务不生效。这个说法比较片面,需要分情况讨论。

情况一: 外层方法 testMerge() 开启事务,调用同类的非事务方法 a()b()。此时若 b() 抛异常,根据事务传播特性,a()b() 的操作都在 testMerge() 的事务管理下,会一同回滚。

@Transactional
public String testMerge() {
    a();
    b();
    return "ok";
}

public void a() {
    standardBakService.save(testAService.buildEntity());
}

public void b() {
    standardBak2Service.save(testBService.buildEntity2());
    throw new RuntimeException("b error");
}

情况二(常出问题!): testMerge() 方法未开启事务,它调用了非事务方法 a()标注了 @Transactional 的方法 b()。当 b() 抛出异常时,a()b() 的事务都不会生效。因为这种调用是通过 this 引用直接进行的,并未经过代理。

public String testMerge() {
    a();
    b();
    return "ok";
}

public void a() {
    standardBakService.save(testAService.buildEntity());
}

@Transactional
public void b() {
    standardBak2Service.save(testBService.buildEntity2());
    throw new RuntimeException("b error");
}

5.1 独立的 Service 类

最直接的解决方案是将需要事务的方法(如 b())剥离到另一个独立的 Service 类中,通过 Spring 注入使用。这是最清晰、最推荐的方式,但可能会增加类的数量。

@Slf4j
@Service
public class TestBService {
      @Transactional
      public void b() {
          standardBak2Service.save(testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

5.2 自注入方式

通过注入自身的代理对象来调用。虽然 Spring 支持这种方式,但它破坏了清晰的依赖关系,可能引发循环依赖问题,需谨慎使用。

@Slf4j
@Service
public class TestMergeService {
      @Autowired
      private TestMergeService testMergeService;

      public String testMerge() {
          a();
          testMergeService.b(); // 通过注入的代理调用
          return "ok";
      }

      public void a() {
          standardBakService.save(testAService.buildEntity());
      }

      @Transactional
      public void b() {
          standardBak2Service.save(testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

5.3 手动获取代理对象

使用 AopContext.currentProxy() 手动获取当前类的代理对象,然后通过代理调用方法。这种方式同样能解决问题,但代码略显突兀。

@Slf4j
@Service
public class TestMergeService {
      public String testMerge() {
          a();
          ((TestMergeService) AopContext.currentProxy()).b(); // 手动获取代理
          return "ok";
      }

      public void a() {
          standardBakService.save(testAService.buildEntity());
      }

      @Transactional
      public void b() {
          standardBak2Service.save(testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

6. Bean 未被 Spring 管理

@Transactional 作为 Spring AOP 的一部分,其生效的前提是该类的实例必须由 Spring 容器管理。务必确保类上标注了 @Service@Component@Controller 等注解。

@Service // 必须有这个注解
public class TestBService {
    @Transactional
    public String testB() {
        standardBak2Service.save(entity2);
        return "testB";
    }
}

7. 异步线程调用

在多线程环境下,事务的传播需要特别注意。由于 Spring 的事务上下文是基于 ThreadLocal 实现的,不同线程拥有独立的事务上下文。

看这个例子:testMerge() 开启事务并调用 testA(),同时在新线程中调用 testB()testB() 中抛异常。

@Transactional
public String testMerge() {
    testAService.testA();
    new Thread(() -> {
        try {
            testBService.testB();
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }).start();
    return "ok";
}

@Transactional
public String testB() {
    DeepzeroStandardBak2 entity2 = buildEntity2();
    dataImportJob2Service.save(entity2);
    throw new RuntimeException("test2");
}

@Transactional
public String testA() {
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

结果: testA() 不回滚,testB() 回滚。

  • testA() 不回滚:因为主线程未捕获到新线程中抛出的异常。
  • testB() 回滚:它在新线程中独立运行,拥有自己的事务,异常导致其自身事务回滚。

8. 不支持事务的引擎

这属于基础设施层面的问题。我们常用的 MySQL,其 InnoDB 引擎支持事务,而 MyISAM 引擎则不支持。现代开发中已很少使用 MyISAM,因为其读性能优势已被 Redis、MongoDB 等专业中间件替代。

不回滚

9. 用错传播属性

@Transactionalpropagation 属性控制事务的传播行为,配置错误是导致不回滚的常见原因。它支持7种传播特性:

传播特性 说明
REQUIRED 默认。当前有事务则加入,没有则新建。
MANDATORY 必须存在当前事务,否则抛异常。
NEVER 必须在非事务环境下运行,否则抛异常。
REQUIRES_NEW 新建事务,挂起当前事务(如果存在)。
NESTED 嵌套事务,依赖于当前事务。
SUPPORTS 有事务则加入,没有则以非事务运行。
NOT_SUPPORTED 以非事务运行,挂起当前事务(如果存在)。

下面通过案例解析几种关键特性:

REQUIRES_NEW

无论是否存在外部事务,总是新建一个独立事务。外部事务被挂起,两个事务互不影响。

@Transactional
public String testMerge() {
    testAService.testA(); // 在外部事务中
    testBService.testB(); // 开启一个全新的独立事务
    return "ok";
}
// TestBService中
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String testB() {
    saveEntity2();
    throw new RuntimeException("testB"); // 仅回滚testB自己的事务
}

结果:testB() 的事务回滚,testMerge()testA() 的事务不受影响。

NESTED

在当前事务内创建一个嵌套的“子事务”。子事务是外部事务的一部分,其提交依赖于外部事务。外部事务回滚会导致子事务回滚;但子事务可以单独回滚而不影响外部事务(通过保存点机制)。

@Transactional
public String testMerge() {
    testAService.testA();
    testBService.testB(); // 嵌套事务
    throw new RuntimeException("testMerge"); // 外部事务异常
    return "ok";
}
// TestBService中
@Transactional(propagation = Propagation.NESTED)
public String testB() {
    saveEntity2();
    // throw new RuntimeException("testB"); // 如果这里抛异常,仅回滚子事务
    return "ok";
}

结果:由于外部事务 (testMerge) 异常,导致 testA()testB()(即使它正常执行)都回滚。

10. 自己吞了异常

这是导致事务不回滚的最常见原因!在业务代码中使用 try...catch 捕获了异常,却没有重新抛出,Spring 事务管理器就无法感知到异常,从而无法触发回滚。

@Transactional
public String testMerge() {
    try {
        testAService.testA();
        testBService.testB(); // 假设这里抛RuntimeException
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        // 没有 throw,异常在此被“吞掉”
    }
    return "ok";
}

解决方案:catch 块中重新抛出 Spring 事务能够处理的异常(如 RuntimeException)。

@Transactional
public String testMerge() {
    try {
        testAService.testA();
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new RuntimeException(e); // 重新抛出
    }
    return "ok";
}

注意: 这并非绝对。如果被调用的方法(如 testB())自身也标注了 @Transactional,且传播特性为默认的 REQUIRED,那么它的异常会在其自身方法边界被事务机制捕获。此时,即使外层 catch 不抛出,整个事务(因为 testB() 加入了外层事务)也可能因 testB() 的失败而回滚。但这依赖于正确的事务配置。

11. 事务无法捕获的异常

Spring 事务默认只回滚 RuntimeExceptionError。对于检查型异常(Checked Exception),如 ExceptionSQLExceptionIOException 等,默认是不回滚的。

@Transactional
public String testMerge() throws Exception {
    try {
        testAService.testA();
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new Exception(e); // 抛出检查型异常,默认不会导致回滚!
    }
    return "ok";
}

解决方案: 使用 @TransactionalrollbackFor 属性明确指定需要回滚的异常类型。

@Transactional(rollbackFor = Exception.class) // 指定Exception也回滚
public String testMerge() throws Exception {
    try {
        testAService.testA();
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new Exception(e);
    }
    return "ok";
}

为了避免因异常类型记忆不清导致的bug,一个稳妥的做法是始终显式设置 rollbackFor = Exception.classThrowable.class

12. 自定义异常范围问题

业务中常自定义异常,并习惯让其继承 RuntimeException。在使用时,如果 rollbackFor 只指定了自定义异常,而实际抛出的却是其他类型异常,同样会导致不回滚。

@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
    try {
        testAService.testA();
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new Exception(e); // 抛出的是Exception,不是CustomException
    }
    return "ok";
}

解决方案:catch 块中统一抛出你定义的自定义异常。

@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
    try {
        testAService.testA();
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new CustomException(e); // 统一转换为自定义异常
    }
    return "ok";
}

13. 嵌套事务中的异常处理

有时,我们希望调用一个可能失败的事务方法,但即使它失败了,也不影响主流程的继续执行。这时就需要在主方法中单独捕获并处理那个子事务的异常,防止其传播。

@Transactional
public String testMerge() {
    testAService.testA(); // 主事务操作
    try {
        testBService.testB(); // 一个可能失败的独立事务操作
    } catch (Exception e) {
        log.error("子操作B失败,但不影响主流程:{}", e);
        // 仅记录日志,不重新抛出异常
    }
    return "ok"; // testA()的操作被提交,testB()的操作因其自身事务回滚而撤销
}

// TestBService
@Service
public class TestBService {
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 关键:使用REQUIRES_NEW
    public String testB() {
        saveEntity2();
        throw new RuntimeException("test2"); // 仅回滚testB自己的事务
    }
}

关键点: 要实现这种隔离,被调用的方法(testB())的事务传播属性通常需要设置为 Propagation.REQUIRES_NEW,确保它在一个全新且独立的事务中运行。

总结

@Transactional 注解是 Spring 框架中非常强大且常用的特性,但其生效条件和使用细节也相当多。从 Spring AOP 的代理机制到不同的事务传播行为,再到异常处理,每一个环节都可能成为陷阱。希望本文梳理的这些常见场景能帮助你更透彻地理解 Spring 事务管理,在未来的开发中有效避坑,写出更加健壮可靠的数据访问代码。如果你在实践中遇到了其他诡异的事务问题,欢迎在 云栈社区Java 板块与大家交流讨论。




上一篇:17岁高中生开发AI卡路里追踪应用,四个月营收突破百万美元
下一篇:Spring事务管理进阶:解决@Transactional局限性的6种实战方案
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-3-10 12:41 , Processed in 0.424431 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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