Spring框架中事务控制的运行原理

Photo by Tomasz Filipek from Pexels: https://www.pexels.com/photo/nature-photography-of-flower-field-1646178/

Spring Transaction 基本介绍

我们在日常开发中经常使用Spring框架来实现事务管理。事务管理是指在执行一系列操作时,保证这些操作要么全部成功,要么全部失败,不会出现中间状态。事务管理可以保证数据的一致性和完整性,避免因为异常或错误导致的数据损坏。

Spring框架中的事务管理有两种实现方式:编程式事务管理声明式事务管理。编程式事务管理是指在代码中显式地控制事务的开始、提交和回滚,这种方式需要编写大量的重复代码,而且容易出错。声明式事务管理是指通过注解或配置文件来声明哪些方法需要进行事务控制,这种方式更简洁、灵活和优雅,也是Spring框架推荐的方式。

无论是哪种方式,Spring框架中的事务管理都是基于AOP(面向切面编程)的代理机制实现的。这让我们可以在不修改原有代码的情况下,为某些方法添加额外的功能或行为,比如日志、安全、缓存等。

声明式事务管理的基本使用

当我们使用声明式事务管理时,我们可以通过 @Transactional 注解来标注哪些方法需要进行事务控制,并且可以指定该注解的属性来设置事务的属性。例如:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 10)
    public void transferMoney(int fromId, int toId, double amount) {
        userDao.decreaseMoney(fromId, amount);
        userDao.increaseMoney(toId, amount);
    }
}

上面的代码表示transferMoney 方法需要进行事务控制,并且设置了传播行为为REQUIRED(表示如果当前没有事务,则创建一个新的事务,如果当前已经有事务,则加入该事务),隔离级别为READ_COMMITTED(表示只能读取已经提交的数据,可以避免脏读,但不能避免不可重复读和幻读),超时时间为10秒(表示如果该方法执行超过10秒,则自动回滚事务)。

当Spring框架扫描到@Transactional 注解时,它会根据注解的属性创建一个 TransactionDefinition 对象,并且会为被注解的类生成一个代理对象,该代理对象会实现和目标类相同的接口,并且会拦截目标类的所有方法。当调用被注解的方法时,代理对象会先从Spring容器中获取一个事务管理器,并且根据 TransactionDefinition 对象获取一个事务状态(TransactionStatus)对象,该对象包含了事务的信息和状态。然后代理对象会执行目标方法,如果目标方法正常返回,则代理对象会调用事务管理器的commit方法来提交事务;如果目标方法抛出异常,则代理对象会调用事务管理器的rollback方法来回滚事务。这样就实现了对目标方法的事务控制。

事务的传播行为

事务传播行为(Transaction Propagation)定义了一个事务方法与其他事务方法的关系,即当一个事务方法被调用时,如何处理现有的事务。在Spring框架中,可以通过设置@Transactional注解的propagation属性来指定事务传播行为。常用的事务传播行为包括:

  1. REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。这是最常用的传播行为。
  2. REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果存在当前事务,会将当前事务挂起。
  3. SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。支持事务但不强制要求。
  4. NOT_SUPPORTED:以非事务方式执行操作,如果存在当前事务,则将其挂起。
  5. MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  6. NEVER:以非事务方式执行操作,如果存在当前事务,则抛出异常。
  7. NESTED:如果当前存在事务,则在嵌套事务中执行。如果当前没有事务,则创建一个新事务。嵌套事务是当前事务的一部分,具有独立的保存点,可以进行回滚。

事务隔离级别

事务隔离级别(Transaction Isolation)定义了事务之间的隔离程度,即一个事务对其他事务的可见性。在Spring框架中,可以通过设置@Transactional注解的isolation属性来指定事务隔离级别。常用的事务隔离级别包括:

  1. DEFAULT(默认):使用底层数据库的默认隔离级别。
  2. READ_UNCOMMITTED:允许读取未提交的数据,可能会导致脏读、不可重复读和幻读。
  3. READ_COMMITTED:只允许读取已提交的数据,可以防止脏读,但仍可能出现不可重复读和幻读。
  4. REPEATABLE_READ:确保在同一事务中多次读取数据时结果始终一致,可以防止脏读和不可重复读,但仍可能出现幻读。
  5. SERIALIZABLE:事务串行执行,确保所有并发事务都看到相同的数据状态,可以防止脏读、不可重复读和幻读。

需要注意的是,隔离级别越高,事务的并发性能可能会降低,因为需要锁定更多的资源以保证数据的一致性。

其他事务属性

在Spring Framework中,事务传播行为和隔离级别是事务管理的两个重要概念。它们决定了事务方法与其他事务方法之间的关系以及并发事务之间的相互影响。除此之外,还有一些常用设置如下

  • 事务的超时时间:定义了一个事务允许执行的最长时间
  • 事务的只读标志:定义了一个事务是否只进行查询操作,而不进行修改操作
  • 事务的回滚规则:定义了哪些异常会导致事务回滚,哪些异常不会导致事务回滚

。。。。

声明式事务管理的原理分析

