Bootstrap

Spring中的事务详解

Spring事务

事务是一组操作,被视为一个不可分割的工作单元,要么全部完成,要么全部失败回滚,来确保数据的一致性和完整性。Spring事务管理允许我们在应用程序中声明式地或编程式地管理事务,它提供了一个事务管理抽象层,使得事务的使用和配置更加简单和灵活。Spring事务管理不直接管理数据库事务,而是通过委托给底层的数据库事务管理器,如JDBCHibernate的事务管理器,来实现对数据库事务的控制。

Spring事务的使用

Spring事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务,声明式事务基于AOP,即使用@Transactional注解,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染,因此在实际使用中声明式事务用的比较多。

通过简单的配置或注解,Spring允许开发者将事务管理从业务代码中分离出来,提高了代码的可读性和可维护性。使用@Transactional注解,简单配置即可管理事务。例如在Service层的方法上标注@Transactional注解,Spring将自动处理事务边界。

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        // 业务逻辑代码
    }
}

@Configuration
@EnableTransactionManagement
public class AppConfig {
   // 其他配置
}

使用TransactionTemplatePlatformTransactionManager手动管理事务,虽然不常用,但在某些情况下,可以用于需要更细粒度控制的场景。

@Service
public class UserService {

    private final TransactionTemplate transactionTemplate;

    @Autowired
    public UserService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void createUser(User user) {
        transactionTemplate.execute(status -> {
            // 业务逻辑代码
            return null;
        });
    }
}
@Service
public class UserService {

    private final PlatformTransactionManager transactionManager;

    @Autowired
    public UserService(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void createUser(User user) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            // 业务逻辑代码
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

值得注意的是,Spring利用AOP和事务拦截器来拦截被@Transactional注解的方法,在方法调用前开启事务,在方法调用后根据方法执行结果决定提交或回滚事务。所以@Transactional注解会有失效情况:

  1. 如果某个方法是非public的,那么@Transactional就会失效。因为事务的底层是利用cglib代理实现,cglib是基于父子类来实现的,子类是不能重载父类的private方法,所以无法很好利用代理,这种情况下会导致@Transactional失效;
  2. 使用的数据库引擎不支持事务。因为Spring的事务调用的也是数据库事务的API,如果数据库都不支持事务,那么@Transactional注解也就失效了;
  3. 添加了@Transactional注解的方法不能在同一个类中调用,否则会使事务失效。这是因为Spring AOP通过代理来管理事务,自调用不会经过代理;
  4. @Transactional注解属性propagation设置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚:
    • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  5. @Transactional注解属性rollbackFor设置错误,rollbackFor可以指定能够触发事务回滚的异常类型。默认情况下,Spring仅在抛出未检查异常(继承自RuntimeException)时回滚事务。对于受检异常(继承自 Exception),事务不会回滚,除非明确配置了rollbackFor属性;
  6. 异常被捕获了,导致@Transactional失效。当事务方法中抛出一个异常后,应该是需要表示当前事务需要rollback,如果在事务方法中手动捕获了该异常,那么事务方法则会认为当前事务应该正常提交,此时就会出现事务方法中明明有报错信息表示当前事务需要回滚,但是事务方法认为是正常,出现了前后不一致,也是因为这样就会抛出UnexpectedRollbackException异常;

Spring事务的隔离级别

事务隔离级别,即数据库中事务隔离级别,指的是一个事务对数据的修改与另一个并行的事务的隔离程度。当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:

问题描述
脏读一个事务读到另一个事务未提交的更新数据。比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
幻读是指当事务不是独立执行时发生的一种现象。如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。
不可重复读在一个事务里面的操作中发现了未被操作的数据。 比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何DDL语句,但先后得到的结果不一致,这就是不可重复读。

Spring事务管理框架支持标准的数据库事务隔离级别,这些隔离级别与底层数据库系统所支持的隔离级别一致。

隔离级别描述
DEFAULT使用数据库本身使用的隔离级别。ORACLE(读已提交) MySQL(可重复读)
READ_UNCOMITTED读未提交(脏读)最低的隔离级别,一切皆有可能。
READ_COMMITED读已提交,ORACLE默认隔离级别,有幻读以及不可重复读风险。
REPEATABLE_READ可重复读,解决不可重复读的隔离级别,但还是有幻读风险。
SERLALIZABLE串行化,所有事务请求串行执行,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。

不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁来保证事务的正确性,那么效率就要降低。因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。

Spring中通过@Transactional(isolation = Isolation.REPEATABLE_READ)可以指定事务的隔离级别。Spring建议的是使用DEFAULT,即数据库本身的隔离级别,配置好数据库本身的隔离级别,无论在哪个框架中读写数据都不用操心了。

Spring事务的传播

事务传播行为指,当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行,是应该加入现有事务,还是应该启动一个新事务。

Spring定义了七种传播行为:

  • @Transactional(propagation=Propagation.REQUIRED):如果当前有事务,那么加入事务, 没有的话新建一个,用于确保所有操作都在同一个事务中进行,这也是Spring提供的默认传播行为;
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {
    // 1. 如果调用方 methodA 有事务,methodB 将加入该事务。
    // 2. 如果调用方没有事务,methodB 将创建一个新的事务。
    }
    
  • @Transactional(propagation=Propagation.REQUIRES_NEW):不管是否存在事务,都创建一个新的事务,如果当前已经存在一个事务,则将当前事务挂起,新的执行完毕,继续执行老的事务;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 1. 如果调用方 methodA 有事务,methodB 将挂起该事务并创建一个新的事务。
        // 2. 如果调用方没有事务,methodB 将创建一个新的事务。
    }
    
