从源码中窥探出事务失效的8种原因

x33g5p2x  于2021-12-01 转载在 其他  
字(8.2k)|赞(0)|评价(0)|浏览(251)

核心流程解读

我们从一段简单的代码入手,从头到尾分析以下其中的奥秘。

如果在一个controller中调用service方法,该方法被@Transaction注解修饰。

controller方法:

@GetMapping("/save")
    public String saveStudent() {
        testService.save();
        return "success";
    }

service方法:

@Transactional
    public void save() {
        //数据操作
    }

那么Spring在启动时,将会在主流程refresh的registerBeanPostProcessors的方法中首先加载所有的BeanPostProcessor,接着在

finishBeanFactoryInitialization中去初始化所有非懒加载的Bean。

首先对该controller进行加载,在属性赋值阶段,发现依赖service,于是转头去加载该service。

Bean的生命周期主要包含以下阶段:

接着实例化该service,执行属性赋值与初始化,在初始化的末尾,会执行BeanPostProcessor实现类AbstractAutoProxyCreator的postProcessAfterInitialization方法,该方法会判断是否需要为该service对象生成代理。

在Bean的生命周期全貌中,postProcessAfterInitialization方法位于绿色箭头标记的地方。

(对Spring Bean的生命周期不熟悉的同学,可以参考我的另外一篇文章还记不住Spring Bean的生命周期?看这篇你就知道方法了!)

接着进行判断是否进行事务代理,如果需要代理,则进入到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法中,尝试获取事务属性。

到这里,我们可以从代理角度得到事务不生效的两个原因

从代理角度来看

computeTransactionAttribute方法中,明确表示,如果当前方法的修饰为不为public,则返回null,代表不支持事务

Spring可能觉得既然不是公开的方法,代表应该由本类自己控制,而不应该被代理控制。另外final方法、static方法也不支持事务

当然,如果你对private方法应用@Transaction注解,则你的idea会进行警告,但真正运行时,并不会出现任何报错,给人一种没有问题的假象。

如果,你就是想对private应用该注解,并且也想让事务生效,则可以使用AspectJ。

AspectJ是一种静态代理方式,直接在编译期间修改字节码,性能比较好。而且不受修饰符的限制,不管你是private、final还是static,都可以进行代理。

还有一个原因比较简单,如果你的service没有加上@Service注解,则代表该service没有被Spring容器所管理,Spring更不会闲着没事生成代理对象,自然就不具有事务的功能。

我们继续走完代码,最终会来自SpringTransactionAnnotationParser的parseTransactionAnnotation方法,该方法会检查是否存在@Transaction注解。

如果存在@Transaction注解,则为该service生成代理对象,注入到controller的service变量中。

之后在调用controll的接口时,首先tomcat还会为本次请求分配一个线程,先执行所有的过滤器,接着进入DispatcherServlet的doDispatch方法中。

doDispatch调用handle方法,接着使用反射调用controller的接口方法,调用handle方法在SpringMVC主流程中被绿色箭头标记的位置。

(对Spring MVC主流程、过滤器与拦截器不清楚的同学,可以参考我的另外一篇文章从源码角度结合详细图例剖析过滤器与拦截器)

接着controller调用service方法,注意,spring为该service变量注入的是service的代理对象。

历经千辛万苦终于调用到了service的代理对象方法,接着代理方法会回调CglibAopProxy的静态内部类DynamicAdvisedInterceptor的intercept方法,该方法内部会拿到事务拦截器TransactionInterceptor,调用拦截器的invoke方法:

public Object invoke(MethodInvocation invocation) throws Throwable {
        //获取代理方法所处的被代理类,即原始类
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

invokeWithinTransaction方法处于父类TransactionAspectSupport中

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		//获取事务属性源,如果为null的话,代表该方法没有开启事务
		TransactionAttributeSource tas = getTransactionAttributeSource();
        //获取事务属性
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        //获取事务管理器
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
        //获取连接点标识,格式为被代理类的全限定名+方法名
        //例如:com.yang.ym.service.StudentService.save
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            //根据事务传播行为,看是否有必要创建出一个事务,判断逻辑位于AbstractPlatformTransactionManager的getTransaction方法中
            //如果有必要创建一个事务时,会立即开启一个新事务,并关闭自动提交
            //最后会利用ThreadLocal将事务信息绑定到当前线程中
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
			Object retVal = null;
			try {
                //调用拦截链中的下一个拦截器,位于ReflectiveMethodInvocation中
                //proceedWithInvocation会判断当前拦截器是否是最后一个,如果是的话,则直接调用连接点方法
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
                //当被代理方法执行异常时,会依据rollBackFor判断是否进行回滚
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
                //将此事务信息从当前线程中移除
				cleanupTransactionInfo(txInfo);
			}
            //提交事务
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

		else {
               //编程式事务,本次不作分析
		}
	}

