面试题:Spring中 @Transactional 注解什么时候会造成事务失效?

面试题:Spring中 @Transactional 注解什么时候会造成事务失效?

导致Transational事务失效的常见场景主要有以下7个:

    1. @Transactional 应用在非 public 修饰的方法上
    1. 代码中使用了try/catch处理了异常
    1. 调用方法内部使用了@Transational注解
    1. timeout超时时间设置过小
    1. 数据库不支持事务
  • 6.@Transactional 注解属性 propagation 设置错误
  • 7.@Transactional 注解属性 rollbackFor 设置错误

如果应付面试的话,光记住恐怕还不行,因为面试官可能会问这些事务失效的场景是为什么。所以本文就详细说说事务失效的原因。由于timeout超时时间设置过小、数据库不支持事务这两个原因太简单了,这里就不说了。

什么是事务?

简单来说事务就是一组数据库的操作,,这组操作要么全部执行成功,要么全部执行失败,没有中间状态。事务具有以下四个标准特性,通常被称为 ACID 特性:

  1. 原子性(Atomicity): 事务是一个不可分割的工作单位,要么全部执行成功,要么全部执行失败。如果事务的任何一部分操作失败,整个事务将回滚到初始状态,之前的操作将被撤销。
  2. 一致性(Consistency): 事务的执行使数据库从一个一致性状态转移到另一个一致性状态。这意味着事务的执行不能破坏数据库的完整性约束,比如主键、外键等。
  3. 隔离性(Isolation): 事务的执行应该与其他事务相互隔离,一个事务的执行不应影响其他事务的执行。数据库系统通常提供了不同的隔离级别,以控制事务之间的可见性和相互影响。
  4. 持久性(Durability): 一旦事务提交,其对数据库的修改应该是永久性的,即使系统崩溃,之前提交的事务的结果也应该得以保留。

事务在数据库中的应用非常广泛,特别是在需要确保数据的完整性、一致性和可靠性的应用中,例如在线银行交易、电商购物等。

Spring中的事务

Spring 提供了强大的事务管理机制,主要分为编程式事务声明式事务两种方式。

编程式事务:

编程式事务是指在代码中手动管理事务的提交、回滚等操作。虽然这种方式代码侵入性较强,但它允许开发者更加灵活地控制事务的执行流程。

java 复制代码
try {
    // 执行一些操作
    transactionManager.commit(status);
} catch (Exception e) {
    transactionManager.rollback(status);
    throw new CustomException("事务执行失败");
}

声明式事务:

声明式事务通过 AOP(面向切面编程)实现,将具体业务与事务处理部分解耦,减少了代码侵入性。Spring 中提供了两种声明式事务的实现方式,一是基于 XML 配置文件的方式,另一种是基于 @Transactional 注解。

  • XML 配置方式:
xml 复制代码
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="test" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="serviceOperation" expression="execution(* com.example.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
  • 注解方式:
java 复制代码
@Service
public class MyService {

    @Transactional
    public void test() {
        // 执行一些操作
    }
}

声明式事务的优势在于代码的简洁性和可维护性,通过注解或配置文件的方式,开发者能够更清晰地定义事务的边界和属性。选择编程式事务还是声明式事务通常取决于具体的业务需求和开发团队的偏好。

@Transactional注解可以作用于哪些地方?

@Transactional 注解在 Spring 中可以应用于接口、类、以及类的方法上。具体应用的位置会影响事务的作用范围和优先级。

  • **作用于类:**当 @Transactional 注解放在类上时,表示该类的所有 public 方法都会应用相同的事务属性信息。这样,类的所有方法都将遵循相同的事务规则。
java 复制代码
@Transactional
@RestController
@RequestMapping("/example")
public class MyController {
    
    @Autowired
    private MyService myService;
    
    @GetMapping("/test")
    public String test() {
        return myService.doSomething();
    }
}
  • 作用于方法: 如果类和方法都配置了 @Transactional,方法的事务配置将覆盖类的事务配置。这允许对某个方法使用不同的事务属性,而不受类级别的限制。
java 复制代码
@RestController
@RequestMapping("/example")
public class MyController {
    
    @Autowired
    private MyService myService;
    
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/test")
    public String test() {
        return myService.doSomething();
    }
}
  • 作用于接口: 在接口上使用 @Transactional 不是推荐的做法,因为它可能导致 @Transactional 失效,尤其是在使用 Spring AOP 以及 CGLib 动态代理的情况下。通常,事务注解更适合用于具体的类或方法上,而不是接口上。

总的来说,@Transactional 注解提供了一种灵活的方式来定义事务的行为,但在使用时需要注意注解的放置位置,以确保符合预期的事务行为。

事务失效的7种场景

1. @Transactional 应用在非 public 修饰的方法上

@Transactional 注解的事务机制是通过 Spring AOP 实现的,而 Spring AOP 基于动态代理。对于动态代理而言,如果一个类中的方法不是 public,默认情况下无法被包外的代码直接访问。这是因为 Java 访问权限的控制,只有 public 方法才能被其他类或包外的代码调用。

