美文网首页Spring
spring——@Transactional为啥不生效

spring——@Transactional为啥不生效

作者: 柯柏笔记 | 来源:发表于2020-04-10 15:37 被阅读0次

    Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。
    在实际工作中,只是简单的在方法上加上@Transactional ,就理所当然的认为能控制事务,这里面是有些坑,如果没用对,事务是不生效的,导致数据不一致,需要大量的排查,以及清洗数据,下面聊一聊,什么情况下会导致事务不生效。

    事务

    • 编程式事务:是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强,如下示例:
    try {
        //TODO something
         transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw new InvoiceApplyException("异常失败");
    }
    
    • 声明式事务:基于AOP面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。声明式事务也有两种实现方式,一是基于TX和AOP的xml配置文件方式,二种就是基于@Transactional注解了。
       @Transactional
        public void createUserPublic(User user){
            userRepository.save(user);
            
        }
    

    @Transactional介绍

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Transactional {
    
        /**
         * Alias for {@link #transactionManager}.
         * @see #transactionManager
         */
        @AliasFor("transactionManager")
        String value() default "";
    
        /**
         * A <em>qualifier</em> value for the specified transaction.
         * <p>May be used to determine the target transaction manager,
         * matching the qualifier value (or the bean name) of a specific
         * {@link org.springframework.transaction.PlatformTransactionManager}
         * bean definition.
         * @since 4.2
         * @see #value
         */
        @AliasFor("value")
        String transactionManager() default "";
    
        /**
         * The transaction propagation type.
         * <p>Defaults to {@link Propagation#REQUIRED}.
         * @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
         */
        Propagation propagation() default Propagation.REQUIRED;
    
        /**
         * The transaction isolation level.
         * <p>Defaults to {@link Isolation#DEFAULT}.
         * <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
         * {@link Propagation#REQUIRES_NEW} since it only applies to newly started
         * transactions. Consider switching the "validateExistingTransactions" flag to
         * "true" on your transaction manager if you'd like isolation level declarations
         * to get rejected when participating in an existing transaction with a different
         * isolation level.
         * @see org.springframework.transaction.interceptor.TransactionAttribute#getIsolationLevel()
         * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
         */
        Isolation isolation() default Isolation.DEFAULT;
    
        /**
         * The timeout for this transaction (in seconds).
         * <p>Defaults to the default timeout of the underlying transaction system.
         * <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
         * {@link Propagation#REQUIRES_NEW} since it only applies to newly started
         * transactions.
         * @see org.springframework.transaction.interceptor.TransactionAttribute#getTimeout()
         */
        int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    
        /**
         * A boolean flag that can be set to {@code true} if the transaction is
         * effectively read-only, allowing for corresponding optimizations at runtime.
         * <p>Defaults to {@code false}.
         * <p>This just serves as a hint for the actual transaction subsystem;
         * it will <i>not necessarily</i> cause failure of write access attempts.
         * A transaction manager which cannot interpret the read-only hint will
         * <i>not</i> throw an exception when asked for a read-only transaction
         * but rather silently ignore the hint.
         * @see org.springframework.transaction.interceptor.TransactionAttribute#isReadOnly()
         * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
         */
        boolean readOnly() default false;
    
        /**
         * Defines zero (0) or more exception {@link Class classes}, which must be
         * subclasses of {@link Throwable}, indicating which exception types must cause
         * a transaction rollback.
         * <p>By default, a transaction will be rolling back on {@link RuntimeException}
         * and {@link Error} but not on checked exceptions (business exceptions). See
         * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
         * for a detailed explanation.
         * <p>This is the preferred way to construct a rollback rule (in contrast to
         * {@link #rollbackForClassName}), matching the exception class and its subclasses.
         * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}.
         * @see #rollbackForClassName
         * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
         */
        Class<? extends Throwable>[] rollbackFor() default {};
    
        /**
         * Defines zero (0) or more exception names (for exceptions which must be a
         * subclass of {@link Throwable}), indicating which exception types must cause
         * a transaction rollback.
         * <p>This can be a substring of a fully qualified class name, with no wildcard
         * support at present. For example, a value of {@code "ServletException"} would
         * match {@code javax.servlet.ServletException} and its subclasses.
         * <p><b>NB:</b> Consider carefully how specific the pattern is and whether
         * to include package information (which isn't mandatory). For example,
         * {@code "Exception"} will match nearly anything and will probably hide other
         * rules. {@code "java.lang.Exception"} would be correct if {@code "Exception"}
         * were meant to define a rule for all checked exceptions. With more unusual
         * {@link Exception} names such as {@code "BaseBusinessException"} there is no
         * need to use a FQN.
         * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(String exceptionName)}.
         * @see #rollbackFor
         * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
         */
        String[] rollbackForClassName() default {};
    
        /**
         * Defines zero (0) or more exception {@link Class Classes}, which must be
         * subclasses of {@link Throwable}, indicating which exception types must
         * <b>not</b> cause a transaction rollback.
         * <p>This is the preferred way to construct a rollback rule (in contrast
         * to {@link #noRollbackForClassName}), matching the exception class and
         * its subclasses.
         * <p>Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(Class clazz)}.
         * @see #noRollbackForClassName
         * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
         */
        Class<? extends Throwable>[] noRollbackFor() default {};
    
        /**
         * Defines zero (0) or more exception names (for exceptions which must be a
         * subclass of {@link Throwable}) indicating which exception types must <b>not</b>
         * cause a transaction rollback.
         * <p>See the description of {@link #rollbackForClassName} for further
         * information on how the specified names are treated.
         * <p>Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(String exceptionName)}.
         * @see #noRollbackFor
         * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
         */
        String[] noRollbackForClassName() default {};
    

    @Transactional可以用作在那些地方

    从源码可以看出来

    1. 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。
    2. 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
    3. 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效

    @Transactional的属性

    1. propagation属性
      propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:
      Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
      Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
      Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
      Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
      Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
      Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
      Propagation.NESTED :和 Propagation.REQUIRED 效果一样。
    2. isolation 属性
      isolation :事务的隔离级别,默认值为 Isolation.DEFAULT。
      Isolation.DEFAULT:使用底层数据库默认的隔离级别。
      Isolation.READ_UNCOMMITTED
      Isolation.READ_COMMITTED
      Isolation.REPEATABLE_READ
      Isolation.SERIALIZABLE
    3. timeout 属性
      timeout :事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
    4. readOnly 属性
      readOnly :指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
    5. rollbackFor 属性
      rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
    6. noRollbackFor属性**
      noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

    @Transactional失效场景

    准备工作

    做一个demo,方便演示

    • 数据库对应的实体类
    @Data
    @Table(name = "test_user")
    @Entity
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        /**
         * 姓名
         */
        private String name;
    
        /**
         * 年龄
         */
        private Integer age;
    
        /**
         * 联系电话
         */
        private String phone;
    
    }
    
    • 数据库访问类
      为了方便演示,使用的sprigjpa,已可以改造现在比较流行的mybatis
    @Repository
    public interface UserRepository  extends JpaRepository<User,Long> {
    }
    
    • 创建一个service
    Service
    @Slf4j
    public class UserService {
    
        @Autowired
        private UserRepository userRepository;
    
        public int createUserWrong1(User user){
            try {
                 this.createUserPrivate(user);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return userRepository.findAll().size();
        }
    
        /**
         * 方法私有
         * @param user
         */
        @Transactional
        private void createUserPrivate(User user){
            userRepository.save(user);
            //if (user.getAge()==100){
            //    throw new RuntimeException("invalid age");
            //}
        }
    
        /**
         * 方法私有
         * @param user
         */
        @Transactional
        public void createUserPublic(User user){
            userRepository.save(user);
            if (user.getAge()==100){
                throw new RuntimeException("invalid age");
            }
        }
    }
    
    • 再创建一个controller
    RestController
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @RequestMapping("/user/save")
        public int saveUser(@RequestBody User user){
            return userService.createUserWrong1(user);
        }
    }
    

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

     /**
         * 方法私有
         * @param user
         */
        @Transactional
        private void createUserPrivate(User user){
            userRepository.save(user);
            if (user.getAge()==100){
                throw new RuntimeException("invalid age");
            }
        }
    

    因为在Spring AOP 代理时, TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。

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

    此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
    注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错

    2 同一个类中方法调用,导致@Transactional失效

    原因:必须通过代理过的类从外部调用目标方法才能生效

      public int createUserWrong1(User user){
            try {
                 this.createUserPublic(user);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return userRepository.findAll().size();
        }
    
    //方法内部调用
    Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    //方法外部调用 aop代理的是UserService 所以生效
    Creating new transaction with name [com.kb.pit.transactional.service.UserService.createUserPublic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    

    由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
    如果我们把@Transactional放到外部调用的方法上,这样代理的就是userService

    3 异常被你的 catch“吃了”导致@Transactional失效

        @Transactional
        public int createUserWrong2(User user){
            try {
               userRepository.save(user);
              throw new RuntimeException("invalid age");
            } catch (Exception e) {
                e.printStackTrace();
        }
            return userRepository.findAll().size();
        }
        }
    
    

    事务生效,但是数据没有回滚,因为捕获了异常,需要主动回滚

    • 只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚,在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理:
            if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
                // Standard transaction demarcation with getTransaction and commit/rollback calls.
                TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    
                Object retVal;
                try {
                    // This is an around advice: Invoke the next interceptor in the chain.
                    // This will normally result in a target object being invoked.
                    retVal = invocation.proceedWithInvocation();
                }
                catch (Throwable ex) {
                    // target invocation exception
                    completeTransactionAfterThrowing(txInfo, ex);
                    throw ex;
                }
                finally {
                    cleanupTransactionInfo(txInfo);
                }
    
    • 默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。打开 Spring 的 DefaultTransactionAttribute 类能看到如下代码块,可以发现相关证据,通过注释也能看到 Spring 这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error 或 RuntimeException 代表了非预期的结果,应该回滚
    /**
         * The default behavior is as with EJB: rollback on unchecked exception
         * ({@link RuntimeException}), assuming an unexpected outcome outside of any
         * business rules. Additionally, we also attempt to rollback on {@link Error} which
         * is clearly an unexpected outcome as well. By contrast, a checked exception is
         * considered a business exception and therefore a regular expected outcome of the
         * transactional business method, i.e. a kind of alternative return value which
         * still allows for regular completion of resource operations.
         * <p>This is largely consistent with TransactionTemplate's default behavior,
         * except that TransactionTemplate also rolls back on undeclared checked exceptions
         * (a corner case). For declarative transactions, we expect checked exceptions to be
         * intentionally declared as business exceptions, leading to a commit by default.
         * @see org.springframework.transaction.support.TransactionTemplate#execute
         */
        @Override
        public boolean rollbackOn(Throwable ex) {
            return (ex instanceof RuntimeException || ex instanceof Error);
        }
    

    4 @Transactional 注解属性 propagation 设置错误

    这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。

    1. TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    2. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    3. TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

    5 @Transactional 注解属性 rollbackFor 设置错误

    rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。


    image.png

    6 数据库引擎不支持事务

    这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

    相关文章

      网友评论

        本文标题:spring——@Transactional为啥不生效

        本文链接:https://www.haomeiwen.com/subject/pvmrmhtx.html