Spring

spring——@Transactional为啥不生效

2020-04-10  本文已影响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("异常失败");
}
   @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;

}
@Repository
public interface UserRepository  extends JpaRepository<User,Long> {
}
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");
        }
    }
}
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();
    }
    }

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

        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);
            }
/**
     * 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,那事务就从根本上失效了。

上一篇下一篇

猜你喜欢

热点阅读