Venus框架加入了对事务传播级别的检测,如果传播级别不是REQUIRED并且没有加入白名单,则会抛出异常,禁止启动,这也可能是你看到这篇文章的原因之一 。
是否继续阅读
如果你很清楚以下几点的话,直接跳到 【如何避免启动异常】章节 的内容。
- 事务不同传播级别的意义
- 不同传播级别在嵌套事务下的不同反应
- 不同事务传播级别对链接池的影响
如果你不清楚上面的几点的话可以花一点时间看一下下面正文的内容,再决定到底使用哪种传播级别。
正文
你可能在很多地方都看过事务的传播级别以及隔离级别,这里就只是简单地归纳一下,更具体的内容可以参看Juergen Hoeller的资料。
事务传播行为类型
|
说明
|
场景
|
风险
|
---|---|---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务。 如果已经存在一个事务中,加入到这个事务中。 | 嵌套事务 | 嵌套事务中抛出异常,会将父事务的操作也进行回滚。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 | 希望子事务不影响父事务 | 多获取一个链接,在和父事务操作同一条记录、同一个数据库的情况下会导致死锁。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | 同REQUIRED。 | |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | 希望在事务中执行一段非事务的逻辑 | 同REQUIRES_NEW。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | 不允许嵌套事务 | 不支持嵌套事务 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 | 同REQUIRED。 | |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。 如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。 | 嵌套事务(savepoint) | 需要try/catch才能实现子事务回滚。 |
下面我们就仔细讨论一下各种传播级别。
根据上述表格,我们可以大致划分为两类,一种是事务,一种是非事务,由于事务本身不是本节的讨论内容,所以只是一句话概括。
事务的话,Spring会进行拦截,获取链接,设置autocommit为false,并如果出错的话进行回滚。(如果有嵌套事务的话会不太一样,下面会说)
非事务的话,Spring也会进行拦截,获取链接,但不会设置autocommit,出错的话也不进行回滚,而是继续向上抛出异常
可以看到,上面的表格中在描述事务传播级别时,都有前置条件,那就是是否自己是一个嵌套事务。也就是调用本方法的上层方法是否是一个事务,下面我们仔细分析在嵌套事务下的各种级别。
|
REQUIRED
|
REQUIRES_NEW
|
SUPPORTS
|
NOT_SUPPORTED
|
NESTED
|
MANDATORY
|
NEVER
|
---|---|---|---|---|---|---|---|
是否获取新的链接 |
是否获取新的链接,是由传播级别是否支持当前事务来决定,由于REQUIRES_NEW会将当前事务挂起,重新建立新的事务,所以需要从新获取链接。而NOT_SUPPORTED也是挂起当前事务,和REQUIRES_NEW的区别在于,NOT_SUPPORTED本身不支持事务。
获取新的链接本身会带来很多隐患:
- 会浪费链接资源
- 可能导致死锁
浪费链接比较好理解,因为挂起父事务后,无论当前支不支持事务,都需要重新获取链接,只是一个在方法前在事务管理器里获取,而另一个则是方法执行到数据库操作时获取(这里需要说的是,使用NOT_SUPPORTED不支持事务的情况下,同一个方法内也只使用一个链接)
死锁是怎么导致的呢?我们将死锁分成两部分进行分析:
- 数据库死锁
这种情况下我们认为链接资源充分,不是死锁的条件。
假设这种场景,父方法需要update某条数据,由于存在事务,所以会持有该数据的行锁。到子方法后,父方法事务被挂起,如果子方法内部也需要update该数据,则会因为上一个事务没有提交,导致会获取锁超时,导致死锁。
死锁的超时时间需要看MySQL自身的设定。所以,希望大家在使用这两种传播级别的时候,想一下是否真的需要挂起上一个事务,导致数据库层面的死锁? - 链接池死锁
假设这种场景,一共10个链接,但是有20个线程在运行,由于嵌套事务的存在,我们需要获取两次链接,导致我们最大需要40个链接,下面我们看一下具体的死锁场景。
首先,我们假设十个线程抢到了链接,但是由于大家都是更新同一条数据,只有一个线程能够获得数据库的行锁,此时,该线程执行子事务,需要再获取一个链接,但是由于链接池没有资源,导致了获取了行锁的线程不能获得链接,阻塞在了这里。但是,其他九个线程又都卡在了获取行锁的地方不能释放链接,这里就导致了九个线程在等待行锁,而另一个线程等待链接的场景,从而导致了死锁。
思考另一个场景,十个线程拿到了链接,大家都没有行锁的问题,但是由于链接池的资源耗尽,导致了死锁异常。不同的区别是这里会抛出链接获取超时的异常,而上面场景会抛出lock wait timeout异常。
下面我们通过最简单的代码,来进行一下嵌套事务行为的分析:
public class A {
@Transactional
method a {
update();
try{
B.b();
}catch{
log();
}
}
}
publicclass B {
method b {
update();
throwRuntimeException();
}
}
我们来分类讨论一下方法b上d如果加上不同的事务传播级别会发生什么?
1. REQUIRED
最普遍的情况,方法b会是默认的传播级别,那么由于b本身会发生异常,所以b中的update会进行回滚,但是!由于REQUIRED是复用父事务,所以链接也是同一个,在rollback时,会将a方法中的update也进行回滚。
回滚完毕之后,会将异常抛至方法a中,并被catch补获,正常运行结束,进行commit时会由于已经在方法b中进行了回滚,所以会抛出异常,UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only")
2. REQUIRES_NEW
本次出问题的传播级别,进入方法b时,根据上面说到的,如果update的是同一条数据,则会死锁,如果是不同的数据,则能够成功执行,但是由于本身之后会抛异常,导致b方法的update会被回滚。
剩下的和REQUIRED一样,只是在最后commit的时候,由于b方法是新起一个事务,所以不会出异常,成功update。
3. SUPPORTS
在嵌套事务下,和REQUIRED一样,不做讨论
4. NOT_SUPPORTED
进入方法b后,先进行update,在抛出异常,由于当前不是事务,所以会已经update的数据会存在数据库中,之后流程与REQUIRES_NEW一样。
5. NESTED
到方法b后,先update,再抛异常,由于本身是事务,所以会回滚。
!!!和REQUIRED不一样的地方在这里,虽然NESTED在子事务不会获取新的链接,但是会设置一个savepoint,即方法b抛异常后,会回滚到方法b未执行之前,此时抛出异常后,虽然会被a捕获,但是能够成功commit!不会发生REQUIRED的异常。
结论是虽然b的update被回滚了,但是a的能够成功update。当然,如果外部的a需要回滚了,内部子事务无论成功失败,都会回滚。
相信大家看完上面的实例分析之后,可能对传播级别有了更好的认识,请大家妥善的检查自己的代码,看看传播级别是否真的写对了,是否是想要NESTED来实现某个子事务失败时可以走另一个子事务,并不影响父事务,而不是REQUIRES_NEW?
如何避免启动异常
我已经知道了异常产生的原因,并且已经理解了传播级别,现在我想取消警告我该怎么做呢?
你只需要在项目里的application.properties中配置transactional.propagation.whitelist,并将你想要排出的方法以逗号分隔即可。
Example:
transactional.propagation.whitelist=com.vip.venus.data.matrixdatasource.transactional.TranscationalTestServiceImpl.create,com.vip.venus.data.matrixdatasource.transactional.TranscationalTestServiceImpl.createSelective