史上最全Spring的@Transactional不生效的12大场景

一、事务不生效

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时,情况会有所不同:

  1. JDK动态代理final方法并不会影响JDK动态代理的行为,因为JDK动态代理并不修改目标类本身,而是创建一个新的代理类来实现相同接口。所以,在使用JDK动态代理的情况下,即使方法被声明为final,事务管理仍然能够正常工作。
  2. CGLIB代理:CGLIB代理通过字节码增强的方式生成子类,然后重写目标类的方法以实现方法级别的拦截。这意味着,如果目标类的方法被声明为final,CGLIB就无法重写这个方法,因此也就无法为其添加事务管理的逻辑。在这种情况下,事务管理将会失效。

总结来说,如果类使用了CGLIB代理,并且该类中的方法被声明为final,那么这些方法上的事务管理将不会生效。为了避免这种情况,可以选择使用JDK动态代理,但这要求类必须实现至少一个接口。另一种解决方法是移除final关键字,这样无论使用哪种代理方式,事务管理都可以正常工作。

如果需要确保final方法也能支持事务管理,并且类没有实现任何接口,那么可能需要考虑其他替代方案,比如更改类的设计使其不再使用final方法,或者使用基于XML的配置来显式指定代理类型。

3、内部调用

在Spring框架中,内部方法调用(即一个类中的方法调用该类内的另一个方法)通常不会触发事务边界。这是因为Spring的事务管理是通过AOP(面向切面编程)代理实现的,而代理只能处理对类的外部调用,而不是类内部的方法调用。

当一个类中的方法被标记为@Transactional时,Spring会为这个类创建一个代理。当外部代码调用该类的一个方法时,实际上是调用了代理对象上的方法,这样就可以在调用前后添加事务管理逻辑。但是,如果一个方法内部调用另一个方法,这种调用发生在同一个实例内部,因此不会经过代理层,也就不会触发事务管理。

例如,假设有一个类Service,其中包含两个方法publicMethodprivateMethod,并且这两个方法都被标记为@Transactional

@Service
public class Service {

    @Transactional
    public void publicMethod() {
        privateMethod();
    }

    @Transactional
    private void privateMethod() {
        // ...
    }
}

在这种情况下,只有publicMethod的外部调用会被代理并受到事务管理。privateMethod的调用由于发生在类内部,不会触发事务边界。这意味着privateMethod实际上不会按照预期那样被事务管理。

为了确保所有方法都受到事务管理,可以采取以下几种策略之一:

  1. 使用@Transactional注解类级别: 如果所有方法都需要事务管理,可以将@Transactional注解放在类级别上,这样所有公开的方法都会自动被事务管理。
  2. 自我调用模式: 使用代理对象来自我调用方法,以确保方法调用会经过代理,从而触发事务管理。例如,您可以将方法改为如下形式:
@Service
public class Service {

    @Autowired
    private Service self;

    @Transactional
    public void publicMethod() {
        self.privateMethod();
    }

    @Transactional
    private void privateMethod() {
        // ...
    }
}
  1. 使用@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中清除。

