原创

结合源码分析Spring声明式事务失效的一些场景

温馨提示:
本文最后更新于 2020年03月24日,已超过 1,543 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

Spring事务

官方事务文档:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction

编程式事务

声明式事务在我们平时用的可能更少一点,因为它对代码的侵入性太高了,而且没有注解@Transactional方便快捷。

private final TransactionTemplate transactionTemplate;
ransactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            // dosomething
        } catch (SomeBusinessException ex) {
           // 回滚事务
            status.setRollbackOnly();
        }
    }
});

声明式事务

spring中声明式事务是使用``@Transactional`注解开启的,原理是Aop,这也是我们用的最多的方式。

但是使用不当,可能导致事务失效等一系列问题。

@Transactional实现原理

// org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
                                         final InvocationCallback invocation) throws Throwable {

  // If the transaction attribute is null, the method is non-transactional.
  TransactionAttributeSource tas = getTransactionAttributeSource();
  final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
  final PlatformTransactionManager tm = determineTransactionManager(txAttr);
  final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

  // 【声明式事务处理】
  if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
    // 【满足开启事务的条件将创建一个新事务】
    // Standard transaction demarcation with getTransaction and commit/rollback calls.
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    Object retVal = null;
    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) {
      // 【回滚或提交事务】
      // 如果满足回滚事务的的条件的话,最终将会执行回滚事务的操作:txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
      // 如果不满足回滚事务的条件,最终将会执行提交事务的操作:txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
      // target invocation exception
      completeTransactionAfterThrowing(txInfo, ex);
      throw ex;
    }
    finally {
      cleanupTransactionInfo(txInfo);
    }
    // 【提交事务】。最终将会执行提交事务的操作:txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    commitTransactionAfterReturning(txInfo);
    return retVal;
  }

  // 【编程式事务处理】
  else {
    final ThrowableHolder throwableHolder = new ThrowableHolder();

    // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
    try {
      Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> {
        TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
        try {
          return invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
          if (txAttr.rollbackOn(ex)) {
            // A RuntimeException: will lead to a rollback.
            if (ex instanceof RuntimeException) {
              throw (RuntimeException) ex;
            }
            else {
              throw new ThrowableHolderException(ex);
            }
          }
          else {
            // A normal return value: will lead to a commit.
            throwableHolder.throwable = ex;
            return null;
          }
        }
        finally {
          cleanupTransactionInfo(txInfo);
        }
      });

      // Check result state: It might indicate a Throwable to rethrow.
      if (throwableHolder.throwable != null) {
        throw throwableHolder.throwable;
      }
      return result;
    }
    catch (ThrowableHolderException ex) {
      throw ex.getCause();
    }
    catch (TransactionSystemException ex2) {
      if (throwableHolder.throwable != null) {
        logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
        ex2.initApplicationException(throwableHolder.throwable);
      }
      throw ex2;
    }
    catch (Throwable ex2) {
      if (throwableHolder.throwable != null) {
        logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
      }
      throw ex2;
    }
  }
}

@Transactional默认回滚的异常

代码位置:org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn

@Override
public boolean rollbackOn(Throwable ex) {
  return (ex instanceof RuntimeException || ex instanceof Error);
}

可以看到,如果没有指定@Transactional的回滚异常,默认就只回滚RuntimeExceptionError

Spring声明式事务失效的一些场景

1.数据库存储引擎不支持事务

查看MySQL系统当前使用的存储引擎

mysql> show variables like '%storage_engine%';
+----------------------------------+--------+
| Variable_name                    | Value  |
+----------------------------------+--------+
| default_storage_engine           | InnoDB |
| default_tmp_storage_engine       | InnoDB |
| disabled_storage_engines         |        |
| internal_tmp_disk_storage_engine | InnoDB |
+----------------------------------+--------+
4 rows in set (0.03 sec)

查看当前MySQL支持的存储引擎

mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.03 sec)

在这里,可以很明显的看到我当前MySQL数据库中InnoDB引擎才支持事务。

PS:在MySQL5.5.5之后,InnoDB就是默认存储引擎了。

2.回滚的异常不符合

文章前面我有说到,在没有指定回滚的异常情况下,默认只回滚RuntimeExceptionError

比如:在此处doSomeThing中会抛出Exception异常,而此处@Transactional并没有指定回滚的异常,所以,此处事务将失效。

@Service
public class OrderServiceImpl implements OrderService {

  @Transactional
  public OrderDto create(OrderDto dto) {
     // 操作数据库...
     doSomeThing()
  }
}

正确的方法是手动指定回滚的异常:@Transactional(rollbackFor = Exception.class)

3.没有被Spring管理/不是Bean

比如,此处,把@Service注解去掉,Spring无法管理,事务将失效。

// @Service
public class OrderServiceImpl implements OrderService {

  @Transactional(rollbackFor = Exception.class)
  public OrderDto create(OrderDto dto) {
    // 操作数据库...
  }
}

4.异常被吃了

比如:此处的异常没有抛出来,事务也将失效。

@Service
public class OrderServiceImpl implements OrderService {

  @Transactional(rollbackFor = Exception.class)
  public OrderDto create(OrderDto dto) {
     try {
       // 操作数据库...
     } catch (Exception e) {
       log.error(e.getMessage(), e);
     }
  }
}

5.加锁处理不当

错误的示例:

@Transactional(rollbackFor = Exception.class)
public synchronized OrderDto create(OrderDto dto) {
  // 操作数据库...
}

这样子导致事务失效的原因是:此处synchronized关键字的范围比@Transactional(Aop)小,它没有涵盖到整个事务过程,恰恰相反的是@Transactional(Aop)把synchronized关键字的范围涵盖在内了。

参考解决办法:可以使用编程式事务。
示例(当然还有其它办法):

private static final Object SYNC_ORDER_OBJECT = new Object();

public OrderDto create(OrderDto dto) {
  synchronized (SYNC_ORDER_OBJECT) {
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    TransactionStatus status = transactionManager.getTransaction(def);

    try {
      // 操作数据库...

      // 提交事务
      transactionManager.commit(status);
      return orderDto;
    } catch (Exception e) {
      // 手动回滚事务
      transactionManager.rollback(status);
      log.error(e.toString(), e);
      throw new CustomException(e.getMessage());
    }
  }
}

6.多线程调用

在文章开头说“@Transactional实现原理”中的org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction,在声明式事务中,开头有这么一段代码:

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

createTransactionIfNecessary方法:

    protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
            @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

        // If no name specified, apply method identification as transaction name.
        if (txAttr != null && txAttr.getName() == null) {
            txAttr = new DelegatingTransactionAttribute(txAttr) {
                @Override
                public String getName() {
                    return joinpointIdentification;
                }
            };
        }

        TransactionStatus status = null;
        if (txAttr != null) {
            if (tm != null) {
                status = tm.getTransaction(txAttr);
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
                            "] because no transaction manager has been configured");
                }
            }
        }
        return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
    }

我们来看一下prepareTransactionInfo方法:

    protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
            @Nullable TransactionAttribute txAttr, String joinpointIdentification,
            @Nullable TransactionStatus status) {

        TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
        if (txAttr != null) {
            // We need a transaction for this method...
            if (logger.isTraceEnabled()) {
                logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
            }
            // The transaction manager will flag an error if an incompatible tx already exists.
            txInfo.newTransactionStatus(status);
        }
        else {
            // The TransactionInfo.hasTransaction() method will return false. We created it only
            // to preserve the integrity of the ThreadLocal stack maintained in this class.
            if (logger.isTraceEnabled()) {
                logger.trace("Don't need to create transaction for [" + joinpointIdentification +
                        "]: This method isn't transactional.");
            }
        }

        // We always bind the TransactionInfo to the thread, even if we didn't create
        // a new transaction here. This guarantees that the TransactionInfo stack
        // will be managed correctly even if no transaction was created by this aspect.
        txInfo.bindToThread();
        return txInfo;
    }

最后来看一下bindToThread方法:

主要作用就是将TransactionInfo绑定到当前线程中,也就是说一个线程就会持有一个TransactionInfo对象,所以,这也就不难理解为什么多线程调用会导致事务失效了。

private static final ThreadLocal<TransactionInfo> transactionInfoHolder = new NamedThreadLocal<>("Current aspect-driven transaction");

// ...

private void bindToThread() {
  // Expose current TransactionStatus, preserving any existing TransactionStatus
  // for restoration after this transaction is complete.
  this.oldTransactionInfo = transactionInfoHolder.get();
  transactionInfoHolder.set(this);
}

7.错误的事务传播特性

Spring中七种事务传播行为,也可以看代码org.springframework.transaction.annotation.Propagation

事务传播行为类型 说明
PROPAGATION_REQUIRED 支持当前事务,如果不存在则创建一个新事务。 类似于 EJB 的同名事务属性。
这是事务注释的默认设置。
PROPAGATION_SUPPORTS 支持当前事务,如果不存在则以非事务方式执行。 类似于 EJB 的同名事务属性。
注意:对于具有事务同步的事务管理器,PROPAGATION_SUPPORTS 与根本没有事务略有不同,因为它定义了同步将应用的事务范围。 因此,相同的资源(JDBC 连接、Hibernate 会话等)将在整个指定范围内共享。 请注意,这取决于事务管理器的实际同步配置。
PROPAGATION_MANDATORY 支持当前事务,如果不存在则抛出异常。类似于EJB的同名事务属性。
PROPAGATION_REQUIRES_NEW 创建一个新事务,如果存在,则暂停当前事务。 类似于同名的 EJB 事务属性。
注意:实际的事务挂起不会在所有事务管理器上开箱即用。 这尤其适用于org.springframework.transaction.jta.JtaTransactionManager ,它需要javax.transaction.TransactionManager对其可用(这在标准 Java EE 中是特定于服务器的)
PROPAGATION_NOT_SUPPORTED 以非事务方式执行,如果存在则暂停当前事务。 类似于 EJB 的同名事务属性。
注意:实际的事务挂起不会在所有事务管理器上开箱即用。 这尤其适用于org.springframework.transaction.jta.JtaTransactionManager ,它需要javax.transaction.TransactionManager对其可用(这在标准 Java EE 中是特定于服务器的)。
PROPAGATION_NEVER 以非事务方式执行,如果存在事务则抛出异常。类似于EJB的同名事务属性。
PROPAGATION_NESTED 如果当前事务存在,则在嵌套事务中执行,其他行为类似于 PROPAGATION_REQUIRED。 EJB 中没有类似的特性。
注意:嵌套事务的实际创建仅适用于特定的事务管理器。 开箱即用,这仅适用于处理 JDBC 3.0 驱动程序时的 JDBC DataSourceTransactionManager。 一些 JTA 提供者也可能支持嵌套事务。

@Transactional注解中可以用propagation指定事务的传播特性,默认是Propagation.REQUIRED,也就是TransactionDefinition.PROPAGATION_REQUIRED

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)

8.方法不是public

比如:我这里create方法使用private修饰的话,事务将失效。

@Service
public class OrderServiceImpl implements OrderService {

  @Transactional(rollbackFor = Exception.class)
  private OrderDto create(OrderDto dto) {
    // 操作数据库...
  }
}

失效原因看下面代码分析👇🏻

在代码org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute中,限定了allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers()),也就是说被调用的方法如果不是public,将返回null。

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

        // The method may be on an interface, but we need attributes from the target class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

        // First try is the method in the target class.
        TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
        if (txAttr != null) {
            return txAttr;
        }

        // Second try is the transaction attribute on the target class.
        txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
        if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
            return txAttr;
        }

        if (specificMethod != method) {
            // Fallback is to look at the original method.
            txAttr = findTransactionAttribute(method);
            if (txAttr != null) {
                return txAttr;
            }
            // Last fallback is the class of the original method.
            txAttr = findTransactionAttribute(method.getDeclaringClass());
            if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
                return txAttr;
            }
        }

        return null;
    }

9.方法用final修饰

spring事务基于AOP,底层可以使用jdk动态代理或者cglib代理,它们会帮我们自动生成代理类去实现事务相关功能,但是如果方法被final修饰后就不能被重写,所以,此时事务也将失效。

PS: 如果方法是static,也无法通过动态代理。

@Service
public class OrderServiceImpl implements OrderService {

  @Transactional(rollbackFor = Exception.class)
  public final OrderDto create(OrderDto dto) {
    // 操作数据库...
  }
}

10.同一个类中的事务问题

Spring默认的事务传播行为REQUIRED:如果不存在外层事务,就主动开启事务,否则使用外层事务。

同一个类中无事务方法调用有事务方法问题

错误示例:

@Override
public void doTheThing() {
  // ...
  actuallyDoTheThing();
}

@Override
@Transactional(rollbackFor = Exception.class)
public void actuallyDoTheThing() {
  // ...
}

doTheThing方法没有开启事务,但是在调用方actuallyDoTheThing方法开启了事务,所以,在actuallyDoTheThing方法中的操作如果符合回滚条件将会回滚,而doTheThing中除去actuallyDoTheThing方法之外的其它方法由于没有开启事务,所以不会回滚。

sonar对此操作都有警告:https://rules.sonarsource.com/java/RSPEC-2229

同一个类中事务传播行为虽然满足事务但传播行为不一致问题

错误示例1:doTheThing方法和actuallyDoTheThing方法两者事务互不影响。

@Override
@Transactional(rollbackFor = Exception.class)
public void doTheThing() {
  // ...
  actuallyDoTheThing();
}

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void actuallyDoTheThing() {
  // ...
}

错误示例2:doTheThing方法有事务,而actuallyDoTheThing方法没有事务。

@Override
@Transactional(rollbackFor = Exception.class)
public void doTheThing() {
  // ...
  actuallyDoTheThing();
}

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED)
public void actuallyDoTheThing() {
  // ...
}

错误示例3:与MANDATORY的事务传播行为相反,MANDATORY是当外层方法不存在事务抛出异常,而NEVER是当外层方法存在事务抛出异常。所以此处会抛出异常,同时也不会进行DB操作。

@Override
@Transactional(rollbackFor = Exception.class)
public void doTheThing() {
  // ...
  actuallyDoTheThing();
}

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NEVER)
public void actuallyDoTheThing() {
  // ...
}

错误示例4:

@Override
@Transactional(rollbackFor = Exception.class)
public void doTheThing() {
  // ...
  actuallyDoTheThing();
}

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public void actuallyDoTheThing() {
  // ...
}

正确做法(事务的传播特性):

@Override
@Transactional(rollbackFor = Exception.class)
public void doTheThing() {
  // ...
  actuallyDoTheThing();
}

@Override
@Transactional(rollbackFor = Exception.class)
public void actuallyDoTheThing() {
  // ...
}

参考文章:
https://z.itpub.net/article/detail/18A4D9564A61EC7AF8EAA66FCA251444
https://juejin.cn/post/6844903504792780814

本文目录