当一个方法被 @Transactional 注解修饰时,Spring AOP 会在运行时生成一个代理对象,该代理对象负责处理事务。然而,由于非 public 方法默认对外部不可见,代理对象也无法直接调用这些非 public 方法。

因此,如果一个类中有被 @Transactional 注解修饰的方法,而这些方法是非 public 的,Spring AOP 生成的代理对象无法直接调用这些方法,从而导致 @Transactional 注解在非 public 方法上不会生效。因此,为了确保 @Transactional 注解正常工作,被注解的方法应该是 public 的。以下是相关代码:

java 复制代码
protected TransactionAttribute computeTransactionAttribute(Method method, Class&lt;?&gt; targetClass) {
   // 不允许非 public 方法作为事务要求
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
   }
   // 其他代码省略....
}

因此,当使用 @Transactional 注解时,请确保被注解的方法是 public 的,以确保 Spring AOP 能够正确生成代理对象并应用事务。

2. 代码中使用了try/catch处理了异常

@Transactional 注解的执行流程遵循事务的标准生命周期:在方法执行前自动开启事务,在方法成功执行完后自动提交事务。然而,如果方法中存在 try/catch 块,且在 catch 块中捕获了异常,事务将不会自动回滚。这一行为的主要原因与 @Transactional 注解的实现有关。部分实现源码如下:

java 复制代码
protected Object invokeWithinTransaction(Method method, Class&lt;?&gt; targetClass, final InvocationCallback invocation)
      throws Throwable {
   // 获取方法的事务属性
   final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   final String joinpointIdentification = methodIdentification(method, targetClass);

   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
       // 标准事务划界,通过 getTransaction 和 commit/rollback 调用。
       // 自动开启事务
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
         // 这是一个 around advice:调用链中的下一个拦截器。
         // 通常导致调用目标对象的方法。
         // 反射调用业务方法
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
          // 目标调用异常
          // 在 catch 逻辑中,执行事务回滚
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
       // 自动提交事务
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }
   else {
     // .....
   }
}

从上述实现源码中可以看出,当方法中出现异常时,@Transactional 才能察觉到异常并执行事务回滚。然而,如果开发者在方法中手动添加了 try/catch 块,并在 catch 块中捕获了异常,@Transactional 将无法感知到异常,从而不会触发事务的自动回滚。这就是当 @Transactional 遇到 try/catch 块时,事务不会自动回滚的原因。

简单来说就是,@Transactional的实现方法是当发生异常的时候才会自动触发回滚,如果使用try/catch将异常捕获了,那么事务可能就无法回滚,导致事务失效。

3. 调用方法内部使用了@Transational注解

java 复制代码
@RequestMapping("/save")
public int saveMapping(UserInfo userInfo) {
    return save(userInfo);
}

@Transactional
public int save(UserInfo userInfo) {
    // 非空验证
    if (userInfo == null ||
        !StringUtils.hasLength(userInfo.getUsername()) ||
        !StringUtils.hasLength(userInfo.getPassword()))
        return 0;
    int result = userService.save(userInfo);
    int num = 10 / 0; // 引发一个异常
    return result;
}

如上面代码所示,当调用类内部的被 @Transactional 修饰的方法时,事务可能不会生效。这是因为 @Transactional 是基于 Spring AOP 实现的,而 Spring AOP 又是基于动态代理实现的。

当调用类内部的方法时,Spring AOP 生成的代理对象可能不会被触发,因为这个调用是通过 this 对象而不是代理对象完成的。这就绕过了代理对象,导致事务失效。

为确保 @Transactional 在类内部调用的方法上生效,一种常见的解决方案是将 @Transactional 注解放在另一个专门的 Service 类中,然后在该 Service 类内部调用需要事务支持的方法。这样确保了方法的调用会经过代理对象,从而使事务生效。

java 复制代码
@Service
public class TransactionalService {
    
    @Autowired
    private UserService userService;

    @Transactional
    public int save(UserInfo userInfo) {
        // 非空验证
        if (userInfo == null ||
            !StringUtils.hasLength(userInfo.getUsername()) ||
            !StringUtils.hasLength(userInfo.getPassword()))
            return 0;
        int result = userService.save(userInfo);
        int num = 10 / 0; // 引发一个异常
        return result;
    }
}

然后在 Controller 中调用 TransactionalService 的方法,这样可以确保事务在类内部的调用中得到正确的应用:

java 复制代码
@RequestMapping("/save")
public int saveMapping(UserInfo userInfo) {
    return transactionalService.save(userInfo);
}

6.@Transactional 注解属性 propagation 设置错误

当配置了错误的 propagation 属性时,可能导致事务不会发生回滚。以下是三种错误的配置示例:

  1. TransactionDefinition.PROPAGATION_SUPPORTS: Propagation.SUPPORTS 表示如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。在下面这个例子中,即使方法中抛出了异常,由于配置为支持当前事务,不会触发回滚。
java 复制代码
@Transactional(propagation = Propagation.SUPPORTS)
public void methodWithSupportsPropagation() {
    // 一些操作
    throw new RuntimeException("Simulating an error");
}
  1. TransactionDefinition.PROPAGATION_NOT_SUPPORTED: NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。在下面这个例子中,即使方法中抛出了异常,由于配置为不支持事务,也不会触发回滚。