  • @Transactional(propagation=Propagation.MANDATORY):必须在一个已有的事务中执行,否则抛出异常。如果当前方法已经存在一个事务,那么加入这个事务,如果当前没有事务,则抛出异常;适用于必须在现有事务中执行的操作;
    @Transactional(propagation = Propagation.MANDATORY)
    public void methodB() {
        // 1. 如果调用方 methodA 有事务,methodB 将加入该事务。
        // 2. 如果调用方没有事务,methodB 将抛出异常。
    }
    
  • @Transactional(propagation=Propagation.NEVER):必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反);适用于某些操作强制不允许在事务内执行;
    @Transactional(propagation = Propagation.NEVER)
    public void methodB() {
        // 1. 如果调用方 methodA 有事务,methodB 将抛出异常。
        // 2. 如果调用方没有事务,methodB 将以非事务方式执行。
    }
    
  • @Transactional(propagation=Propagation.SUPPORTS):如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行;适用于某些不强制需要事务控制的情况;
    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodB() {
        // 1. 如果调用方 methodA 有事务,methodB 将加入该事务。
        // 2. 如果调用方没有事务,methodB 将以非事务方式执行。
    }
    
  • @Transactional(propagation=Propagation.NOT_SUPPORTED):Spring不为这个方法开启事务,即总是以非事务方式执行。如果当前已经存在一个事务,则将当前事务挂起;适用于不希望在事务中执行的操作,比如复杂的查询操作,不需要事务控制;
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void methodB() {
        // 1. 如果调用方 methodA 有事务,methodB 将挂起该事务并以非事务方式执行。
        // 2. 如果调用方没有事务,methodB 将以非事务方式执行。
    }
    
  • @Transactional(propagation=Propagation.NESTED):如果当前方法已经存在一个事务,则创建一个嵌套事务。如果当前没有事务,则创建一个新事务;适用于需要嵌套事务支持的场景,例如部分操作需要回滚,但不影响主事务;
    @Transactional(propagation = Propagation.NESTED)
    public void methodB() {
        // 1. 如果调用方 methodA 有事务,methodB 将创建一个嵌套事务。
        // 2. 如果调用方没有事务,methodB 将创建一个新的事务。
    }
    

在大多数情况下,如果想要确保所有操作都在同一个事务中进行,REQUIRED这个默认传播行为已经足够。但是随着事务越来越大,执行时间也会变长,就需要将这个大事务拆分成多个事务,如果确保这个事务能够拆分成多个事务,就需要指定Spring的事务传播行为。比如,在用户注册时候,需要记录注册日志,这时候可以将记录日志的操作单独划分为一个事务,而注册是另一个单独的事务,可以将保存日志的方法指定Propagation.REQUIRES_NEW从而实现。