在多线程环境下,有几个关键点需要注意:

  1. 独立线程的事务隔离
    • 每个线程都有其自己的ThreadLocal副本,这意味着不同线程之间的事务上下文是相互隔离的。这意味着如果一个线程启动了一个事务,而另一个线程试图访问同一资源,它将看不到前一个线程中的事务状态,除非显式地通过某种机制共享事务上下文。
  2. 共享资源的事务管理
    • 当多个线程试图访问由Spring管理的同一资源时,如果没有适当的同步机制,可能会出现并发问题。例如,如果一个线程正在执行一个事务中的操作,而另一个线程同时尝试修改相同的资源,那么第二个线程的操作可能会干扰第一个线程的事务。
  3. 显式事务管理
    • 对于需要跨线程共享事务上下文的情况,通常需要显式地管理事务,例如通过使用TransactionTemplatePlatformTransactionManager API手动管理事务。这样可以在需要的时候显式地开始、提交或回滚事务。
  4. 线程池中的事务管理
    • 如果您使用的是线程池,那么线程池中的线程会复用。这意味着如果一个线程在一个事务中结束并被归还到线程池中,然后被重新分配到一个新的任务,它可能会携带旧的事务上下文。为了避免这种情况,可以使用TransactionSynchronizationManager来清理线程本地的事务状态。
  5. 使用@Transactional注解
    • 如果使用@Transactional注解来声明事务边界,那么只要确保线程调用符合Spring AOP的代理机制,事务管理通常会按预期工作。但是,如果多个线程并发地调用同一个带有@Transactional注解的方法,那么每个线程都会有自己的事务实例。
  6. 事务传播行为
    • 对于在多线程环境中使用@Transactional的方法,了解事务传播行为非常重要。例如,如果您使用PROPAGATION_REQUIRES_NEW,那么即使在一个线程内,内部方法调用也会创建一个新的事务实例。

总结

  • 多线程环境下的事务管理需要特别注意,特别是当涉及到共享资源时。
  • 如果希望在多线程环境中保持事务的一致性,通常需要显式地管理事务,或者确保事务传播行为符合需求。
  • 使用@Transactional注解时,每个线程会有自己的事务实例,除非显式地配置事务传播行为来改变这一点。

要确保多线程环境中事务的正确管理,可能需要考虑使用更高级别的事务管理API,例如TransactionTemplatePlatformTransactionManager,并在必要时使用同步机制来确保事务的正确性。

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提供了七种不同的传播行为,每种都有不同的用途。以下是这些传播行为的概述:

  1. PROPAGATION_REQUIRED:
    • 这是默认的传播行为。
    • 如果当前存在事务,则加入该事务。
    • 如果当前不存在事务,则创建一个新的事务。
  2. PROPAGATION_SUPPORTS:
    • 如果当前存在事务,则加入该事务。
    • 如果当前不存在事务,则以非事务方式执行。
  3. PROPAGATION_MANDATORY:
    • 如果当前存在事务,则加入该事务。
    • 如果当前不存在事务,则抛出TransactionRequiredException
  4. PROPAGATION_REQUIRES_NEW:
    • 创建一个新的事务。
    • 如果当前存在事务,则挂起该事务。
    • 新事务完成后,恢复之前被挂起的事务(如果有的话)。
  5. PROPAGATION_NOT_SUPPORTED:
    • 以非事务方式执行。
    • 如果当前存在事务,则挂起该事务。
  6. PROPAGATION_NEVER:
    • 以非事务方式执行。
    • 如果当前存在事务,则抛出IllegalTransactionStateException
  7. 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时,将创建一个嵌套事务。如果嵌套事务回滚,这通常意味着:

  1. 嵌套事务回滚
    • 如果嵌套事务中发生了异常并导致回滚,那么嵌套事务所做的更改将被撤销。
    • 如果嵌套事务回滚,它不会自动导致父事务回滚,除非嵌套事务抛出了RollbackExceptionRuntimeException等未被捕获的异常。
  2. 父事务状态
    • 父事务的状态不受嵌套事务回滚的直接影响,除非嵌套事务抛出的异常导致父事务也回滚。
    • 如果嵌套事务回滚,但父事务中的其他部分仍然成功执行,那么父事务将继续进行。

假设有以下代码:

@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 也回滚。

解决方案

  1. 显式控制回滚

    • 如果希望嵌套事务的回滚导致父事务回滚,可以在嵌套事务中抛出 RollbackExceptionRuntimeException
    • 如果不希望嵌套事务的回滚影响父事务,可以捕获异常并处理,但不要抛出 RollbackExceptionRuntimeException
  2. 使用@Transactional注解的rollbackFor属性

    • 如果希望嵌套事务中的特定异常导致父事务回滚,可以使用 @Transactional(rollbackFor = YourException.class) 来指定这些异常。
  3. 其实还可以在子事务中通过try…catch吃掉异常,这样也可以使父事务不受影响
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值