java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodWithNotSupportedPropagation() {
    // 一些操作
    throw new RuntimeException("Simulating an error");
}
  1. TransactionDefinition.PROPAGATION_NEVER: NEVER 表示以非事务方式运行,如果当前存在事务,则抛出异常,在这个例子中,即使方法中抛出了异常,由于配置为绝对不支持事务,也不会触发回滚。
java 复制代码
@Transactional(propagation = Propagation.NEVER)
public void methodWithNeverPropagation() {
    // 一些操作
    throw new RuntimeException("Simulating an error");
}

这些错误的配置可能导致事务不按预期回滚,因此在配置事务的传播行为时,确保了解每种传播行为的含义,并根据业务需求进行正确的配置。

7.@Transactional 注解属性 rollbackFor 设置错误

@Transactional 注解中,属性 rollbackFor 用于指定能够触发事务回滚的异常类型,如果设置错误,可能导致事务不会按预期进行回滚。以下是一个示例:

java 复制代码
@Transactional(rollbackFor = MyException.class)
public void someTransactionalMethod() {
    try {
        // 一些业务操作
        throw new RuntimeException("Simulating an error");
    } catch (RuntimeException e) {
        // 捕获异常,但并不抛出 MyException
    }
}

在上述例子中,rollbackFor 属性被设置为 MyException.class,但实际上在方法中抛出的是 RuntimeException。因此,尽管发生了异常,但由于异常类型不匹配,事务可能不会按照预期进行回滚。

正确的设置应该与实际抛出的异常类型一致,或者设置为异常类型的父类,以确保所有符合条件的异常都能触发事务回滚。例如:

java 复制代码
@Transactional(rollbackFor = RuntimeException.class)
public void someTransactionalMethod() {
    try {
        // 一些业务操作
        throw new MyException("Simulating an error");
    } catch (MyException e) {
        // 捕获异常
    }
}

在这个例子中,rollbackFor 属性被设置为 RuntimeException.class,确保了对于任何继承自 RuntimeException 的异常,都会触发事务回滚。

总结

事务是数据库操作的一组原子性、一致性、隔离性和持久性特性的集合。在Spring中,事务管理分为编程式事务和声明式事务。声明式事务使用@Transactional注解,但在使用时需要注意一些场景,否则可能导致事务失效。

  1. @Transactional 应用在非 public 修饰的方法上: @Transactional 是基于Spring AOP实现的,而AOP基于动态代理。非public方法默认对外不可见,导致AOP生成的代理对象无法直接调用非public方法,从而事务失效。确保被注解的方法是public,以保证AOP正确生成代理对象并应用事务。
  2. 代码中使用了try/catch处理了异常: @Transactional 在方法执行前开启事务,在成功执行后提交事务。但如果在方法中使用try/catch捕获异常,事务将不会自动回滚。@Transactional是在发生异常时才会触发回滚,try/catch块可能阻止异常传播,导致事务不回滚。
  3. 调用方法内部使用了@Transational注解: 当调用类内部的被 @Transactional 修饰的方法时,由于Spring AOP基于动态代理实现,可能绕过代理对象,使事务失效。解决方法是将 @Transactional 注解放在专门的Service类中,确保方法的调用会经过代理对象,使事务生效。
  4. @Transactional 注解属性 propagation 设置错误: 事务的传播行为配置错误可能导致事务不回滚。例如,Propagation.SUPPORTS表示如果存在事务则加入,不触发回滚。了解每种传播行为的含义,并根据业务需求正确配置。
  5. @Transactional 注解属性 rollbackFor 设置错误: rollbackFor 属性用于指定能够触发事务回滚的异常类型。如果设置错误,可能导致事务不按预期回滚。确保设置与实际抛出异常类型一致,或设置为异常类型的父类,以保证触发事务回滚。

这些场景是导致事务失效的主要原因,了解并正确处理这些情况是保障事务一致性和可靠性的关键。

更多优质内容

微信公众号:ByteRaccoon、知乎\稀土掘金\小红书都叫:浣熊say

PS:本文当中有些链接跳转不过去,是因为掘金禁止跳转微信公众号的链接,对连接内容感兴趣的,可以直接看我的微信公众号原文,里面的文章可以正常跳转。

相关推荐
程序员爱钓鱼11 分钟前
Go语言实战案例-项目实战篇:新闻聚合工具
后端·google·go
IT_陈寒13 分钟前
Python开发者必须掌握的12个高效数据处理技巧,用过都说香!
前端·人工智能·后端
一只叫煤球的猫8 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9659 小时前
tcp/ip 中的多路复用
后端
bobz9659 小时前
tls ingress 简单记录
后端
皮皮林55110 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友10 小时前
什么是OpenSSL
后端·安全·程序员
bobz96510 小时前
mcp 直接操作浏览器
后端
前端小张同学13 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook13 小时前
Manim实现闪光轨迹特效
后端·python·动效