这里给到了我们新的思路,在异常处理这块,也可能导致事务不生效或未回滚。

从异常处理以及异常类型角度来看

从invokeWithinTransaction中看出,如果我们在事务方法中,手动捕获了异常,但并未向上抛出,则已经修改的数据不会回滚

例如:

@Transactional
    public void save() {
        try {
            //a将不会进行回滚 
            Student a = new Student(null, "a", 18);
            studentDao.insert(a);

            //模拟业务异常
            int i = 1 / 0;
            
            Student b = new Student(null, "b", 18);
            studentDao.insert(b);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

当然,也不是抛出什么异常都会进行回滚的。

首先当原方法出现异常时,会进入到completeTransactionAfterThrowing中,最终会在RuleBasedTransactionAttribute的rollbackOn方法中进行判断。

public boolean rollbackOn(Throwable ex) {

		RollbackRuleAttribute winner = null;
		int deepest = Integer.MAX_VALUE;

        //@Transactional中的rollbackFor的值将会在项目启动的时候注入到rollbackRules中
        //将抛出的异常在rollbackRules中进行比对
		if (this.rollbackRules != null) {
			for (RollbackRuleAttribute rule : this.rollbackRules) {
				int depth = rule.getDepth(ex);
				if (depth >= 0 && depth < deepest) {
					deepest = depth;
					winner = rule;
				}
			}
		}

		//如果没有匹配到,则采用默认匹配策略
		if (winner == null) {
			return super.rollbackOn(ex);
		}

		return !(winner instanceof NoRollbackRuleAttribute);
	}

默认匹配策略处于父类DefaultTransactionAttribute中

public boolean rollbackOn(Throwable ex) {
		return (ex instanceof RuntimeException || ex instanceof Error);
	}

可以看出,如果抛出的异常在rollbackFor中匹配不到时,将会采用默认策略。在默认策略中,如果异常属于运行时异常或错误时,也将进行回滚。

在Java的异常体系中,Throwable下的继承关系如下图:

Throwable分为Error和Exception,其中Exception又分为受检异常(也称运行时异常)和受检查异常(编译器强制手动捕获)。

如果不在rollbackFor中指定受检异常时,事务方法将不会进行回滚。

例如:

@Transactional
    public void save() throws FileNotFoundException {
        //不会进行回滚
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        throw new FileNotFoundException();
    }

只有在rollbackFor中指定受检异常,或直接指定Exception时,才会进行回滚。

@Transactional(rollbackFor = Exception.class)
    public void save() throws FileNotFoundException {
        //会进行回滚
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        throw new FileNotFoundException();
    }

从传播行为角度来看

多个事务之间可能存在互相调用,那么在发生异常的情况下,怎么控制多个事务的回滚呢?

怎么指定部分事务不回滚,其余部分事务进行回滚呢?这就用到了事务的传播行为。

在Propagation枚举类中,共定义了七种传播行为,它们的特性为(针对于子方法):

|
传播行为
|
特性 |
|
REQUIRED
|

  1. 外围方法开启事务,子方法直接加入到外围事务中,形成一个整体事务。
  2. 外围方法没开启事务,则子方法创建独立的事务,不同子方法创建的事务互不干扰,可以独立回滚或提交。 |
    |
    SUPPORTS
    |
  3. 外围方法开启事务,子方法一起加入到外围事务中,形成一个整体事务。
  4. 外围方法没开启事务,则子方法直接不使用事务。 |
    |
    MANDATORY
    |
  5. 外围方法开启事务,子方法一起加入到外围事务中,形成一个整体事务。
  6. 外围方法没开启事务,则子方法直接抛出异常,强制需要外围方法有事务。 |
    |
    REQUIRES_NEW
    |
  7. 不管外围方法有没有开启事务,子方法都会创建一个属于自己的事务,子方法创建的事务互不干扰,与外围方法也互不干扰。 |
    |
    NOT_SUPPORTED
    |
  8. 不管外围方法有没有开启事务,子方法都不想去使用事务,外围方法回滚时,不会回滚子方法。 |
    |
    NEVER
    |
  9. 外围方法开启事务,则子方法直接抛出异常。
  10. 外围方法没开启事务,子方法也不会使用事务。 |
    |
    NESTED
    |
  11. 外围方法开启事务,外围事务回滚时,子事务会全部回滚。但某一个子事务由于异常回滚时,不会影响外围事务与其他子事务。
  12. 外围方法没开启事务,同REQUIRED(2)。 |

假如在某次开发中,本来所有的方法调用应该在一个事务中(propagation = Propagation.REQUIRED),但错误地指定为了REQUIRES_NEW。

首先我们调用TestService的save方法:

@Transactional
    public void save() {
        studentService.saveA();
        studentService.saveB();
    }

saveA与saveB在另外一个service类中:

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveA() {
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveB() {
        Student b = new Student(null, "b", 20);
        studentDao.insert(b);
        //模拟业务异常
        int i = 1 / 0;
    }

这样saveB在出现异常后,数据b可以被回滚,但数据a不会被回滚,形成脏数据。

对七种传播行为的测试,可以参考我的另外一篇博客Spring事务的传播行为

从方法调用角度来看

如果在一个service中,非事务方法调用事务方法,接着controller调用非事务方法时,则事务方法内的事务失效。

即调用saveA时,saveB里的事务会失效,即数据a不会被回滚。

public void saveA(){
        saveB();
    }

    @Transactional
    public void saveB() {
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        int i = 1 / 0;
    }

这是为什么呢?很简单

当controller调用saveA时,并不会被代理对象拦截到,因为不需要为saveA生成代理方法。那么就会直接执行原对象的方法,这时候的saveB,相当于this.saveB,即调用原对象的saveB方法,没经过代理对象调用,所以事务不会生效

那怎么修改,使得事务生效呢?

可以把this.saveB改为代理对象.saveB,需要先在该service中注入自己。

@Service
public class TestService {
    
    @Autowired
    TestService testService;

    public void saveA() {
        testService.saveB();
    }

    @Transactional
    public void saveB() {
        //事务生效
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        int i = 1 / 0;
    }

}

在本类中注入自己,很奇怪啊,不会发生循环依赖吗?

当然,Spring本身具有解决循环依赖的能力,下图以A依赖A为例,解释解决循环依赖的具体过程:

如果想要了解更多关于Spring是如何解决循环依赖的知识点,可以参考我的另外一篇文章手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

那事务方法调用事务方法,不过是以多线程的方式调用的呢?

例如:

@Transactional
    public void saveA() {
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);

        new Thread(()->{
            Student b = new Student(null, "b", 18);
            studentDao.insert(b);
        }).start();

        int i=1/0;
    }

数据b也不会被回滚,事务失效了。

在TransactionSynchronizationManager类中,会将当前的事务信息利用ThreadLocal绑定到线程中,也就是说,每个线程使用的是单独的数据库连接,分别存在于不同的事务中。因此出现异常后,数据a所处的事务会回滚,而数据b处在另外一个事务中,不会进行回滚。

从外部条件角度来看

在MySQL5.5版本之前,其默认的存储引擎为MyISAM,它特点有

  • 不支持事务
  • 不支持外键
  • 不支持行锁,只支持表锁
  • 为聚集索引
  • 支持全文索引

如果我们操作的表属于MyISAM类型,则事务是不可能生效的。

MySQL5.5之后,默认存储引擎改为了InnoDB,这个是支持事务的。

所以,小伙伴们在发现事务不生效,抓耳挠腮始终找不到原因时,别忘了偷瞄一下表的类型。

总结

事务失效或不回滚的情况,可以从以下的角度来思考:

从代理角度来看

1、方法的修饰符不是public类型的,另外final与static也是不支持事务的。

2、事务方法所处的对象并没有Spring管理,自然也不会生成代理对象。

从异常处理以及异常类型角度来看

1、事务方法中手动捕获异常,但并未向上抛出,那么该方法不会进行回滚。

2、事务方法中抛出的异常,不在rollbackFor中,也不属于RuntimeException或者Error时,不会进行回滚。

从传播行为角度来看

1、传播行为指定错误,例如:内部方法和外部方法不在一个事务中,内部方法回滚后,外部方法没有进行回滚

从方法调用角度来看

1、非事务方法调用事务方法,因为相当于调用原始类的普通方法。

2、事务方法a在另外一个线程中调用事务方法b,a出现异常,不会回滚b。因为每个线程处于不同的事务中,不会互相影响。

从外部条件角度来看

1、存储引擎不支持事务,例如表的类型为MyIASM,本身就不支持事务

相关文章