Spring系列之事务、@Transactional原理及失效场景

概要

Spring事务基于数据库事务,JDBC事务过程:

  1. 获取连接Connection con = DriverManager.getConnection()
  2. 开启事务con.setAutoCommit(true/false);
  3. 执行CRUD
  4. 提交事务/回滚事务con.commit(), con.rollback();
  5. 关闭连接conn.close();

Spring事务主要分为两种:

  1. 编程式事务
  2. 声明式事务

编程式事务

try {
    // something
    transactionManager.commit();
} catch (Exception e) {
    transactionManager.rollback();
    throw new RuntimeException("失败");
}

显而易见,代码侵入性比较强,代码冗余。

声明式事务

使用Spring声明式事务,可以不再写步骤2和4的代码。基于AOP,有两种实现方式:基于TX和AOP的xml配置文件方式、基于@Transactional注解的形式。配置文件开启注解驱动,在类和方法上通过注解@Transactional标识。Spring在启动时会去解析并生成相关的bean,为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中把相关的事务处理掉(开启正常提交事务,异常回滚事务)。真正的数据库层的事务提交和回滚是通过binlog或redo log实现。

Transactional

@Transactional可作用在接口、类、方法:

  • 类:表示该类所有的public方法都配置相同的事务属性信息
  • 方法:方法的事务优先级更高,会覆盖类的事务配置信息
  • 接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置Spring AOP使用CGLib动态代理,将会导致@Transactional注解失效

属性

@Transactional注解有哪些属性?

  1. propagation,事务的传播行为,包括7种枚举值;
  2. isolation,事务的隔离级别,枚举值有5个,Isolation.DEFAULT:默认值,即使用底层数据库的隔离级别,其他四种即为MySQL的四种隔离级别。源码在DataSourceUtils:
// Apply specific isolation level, if any.
Integer previousIsolationLevel = null;
if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
	int currentIsolation = con.getTransactionIsolation();
	if (currentIsolation != definition.getIsolationLevel()) {
		previousIsolationLevel = currentIsolation;
		con.setTransactionIsolation(definition.getIsolationLevel());
	}
}
  1. timeout,事务超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务
  2. readOnly,指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true
  3. rollbackFor,指定触发事务回滚的异常类型,可指定多个异常类型
  4. noRollbackFor,指定不触发事务回滚的异常类型,可指定多个异常类型
  5. transactionManager,事务管理器,用于配置有多数据源,即多事务管理器情况下具体指定某一个事务管理器

事务传播性

事务的传播性一般在事务嵌套时候使用,比如在事务A里面调用另外一个使用事务的方法,那么这俩个事务是各自作为独立的事务执行提交,还是内层的事务合并到外层的事务一块提交那,这就是事务传播性要确定的问题。

Spring事务的传播属性,即多个事务同时存在时,Spring应该如何处理这些事务的行为。亦多个事务方法相互调用时,事务如何在这些方法间传播。这些属性在TransactionDefinition接口中定义:

  • 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:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效

隔离级别

  • ISOLATION_DEFAULT:PlatfromTransactionManager默认隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC相对应;
  • ISOLATION_READ_UNCOMMITTED:最低,它充许别外一个事务可以看到这个事务未提交的数据。会产生脏读,不可重复读和幻读;
  • ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。可避免脏读,但可能会出现不可重复读和幻读;
  • ISOLATION_REPEATABLE_READ:可防止脏读,不可重复读。但可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证避免下面的情况产生(不可重复读);
  • ISOLATION_SERIALIZABLE:花费最高代价但最可靠。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免幻读

readOnly

指定事务是否为只读事务,默认值为false。设置为true,需要底层数据库支持。
对MySQL来说,有两种提交模式:

  • SET AUTOCOMMIT=0:禁止自动提交
  • SET AUTOCOMMIT=1:开启自动提交

若开启事务,AUTOCOMMIT要为false,Spring源码处理:

con.setAutoCommit(false);
// 只读事务
if (isEnforceReadOnly() && definition.isReadOnly()) {
	try (Statement stmt = con.createStatement()) {
        stmt.executeUpdate("SET TRANSACTION READ ONLY");
    }
}

不加@Transaction注解,默认是不开启事务。单条查询语句也没有必要开启事务,数据库默认的配置就能满足需求。但一次执行多条查询语句,如统计查询,报表查询;此时多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,就会造成数据的前后不一。需要开启读事务。

不生效

不生效的几种场景:

1. 底层数据库引擎不支持事务

若数据库引擎不支持事务,则Spring自然无法支持事务

2. propagation设置错误

当Spring开启事务并设置传播机制,覆盖MySQL已有的事务隔离级别。如果MySQL不支持该隔离级别,Spring的事务就也不会生效。和第一点比较类似,但是不尽相同。三种配置将会导致事务失效:

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

3. 非public方法

在非public方法上标记@Transactional,不报错(可以通过编译,但是IDEA有警告),没有事务功能。因为Transactional事务基于AOP,动态代理只能针对public方法进行代理;而不管使用JDK还是CGlib动态代理。AbstractFallbackTransactionAttributeSource类源码如下:

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

另外方法的修饰符不能有final,static
在这里插入图片描述

4. 异常被catch

在整个事务的方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。

