Spring事务基于数据库事务,JDBC事务过程:
Connection con = DriverManager.getConnection()
con.setAutoCommit(true/false);
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可作用在接口、类、方法:
@Transactional注解有哪些属性?
// 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
。如果超过该时间限制但事务还没有完成,则自动回滚事务
事务传播性
事务的传播性一般在事务嵌套时候使用,比如在事务A里面调用另外一个使用事务的方法,那么这俩个事务是各自作为独立的事务执行提交,还是内层的事务合并到外层的事务一块提交那,这就是事务传播性要确定的问题。
Spring事务的传播属性,即多个事务同时存在时,Spring应该如何处理这些事务的行为。亦多个事务方法相互调用时,事务如何在这些方法间传播。这些属性在TransactionDefinition接口中定义:
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的事务就也不会生效。和第一点比较类似,但是不尽相同。三种配置将会导致事务失效:
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注解需要做到如下四点:
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():
@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声明式事务处理的实现原理