一、事务不生效
1、访问权限的问题
在Spring框架中,AbstractFallbackTransactionAttributeSource
是用于确定一个给定的方法是否应该被事务管理的一个抽象类。它的computeTransactionAttribute
方法用于计算并返回一个方法的TransactionAttribute
。computeTransactionAttribute方法中有个判断如果目标不是public,则TransactionAttribute会返回null,即不支持事务。
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
当然,这种行为是可以根据需要进行调整的。如果您确实需要为非公共方法提供事务支持,可以通过自定义TransactionAttributeSource
实现来覆盖这种默认行为。
2、方法被final修饰
Java中的final
关键字用来声明一个不可变的成员变量或方法。当一个方法被声明为final
时,意味着这个方法不能被子类重写。然而,final
方法与Spring的事务管理之间的关系并不是直接相关的,因为事务管理是在运行时通过代理机制实现的,而不是通过方法的可重写性来控制的。
在Spring框架中,事务管理通常是通过AOP(面向切面编程)实现的。对于一个带有事务注解(如@Transactional
)的方法,Spring AOP会在运行时动态创建一个代理对象来调用该方法,并在这个过程中插入必要的事务管理逻辑。具体来说,Spring使用了两种主要的代理类型:
- JDK动态代理:适用于实现了接口的类。
- CGLIB代理:适用于没有实现任何接口的类,或者方法级的拦截。
对于带有@Transactional
注解的方法,Spring会尝试通过这些代理来执行该方法,并在其前后添加事务管理代码。但是,当方法被声明为final
时,情况会有所不同:
- JDK动态代理:
final
方法并不会影响JDK动态代理的行为,因为JDK动态代理并不修改目标类本身,而是创建一个新的代理类来实现相同接口。所以,在使用JDK动态代理的情况下,即使方法被声明为final
,事务管理仍然能够正常工作。 - CGLIB代理:CGLIB代理通过字节码增强的方式生成子类,然后重写目标类的方法以实现方法级别的拦截。这意味着,如果目标类的方法被声明为
final
,CGLIB就无法重写这个方法,因此也就无法为其添加事务管理的逻辑。在这种情况下,事务管理将会失效。
总结来说,如果类使用了CGLIB代理,并且该类中的方法被声明为final
,那么这些方法上的事务管理将不会生效。为了避免这种情况,可以选择使用JDK动态代理,但这要求类必须实现至少一个接口。另一种解决方法是移除final
关键字,这样无论使用哪种代理方式,事务管理都可以正常工作。
如果需要确保final
方法也能支持事务管理,并且类没有实现任何接口,那么可能需要考虑其他替代方案,比如更改类的设计使其不再使用final
方法,或者使用基于XML的配置来显式指定代理类型。
3、内部调用
在Spring框架中,内部方法调用(即一个类中的方法调用该类内的另一个方法)通常不会触发事务边界。这是因为Spring的事务管理是通过AOP(面向切面编程)代理实现的,而代理只能处理对类的外部调用,而不是类内部的方法调用。
当一个类中的方法被标记为@Transactional
时,Spring会为这个类创建一个代理。当外部代码调用该类的一个方法时,实际上是调用了代理对象上的方法,这样就可以在调用前后添加事务管理逻辑。但是,如果一个方法内部调用另一个方法,这种调用发生在同一个实例内部,因此不会经过代理层,也就不会触发事务管理。
例如,假设有一个类Service
,其中包含两个方法publicMethod
和privateMethod
,并且这两个方法都被标记为@Transactional
:
@Service
public class Service {
@Transactional
public void publicMethod() {
privateMethod();
}
@Transactional
private void privateMethod() {
// ...
}
}
在这种情况下,只有publicMethod
的外部调用会被代理并受到事务管理。privateMethod
的调用由于发生在类内部,不会触发事务边界。这意味着privateMethod
实际上不会按照预期那样被事务管理。
为了确保所有方法都受到事务管理,可以采取以下几种策略之一:
- 使用
@Transactional
注解类级别: 如果所有方法都需要事务管理,可以将@Transactional
注解放在类级别上,这样所有公开的方法都会自动被事务管理。 - 自我调用模式: 使用代理对象来自我调用方法,以确保方法调用会经过代理,从而触发事务管理。例如,您可以将方法改为如下形式:
@Service
public class Service {
@Autowired
private Service self;
@Transactional
public void publicMethod() {
self.privateMethod();
}
@Transactional
private void privateMethod() {
// ...
}
}
- 使用
@Transactional(propagation = Propagation.REQUIRES_NEW)
:
如果希望在内部方法调用时也开启新的事务,可以使用REQUIRES_NEW
传播行为。这将确保每个方法都在自己的事务中运行,即使是从另一个事务方法内部调用也是如此。但请注意,这种方式可能会导致性能问题,因为每个方法都会开始一个新事务。
@Service
public class Service {
@Transactional
public void publicMethod() {
privateMethod();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void privateMethod() {
// ...
}
}
4、未被spring管理
即使用spring事务的前提是:对象要被spring管理,需要创建bean实例(低级错误一般不会发生)
5、多线程调用
Spring的事务管理是基于线程本地变量(ThreadLocal)来实现的。具体来说,Spring使用ThreadLocal
来存储当前线程的事务上下文信息。当一个线程开始一个新的事务时,该事务的信息会被绑定到当前线程的ThreadLocal
变量中。当方法完成或异常发生时,事务会被提交或回滚,然后从ThreadLocal
中清除。
在多线程环境下,有几个关键点需要注意:
- 独立线程的事务隔离:
- 每个线程都有其自己的
ThreadLocal
副本,这意味着不同线程之间的事务上下文是相互隔离的。这意味着如果一个线程启动了一个事务,而另一个线程试图访问同一资源,它将看不到前一个线程中的事务状态,除非显式地通过某种机制共享事务上下文。
- 每个线程都有其自己的
- 共享资源的事务管理:
- 当多个线程试图访问由Spring管理的同一资源时,如果没有适当的同步机制,可能会出现并发问题。例如,如果一个线程正在执行一个事务中的操作,而另一个线程同时尝试修改相同的资源,那么第二个线程的操作可能会干扰第一个线程的事务。
- 显式事务管理:
- 对于需要跨线程共享事务上下文的情况,通常需要显式地管理事务,例如通过使用
TransactionTemplate
或PlatformTransactionManager
API手动管理事务。这样可以在需要的时候显式地开始、提交或回滚事务。
- 对于需要跨线程共享事务上下文的情况,通常需要显式地管理事务,例如通过使用
- 线程池中的事务管理:
- 如果您使用的是线程池,那么线程池中的线程会复用。这意味着如果一个线程在一个事务中结束并被归还到线程池中,然后被重新分配到一个新的任务,它可能会携带旧的事务上下文。为了避免这种情况,可以使用
TransactionSynchronizationManager
来清理线程本地的事务状态。
- 如果您使用的是线程池,那么线程池中的线程会复用。这意味着如果一个线程在一个事务中结束并被归还到线程池中,然后被重新分配到一个新的任务,它可能会携带旧的事务上下文。为了避免这种情况,可以使用
- 使用
@Transactional
注解:- 如果使用
@Transactional
注解来声明事务边界,那么只要确保线程调用符合Spring AOP的代理机制,事务管理通常会按预期工作。但是,如果多个线程并发地调用同一个带有@Transactional
注解的方法,那么每个线程都会有自己的事务实例。
- 如果使用
- 事务传播行为:
- 对于在多线程环境中使用
@Transactional
的方法,了解事务传播行为非常重要。例如,如果您使用PROPAGATION_REQUIRES_NEW
,那么即使在一个线程内,内部方法调用也会创建一个新的事务实例。
- 对于在多线程环境中使用
总结
- 多线程环境下的事务管理需要特别注意,特别是当涉及到共享资源时。
- 如果希望在多线程环境中保持事务的一致性,通常需要显式地管理事务,或者确保事务传播行为符合需求。
- 使用
@Transactional
注解时,每个线程会有自己的事务实例,除非显式地配置事务传播行为来改变这一点。
要确保多线程环境中事务的正确管理,可能需要考虑使用更高级别的事务管理API,例如TransactionTemplate
或PlatformTransactionManager
,并在必要时使用同步机制来确保事务的正确性。
6、表不支持事务
众所周知,在mysql5前,默认的数据库引擎是myisam
它的好处:索引文件和数据文件是分开储存的,对于查多写少的表达操作,性能比innodb更好
有些老项目中,可能还在用它。
在创建表的时候,只需要把ENGINE参数设置为MyISAM即可
myisam好用就是有一个致命的缺点:不支持事务
如果单表操作还好,但是如需跨表操作,由于不支持事务数据极有可能出现数据不完整的情况。
7、未开启事务
在spring boot(现在spring boot是默认开启的)未出来之前,spring开启事务是需要配置的,如果未配置的话就默认不开启。
以下是spring的xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 定义事务规则 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<!-- 关联事务规则与切点 -->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.example.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
二、事务不回滚
1、错误的传播特性
Spring提供了七种不同的传播行为,每种都有不同的用途。以下是这些传播行为的概述:
PROPAGATION_REQUIRED
:- 这是默认的传播行为。
- 如果当前存在事务,则加入该事务。
- 如果当前不存在事务,则创建一个新的事务。
PROPAGATION_SUPPORTS
:- 如果当前存在事务,则加入该事务。
- 如果当前不存在事务,则以非事务方式执行。
PROPAGATION_MANDATORY
:- 如果当前存在事务,则加入该事务。
- 如果当前不存在事务,则抛出
TransactionRequiredException
。
PROPAGATION_REQUIRES_NEW
:- 创建一个新的事务。
- 如果当前存在事务,则挂起该事务。
- 新事务完成后,恢复之前被挂起的事务(如果有的话)。
PROPAGATION_NOT_SUPPORTED
:- 以非事务方式执行。
- 如果当前存在事务,则挂起该事务。
PROPAGATION_NEVER
:- 以非事务方式执行。
- 如果当前存在事务,则抛出
IllegalTransactionStateException
。
PROPAGATION_NESTED
:- 如果当前存在事务,则执行一个嵌套事务。
- 如果当前不存在事务,则表现得像
PROPAGATION_REQUIRED
目前只有三种传播特性才会创建新事务:
PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED
2、自己吞了异常
事务不回滚,最常见的问题是:开发者在代码中手动try…catch了异常。
因为开发者自己捕获了异常,又没有手动抛出
@Service
public class ExampleService {
@Transactional
public void updateData() {
try {
someMethodThatMightThrowException();
} catch (Exception e) {
log.error("An error occurred", e);
// 这里没有再次抛出异常
}
}
private void someMethodThatMightThrowException() {
// 这里可能会抛出异常
throw new RuntimeException("An error occurred");
}
}
解决方案
为了避免这种情况,可以采取以下几种措施:
再次抛出异常:
- 在
catch
块中再次抛出异常,这样事务管理器会捕捉到异常并回滚事务。
@Transactional
public void updateData() {
try {
someMethodThatMightThrowException();
} catch (Exception e) {
log.error("An error occurred", e);
throw e; // 再次抛出异常
}
}
使用rollbackFor
属性:
- 如果希望捕获特定类型的异常而不回滚事务,可以使用
@Transactional
注解中的rollbackFor
属性来指定哪些类型的异常会导致事务回滚。
@Transactional(rollbackFor = RuntimeException.class)
public void updateData() {
try {
someMethodThatMightThrowException();
} catch (Exception e) {
log.error("An error occurred", e);
if (e instanceof RuntimeException) {
throw e; // 再次抛出运行时异常
}
}
}
使用noRollbackFor
属性:
- 如果希望某些类型的异常不会导致事务回滚,可以使用
noRollbackFor
属性。
@Transactional(noRollbackFor = IOException.class)
public void updateData() {
try {
someMethodThatMightThrowException();
} catch (IOException e) {
log.error("An I/O error occurred", e);
// 不再抛出异常
} catch (Exception e) {
log.error("An error occurred", e);
throw e; // 再次抛出其他类型的异常
}
}
使用@Transactional
注解的readOnly
属性:
- 如果方法是只读的,即不会修改数据库状态,可以将
readOnly
属性设置为true
。这样,即使方法抛出异常,事务也不会回滚。
@Transactional(readOnly = true)
public void updateData() {
try {
someMethodThatMightThrowException();
} catch (Exception e) {
log.error("An error occurred", e);
throw e; // 再次抛出异常
}
}
3、手动抛了别的异常
因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
以下设置都会回滚
@Service
public class ExampleService {
@Transactional(rollbackFor = Exception.class)
public void updateData() {
try {
someMethodThatMightThrowException();
} catch (Exception e) {
log.error("An error occurred", e);
throw e; // 再次抛出异常
}
}
private void someMethodThatMightThrowException() throws Exception {
// 这里可能会抛出异常
throw new Exception("An error occurred");
}
}
4、自定义了回滚异常
这个一般是使用rollbackFor参数来设置的,建议rollbackFor = Exception.class不会出现问题
5、嵌套事务回滚多了
在Spring中,嵌套事务可以通过PROPAGATION_NESTED
传播行为来实现。当一个事务方法调用另一个事务方法,并且后者使用PROPAGATION_NESTED
时,将创建一个嵌套事务。如果嵌套事务回滚,这通常意味着:
- 嵌套事务回滚:
- 如果嵌套事务中发生了异常并导致回滚,那么嵌套事务所做的更改将被撤销。
- 如果嵌套事务回滚,它不会自动导致父事务回滚,除非嵌套事务抛出了
RollbackException
或RuntimeException
等未被捕获的异常。
- 父事务状态:
- 父事务的状态不受嵌套事务回滚的直接影响,除非嵌套事务抛出的异常导致父事务也回滚。
- 如果嵌套事务回滚,但父事务中的其他部分仍然成功执行,那么父事务将继续进行。
假设有以下代码:
@Service
public class ExampleService {
@Transactional(propagation = Propagation.REQUIRED)
public void outerMethod() {
innerMethod();
}
@Transactional(propagation = Propagation.NESTED)
public void innerMethod() {
try {
someMethodThatMightThrowException();
} catch (Exception e) {
log.error("An error occurred in nested transaction", e);
// 如果需要回滚父事务,可以抛出 RollbackException 或 RuntimeException
throw new RollbackException("Nested transaction failed");
}
}
private void someMethodThatMightThrowException() {
// 这里可能会抛出异常
throw new Exception("An error occurred");
}
}
在这个例子中,outerMethod
使用 PROPAGATION_REQUIRED
,而 innerMethod
使用 PROPAGATION_NESTED
。如果 innerMethod
抛出异常,那么嵌套事务将回滚,但由于我们抛出了 RollbackException
,这将导致父事务 outerMethod
也回滚。
解决方案
-
显式控制回滚:
- 如果希望嵌套事务的回滚导致父事务回滚,可以在嵌套事务中抛出
RollbackException
或RuntimeException
。 - 如果不希望嵌套事务的回滚影响父事务,可以捕获异常并处理,但不要抛出
RollbackException
或RuntimeException
。
- 如果希望嵌套事务的回滚导致父事务回滚,可以在嵌套事务中抛出
-
使用
@Transactional
注解的rollbackFor
属性:- 如果希望嵌套事务中的特定异常导致父事务回滚,可以使用
@Transactional(rollbackFor = YourException.class)
来指定这些异常。
- 如果希望嵌套事务中的特定异常导致父事务回滚,可以使用
-
其实还可以在子事务中通过try…catch吃掉异常,这样也可以使父事务不受影响