@Transactional
public void methodA() {
	try {
		// do some transaction of A
		// call B to do another transaction
		methodB();
	} catch (Exception ex) {
		return;
	}
}

会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

因为当ServiceB中抛出一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现前后不一致,抛出UnexpectedRollbackException异常。

spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。

在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。

5. rollbackFor属性设置错误

指定异常触发回滚,但是设置错误导致一些异常不能触发回滚。rollbackFor可以指定能够触发事务回滚的异常类型。Spring默认抛出未检查unchecked异常(继承自RuntimeException的异常)或Error才回滚事务;其他异常不会触发回滚事务。源码:

private int getDepth(Class<?> exceptionClass, int depth) {
	if (exceptionClass.getName().contains(this.exceptionName)) {
		// Found it!
		return depth;
	}
	// If we've gone as far as we can go and haven't found it...
	if (exceptionClass == Throwable.class) {
		return -1;
	}
	return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

6. noRollbackFor属性设置错误

和rollbackFor类似。

7. 方法中调用同类的方法

最复杂的情况。一个类中的A方法(无注解@Transactional)在内部调用B方法(有注解@Transactional,不论方法B是用public还是private修饰),这样会导致B方法中的事务失效。

简单来说,由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

具体来说,Spring在扫描Bean时会自动为标注@Transactional注解的类生成一个代理类,当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。即并不通过创建代理对象进行调用,所以并不会进入TransactionInterceptor的invoke方法,不会开启事务。

8.未加类注解标记为Spring Bean

没啥好说的。。一般情况下会出现应用启动失败。。

9.应用未开启事务

没啥好说的,写这么多,主要是面试官一个比一个恶心。这种情况在遗留Spring老项目中可能会出现。

10.多线程调用

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {
    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

事务方法add中,调用事务方法doOtherThing,但事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛异常,add方法也回滚是不可能的。

Spring的事务是通过数据库连接来实现的。当前线程中保存一个map,key是数据源,value是数据库连接:
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

建议

正确的使用Transactional注解需要做到如下四点:

  • 不要在类上标注Transactional注解,要在需要的方法上标注。即使类的每个方法都需要事务也不要在类上标注,因为有可能你或别人新添加的方法根本不需要事务;
  • 标注Transactional注解的方法体中不要涉及耗时很久的操作,如IO操作、网络通信等;
  • 根据业务需要设置合适的事务参数,如是否需要新事务、超时时间等;
  • 控制事务影响的范围,代码中减少事务影响的代码。

原理

Spring容器在初始化每个单例bean的时候,会遍历容器中的所有BeanPostProcessor实现类,并执行其postProcessAfterInitialization方法,在执行AbstractAutoProxyCreator类的postProcessAfterInitialization方法时会遍历容器中所有的切面,查找与当前实例化bean匹配的切面,这里会获取事务属性切面,查找@Transactional注解及其属性值,然后根据得到的切面创建一个代理对象,默认是使用JDK动态代理创建代理,如果目标类是接口,则使用JDK动态代理,否则使用Cglib。在创建代理的过程中会获取当前目标方法对应的拦截器,此时会得到TransactionInterceptor实例,在它的invoke方法中实现事务的开启和回滚,在需要进行事务操作的时候,Spring会在调用目标类的目标方法之前进行开启事务、调用异常回滚事务、调用完成会提交事务。是否需要开启新事务,是根据@Transactional注解上配置的参数值来判断的。如果需要开启新事务,获取Connection连接,然后将连接的自动提交事务改为false,改为手动提交。当对目标类的目标方法进行调用的时候,若发生异常将会进入completeTransactionAfterThrowing方法。

如果在类A上标注Transactional注解,Spring容器会在启动的时候,为类A创建一个代理类B,类A的所有public方法都会在代理类B中有一个对应的代理方法,调用类A的某个public方法会进入对应的代理方法中进行处理;如果只在类A的b方法(使用public修饰)上标注Transactional注解,Spring容器会在启动的时候,为类A创建一个代理类B,但只会为类A的b方法创建一个代理方法,调用类A的b方法会进入对应的代理方法中进行处理,调用类A的其它public方法,则还是进入类A的方法中处理。在进入代理类的某个方法之前,会先执行TransactionInterceptor类中的invoke方法,完成整个事务处理的逻辑,如是否开启新事务、在目标方法执行期间监测是否需要回滚事务、目标方法执行完成后提交事务等。

PlatformTransactionManager,基于AOP的类TransactionAspectSupport:
核心属性:

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

核心方法invokeWithinTransaction():


getTransaction():

@Override
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) {
		thrownew 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) {
		thrownew IllegalTransactionStateException(
				"No existing transaction found for transaction marked with propagation 'mandatory'");
	}
	elseif (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
		SuspendedResourcesHolder suspendedResources = suspend(null);
		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);
	}
}

事务拦截器
PlatformTransactionManager是Spring 中的事务管理接口,真正定义事务如何回滚和提交。
TransactionInterceptor,负责拦截方法执行,判断是否需要提交或者回滚事务。
在这里插入图片描述

参考

Spring事务处理时自我调用的解决方案及一些实现方式的风险
6种@Transactional注解失效场景
聊聊Spring事务失效的12种场景
Spring声明式事务处理的实现原理

posted @ 2022-11-02 14:35  johnny233  阅读(328)  评论(0编辑  收藏  举报  来源