Spring事务工作原理

在Spring框架中,事务管理的实现是通过集成数据库事务API来实现的。具体来说,Spring事务管理的核心在于使用各种PlatformTransactionManager接口的实现类,这些实现类会调用底层数据库事务API来管理事务。

public interface PlatformTransactionManager extends TransactionManager {

    /**
     * 打开事务
     */
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;

	/**
	 * 提交事务
	 */
	void commit(TransactionStatus status) throws TransactionException;

	/**
	 * 回滚事务
	 */
	void rollback(TransactionStatus status) throws TransactionException;
}

@Transactional注解为例,@Transactional主要是利用Spring AOP实现的。 @EnableTransactionManagement是开启注解式事务,这个注解就是探究Spring事务的入口。它通过@Import引入了另一个配置TransactionManagentConfigurationSelector

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
    AdviceMode mode() default AdviceMode.PROXY;
}

TransactionManagementConfigurationSelector的作用是根据EnableTransactionManagementmode.mode的属性值,选择AbstractTransactionManagementConfiguration的哪个实现,默认为PROXY模式。

protected String[] selectImports(AdviceMode adviceMode) {
    switch (adviceMode) {
        case PROXY:
            return new String[] {AutoProxyRegistrar.class.getName(),
                    ProxyTransactionManagementConfiguration.class.getName()};
        case ASPECTJ:
            return new String[] {determineTransactionAspectClass()};
        default:
            return null;
    }
}

selectImports方法对Proxy模式而言,注入的有两个Bean,一个负责注册,一份负责执行:

  • AutoProxyRegistrar:`负责在Spring容器中注册和启用自动代理功能。
  • ProxyTransactionManagementConfiguration:负责在使用代理模式时,事务管理器能够正确地与应用程序的业务逻辑集成,并通过AOP拦截器织入事务管理逻辑。

ProxyTransactionManagementConfiguration类中,最关键的是有一个TransactionInterceptor类型的Bean,这个Bean在Spring中负责管理和执行事务的核心逻辑。

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor(
        TransactionAttributeSource transactionAttributeSource) {
    TransactionInterceptor interceptor = new TransactionInterceptor();
    interceptor.setTransactionAttributeSource(transactionAttributeSource);
    if (this.txManager != null) {
        interceptor.setTransactionManager(this.txManager);
    }
    return interceptor;
}

其中TransactionInterceptor类中invoke方法就是实现@Transactional注解代理的关键。

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, invocation::proceed);
}
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {


    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    final TransactionManager tm = determineTransactionManager(txAttr);

    //省略部分代码

    //获取事物管理器
    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 打开事务(内部就是getTransactionStatus的过程)
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // 执行业务逻辑 invocation.proceedWithInvocation();
        } catch (Throwable ex) {
            // 异常回滚
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        } finally {
            cleanupTransactionInfo(txInfo);
        }

        //省略部分代码

        //提交事物
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
}

invokeWithinTransaction方法是TransactionInterceptor类中的核心方法,它负责在执行目标方法时管理事务的生命周期。首先该方法通过getTransactionAttributeSource()获取事务属性源,进而确定当前方法是否需要事务支持。接着根据获取的事务属性,选择合适的事务管理器,并生成一个方法标识以记录当前事务的上下文信息。在方法执行前,会检查是否需要开启新的事务,并在事务环境中执行目标方法的逻辑。如果方法执行过程中出现异常,事务管理器会回滚事务以保证数据一致性,否则在方法执行成功后,事务管理器将提交事务。最后无论方法执行结果如何,都会清理事务相关的信息,释放资源并恢复状态,来保证事务管理的完整性和有效性。

简而言之,当一个方法使用了@Transactional注解,在程序运行时,JVM为该Bean创建一个代理对象,并且在调用目标方法的时候进行使用TransactionInterceptor拦截,代理对象负责在调用目标方法之前开启事务,然后执行方法的逻辑。方法执行成功,则提交事务,如果执行方法中出现异常,则回滚事务。同时Spring利用ThreadLocal会将事务资源(如数据库连接)与当前线程绑定,以确保在同一事务中共享资源,这些资源在事务提交或回滚时会被清理。

;