通过上面的分析,我们可以看到Spring框架中事务控制的运行原理是基于AOP代理机制实现的,它可以在不修改原有代码的情况下,为指定的方法添加事务控制的逻辑。这样可以大大简化我们的编程工作,也可以提高我们的代码质量和可维护性。

讲解完基本的使用方法,下面我们一起深入源码来探寻事务控制的原理。下面是一个简化的伪代码,用来说明Spring框架中声明式事务管理的运行原理:

// 生成的代理对象
public class UserServiceImplProxy implements UserService {

    // 目标对象
    private UserServiceImpl target;

    // 事务管理器
    private PlatformTransactionManager transactionManager;

    // 事务定义
    private TransactionDefinition transactionDefinition;

    public void transferMoney(int fromId, int toId, double amount) {
        // 获取事务状态
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
        try {
            // 调用目标方法
            target.transferMoney(fromId, toId, amount);
            // 提交事务
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(transactionStatus);
            // 抛出异常
            throw e;
        }
    }
}

Spring框架中事务管理的核心接口和类

在事务控制模块中一些重要的概念实现如下:

  • PlatformTransactionManager接口:定义了事务管理器的基本操作
    • DataSourceTransactionManager类:实现了基于JDBC的事务管理器
    • JpaTransactionManager类:实现了基于JPA的事务管理器
  • TransactionDefinition接口:定义了事务的属性和配置
  • TransactionStatus接口:定义了事务的状态和信息
  • TransactionInterceptor:AOP事务方法拦截器

Spring框架提供了一个接口 PlatformTransactionManager 来定义事务管理器的基本操作,比如获取事务状态、提交事务和回滚事务。不同的数据源需要实现不同的事务管理器,比如 JDBC 对应的是 DataSourceTransactionManager ,JPA对应的是 JpaTransactionManager 等。

从类图上来观察继承关系

Spring框架还提供了一个接口TransactionDefinition 来定义事务的属性,比如传播行为(propagation)隔离级别(isolation)超时时间只读标志等。

事务管理的核心逻辑:Transaction Interceptor

当我们理解了本小节开头的伪代码时,再来看TransactionInterceptor 内的实现会发现其实Spring的事务管理模块并不复杂,由于它实现了MethodInterceptor 所以直接看invoke 方法,可以看到调用了父类的invokeWithinTransaction 方法

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
    // Work out the target class: may be {@code null}.
    // The TransactionAttributeSource should be passed the target class
    // as well as the method, which may be from an interface.
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

    // Adapt to TransactionAspectSupport's invokeWithinTransaction...
    return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
        @Override
        @Nullable
        public Object proceedWithInvocation() throws Throwable {
            return invocation.proceed();
        }
        @Override
        public Object getTarget() {
            return invocation.getThis();
        }
        @Override
        public Object[] getArguments() {
            return invocation.getArguments();
        }
    });
}

进入 invokeWithinTransaction 方法内,可以观察到其整体逻辑是和我们之前分析的伪代码类似的:

@Nullable
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 TransactionManager tm = determineTransactionManager(txAttr);
	
    if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
        // 省略响应式事务管理的逻辑
    }
	
    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
        // 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);
        }

        if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
            // Set rollback-only in case of Vavr failure matching our rollback rules...
            TransactionStatus status = txInfo.getTransactionStatus();
            if (status != null && txAttr != null) {
                retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
            }
        }

        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
....
}

暂且不要深入到嵌套方法内的细节,上述代码的整体逻辑为:

  1. 获取事务管理器
  2. 视情况创建事务
  3. 执行业务方法
  4. 当捕捉到异常判断是否回滚处理
  5. 无异常时判断是否提交处理

看完了整体,再深入到细节。在AbstractPlatformTransactionManager.getTransaction 方法体内,我们可以看到事务隔离级别的实现方式,源码中用了多个条件判断来控制不同隔离级别时事务的行为:

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
        throws TransactionException {

    // Use defaults if no transaction definition given.
    TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

    Object transaction = doGetTransaction();
    boolean debugEnabled = logger.isDebugEnabled();

    if (isExistingTransaction(transaction)) {
        // Existing transaction found -> check propagation behavior to find out how to behave.
        return handleExistingTransaction(def, transaction, debugEnabled);
    }

    // Check definition settings for new transaction.
    if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
        throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
    }

    // No existing transaction found -> check propagation behavior to find out how to proceed.
    if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        SuspendedResourcesHolder suspendedResources = suspend(null);
        if (debugEnabled) {
            logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
        }
        try {
            return startTransaction(def, transaction, debugEnabled, suspendedResources);
        }
        catch (RuntimeException | Error ex) {
            resume(null, suspendedResources);
            throw ex;
        }
    }
    else {
        // Create "empty" transaction: no actual transaction, but potentially synchronization.
        if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
            logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                    "isolation level will effectively be ignored: " + def);
        }
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
    }
}

TransactionAspectSupport.completeTransactionAfterThrowing 方法内,可以观察当捕捉到异常判断是否回滚处理的逻辑:

