Spring系列之事务、@Transactional原理及失效场景
概要
Spring事务基于数据库事务,JDBC事务过程:
- 获取连接
Connection con = DriverManager.getConnection()
- 开启事务
con.setAutoCommit(true/false);
- 执行CRUD
- 提交事务/回滚事务
con.commit()
,con.rollback();
- 关闭连接
conn.close();
Spring事务主要分为两种:
- 编程式事务
- 声明式事务
编程式事务
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注解有哪些属性?
- propagation,事务的传播行为,包括7种枚举值;
- 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());
}
}
- timeout,事务超时时间,默认值为
-1
。如果超过该时间限制但事务还没有完成,则自动回滚事务 - readOnly,指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true
- rollbackFor,指定触发事务回滚的异常类型,可指定多个异常类型
- noRollbackFor,指定不触发事务回滚的异常类型,可指定多个异常类型
- 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声明式事务处理的实现原理