/**
 * Handle a throwable, completing the transaction.
 * We may commit or roll back, depending on the configuration.
 * @param txInfo information about the current transaction
 * @param ex throwable encountered
 */
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                    "] after exception: " + ex);
        }
        if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
            try {
                txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            }
            catch (TransactionSystemException ex2) {
                logger.error("Application exception overridden by rollback exception", ex);
                ex2.initApplicationException(ex);
                throw ex2;
            }
            catch (RuntimeException | Error ex2) {
                logger.error("Application exception overridden by rollback exception", ex);
                throw ex2;
            }
        }
        else {
            // We don't roll back on this exception.
            // Will still roll back if TransactionStatus.isRollbackOnly() is true.
            try {
                txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
            }
            catch (TransactionSystemException ex2) {
                logger.error("Application exception overridden by commit exception", ex);
                ex2.initApplicationException(ex);
                throw ex2;
            }
            catch (RuntimeException | Error ex2) {
                logger.error("Application exception overridden by commit exception", ex);
                throw ex2;
            }
        }
    }
}

最后则是在 TransactionAspectSupport.commitTransactionAfterReturning 方法中成功执行事务的提交逻辑:

/**
 * Execute after successful completion of call, but not after an exception was handled.
 * Do nothing if we didn't create a transaction.
 * @param txInfo information about the current transaction
 */
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
        }
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

注意事项及最佳实践

下面是一些使用Spring框架进行事务控制时需要注意的问题:

  • @Transactional 注解默认只对 RuntimeExceptionError 进行回滚,对其他异常不进行回滚。如果需要对其他异常进行回滚,可以通过设置注解的rollbackFor 属性来指定。
  • @Transactional 注解只能应用在 public 方法上,对于private、protecteddefault方法无效。这是因为Spring框架默认使用JDK动态代理来生成代理对象,而JDK动态代理只能拦截public方法。如果需要拦截非public方法,可以通过设置Spring容器的aop:config标签的proxy-target-class属性为true,让Spring框架使用CGLIB动态代理来生成代理对象,在Spring Boot中则默认启用此属性。CGLIB动态代理是通过继承目标类来生成子类作为代理对象,因此可以拦截非public方法。
  • @Transactional 注解只能对外部调用有效,对于同一个类中的内部调用无效。这是因为同一个类中的内部调用并没有经过代理对象,而是直接调用了目标方法,因此无法触发AOP拦截和增强。如果需要对内部调用也进行事务控制,可以通过使用ApplicationContextAutowireCapableBeanFactory 来获取当前类的代理对象,然后通过代理对象来调用内部方法。
  • @Transactional 注解可以同时应用在接口、类和方法上,它们之间有继承和覆盖关系。
    • 注解的作用范围:如果在接口上使用注解,它将应用于所有实现该接口的类和方法。如果在类上使用注解,它将应用于该类中的所有方法。如果在方法上使用注解,它将仅应用于该方法。
    • 继承关系:当一个类继承另一个类时,子类继承了父类的@Transactional注解。但是,子类可以选择覆盖父类的注解,并使用自己的注解配置。这意味着子类可以通过覆盖注解来修改事务的传播行为、隔离级别等属性。

在日常的开发中,也最好遵循一些业界探索出的实践经验,以确保事务的正确管理和数据的一致性:

  1. 明确标记事务边界:使用@Transactional注解明确标记需要进行事务管理的方法。将注解放在方法上,以确保在方法执行期间启用事务管理。
  2. 设置适当的事务传播行为和隔离级别:根据业务需求,选择适当的事务传播行为和隔离级别。确保在不同的方法调用中正确管理事务的传播和隔离。
  3. 限制事务作用范围:将事务的作用范围限制在需要进行事务管理的最小代码块上,而不是整个方法或类。这样可以减少锁定资源的时间和范围,提高并发性能。
  4. 避免长时间事务:长时间事务会占用数据库资源并降低系统性能。尽量将事务的执行时间控制在合理的范围内,避免长时间的事务操作。
  5. 捕获并处理异常:在事务方法中捕获并处理异常是必要的。根据业务需求,选择适当的处理方式,如回滚事务、记录日志或抛出自定义异常。
  6. 尽量避免嵌套事务:嵌套事务会增加事务管理的复杂性,并可能导致死锁等并发问题。除非必要,尽量避免使用嵌套事务。
  7. 注意事务的回滚规则:通过设置rollbackFor属性,明确指定哪些异常会触发事务的回滚。确保异常的正确处理和事务的正确回滚。
  8. 定期进行性能调优和监控:对于事务频繁的应用程序,定期进行性能调优和监控是必要的。通过监控事务执行时间、数据库锁定情况等指标,优化事务管理和数据库设计。
  9. 编写单元测试验证事务管理:编写单元测试来验证事务管理的正确性。使用Spring的测试框架和事务支持来模拟数据库操作和事务回滚,确保事务管理的正确性。

总之,良好的事务管理实践可以确保数据的一致性和事务的正确执行。遵循上述最佳实践和注意事项,可以提高系统的稳定性和性能。

posted @ 2023-06-06 18:49  夜色微光  阅读(363)  评论(0编辑  收藏  举报