@Transactional注解的失效场景
@Transactional注解的失效場(chǎng)景
引言
@Transactional 注解相信大家并不陌生,平時(shí)開(kāi)發(fā)中很常用的一個(gè)注解,它能保證方法內(nèi)多個(gè)數(shù)據(jù)庫(kù)操作要么同時(shí)成功、要么同時(shí)失敗。使用@Transactional注解時(shí)需要注意許多的細(xì)節(jié),不然你會(huì)發(fā)現(xiàn)@Transactional總是莫名其妙的就失效了。
下面我們從what ,where,when四個(gè)方面徹底弄明白如何回答面試官的問(wèn)題。
一、什么是事務(wù)(WHAT)
事務(wù)(Transaction),一般是指要做的或所做的事情。在計(jì)算機(jī)術(shù)語(yǔ)中是指訪問(wèn)并可能更新數(shù)據(jù)庫(kù)中各種數(shù)據(jù)項(xiàng)的一個(gè)程序執(zhí)行單元(unit)。
這里我們以取錢(qián)的例子來(lái)講解:比如你去ATM機(jī)取1000塊錢(qián),大體有兩個(gè)步驟:第一步輸入密碼金額,銀行卡扣掉1000元錢(qián);第二步從ATM出1000元錢(qián)。這兩個(gè)步驟必須是要么都執(zhí)行要么都不執(zhí)行。如果銀行卡扣除了1000塊但是ATM出錢(qián)失敗的話,你將會(huì)損失1000元;如果銀行卡扣錢(qián)失敗但是ATM卻出了1000塊,那么銀行將損失1000元。
如何保證這兩個(gè)步驟不會(huì)出現(xiàn)一個(gè)出現(xiàn)異常了,而另一個(gè)執(zhí)行成功呢?事務(wù)就是用來(lái)解決這樣的問(wèn)題。事務(wù)是一系列的動(dòng)作,它們綜合在一起才是一個(gè)完整的工作單元,這些動(dòng)作必須全部完成,如果有一個(gè)失敗的話,那么事務(wù)就會(huì)回滾到最開(kāi)始的狀態(tài),仿佛什么都沒(méi)發(fā)生過(guò)一樣。在企業(yè)級(jí)應(yīng)用程序開(kāi)發(fā)中,事務(wù)管理是必不可少的技術(shù),用來(lái)確保數(shù)據(jù)的完整性和一致性。
在我們?nèi)粘i_(kāi)發(fā)中事務(wù)分為聲明式事務(wù)和編程式事務(wù)。
編程式事務(wù)
是指在代碼中手動(dòng)的管理事務(wù)的提交、回滾等操作,代碼侵入性比較強(qiáng)。
編程式事務(wù)指的是通過(guò)編碼方式實(shí)現(xiàn)事務(wù),允許用戶在代碼中精確定義事務(wù)的邊界。
即類(lèi)似于JDBC編程實(shí)現(xiàn)事務(wù)管理。管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。
對(duì)于編程式事務(wù)管理,spring推薦使用TransactionTemplate。
try {//TODO somethingtransactionManager.commit(status); } catch (Exception e) {transactionManager.rollback(status);throw new InvoiceApplyException("異常"); }聲明式事務(wù)
管理建立在AOP之上的。其本質(zhì)是對(duì)方法前后進(jìn)行攔截,然后在目標(biāo)方法開(kāi)始之前創(chuàng)建或者加入一個(gè)事務(wù),在執(zhí)行完目標(biāo)方法之后根據(jù)執(zhí)行情況提交或者回滾事務(wù)。
聲明式事務(wù)最大的優(yōu)點(diǎn)就是不需要通過(guò)編程的方式管理事務(wù),這樣就不需要在業(yè)務(wù)邏輯代碼中摻雜事務(wù)管理的代碼,只需在配置文件中做相關(guān)的事務(wù)規(guī)則聲明(或通過(guò)基于@Transactional注解的方式),便可以將事務(wù)規(guī)則應(yīng)用到業(yè)務(wù)邏輯中。
簡(jiǎn)單地說(shuō),編程式事務(wù)侵入到了業(yè)務(wù)代碼里面,但是提供了更加詳細(xì)的事務(wù)管理;
而聲明式事務(wù)由于基于AOP,所以既能起到事務(wù)管理的作用,又可以不影響業(yè)務(wù)代碼的具體實(shí)現(xiàn)。
聲明式事務(wù)也有兩種實(shí)現(xiàn)方式,一是基于TX和AOP的xml配置文件方式,二種就是基于@Transactional注解了。
@GetMapping("/user") @Transactional public String user() {int insert = userMapper.insert(userInfo); }二、@Transactional可以在什么地方使用(WHERE)
1、@Transactional注解可以作用于哪些地方?
@Transactional 可以作用在接口、類(lèi)、類(lèi)方法。
- 作用于類(lèi):當(dāng)把@Transactional 注解放在類(lèi)上時(shí),表示所有該類(lèi)的public方法都配置相同的事務(wù)屬性信息。
- 作用于方法:當(dāng)類(lèi)配置了@Transactional,方法也配置了@Transactional,方法的事務(wù)會(huì)覆蓋類(lèi)的事務(wù)配置信息。
- 作用于接口:不推薦這種使用方法,因?yàn)橐坏?biāo)注在Interface上并且配置了Spring AOP 使用CGLib動(dòng)態(tài)代理,將會(huì)導(dǎo)致@Transactional注解失效
2、@Transactional屬性詳解
propagation屬性
propagation 代表事務(wù)的傳播行為,默認(rèn)值為 Propagation.REQUIRED,其他的屬性信息如下:
- Propagation.REQUIRED:如果當(dāng)前存在事務(wù),則加入該事務(wù),如果當(dāng)前不存在事務(wù),則創(chuàng)建一個(gè)新的事務(wù)。( 也就是說(shuō)如果A方法和B方法都添加了注解,在默認(rèn)傳播模式下,A方法內(nèi)部調(diào)用B方法,會(huì)把兩個(gè)方法的事務(wù)合并為一個(gè)事務(wù) )
- Propagation.SUPPORTS:如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前不存在事務(wù),則以非事務(wù)的方式繼續(xù)運(yùn)行。
- Propagation.MANDATORY:如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前不存在事務(wù),則拋出異常。
- Propagation.REQUIRES_NEW:重新創(chuàng)建一個(gè)新的事務(wù),如果當(dāng)前存在事務(wù),暫停當(dāng)前的事務(wù)。( 當(dāng)類(lèi)A中的 a 方法用默認(rèn)Propagation.REQUIRED模式,類(lèi)B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中調(diào)用 b方法操作數(shù)據(jù)庫(kù),然而 a方法拋出異常后,b方法并沒(méi)有進(jìn)行回滾,因?yàn)镻ropagation.REQUIRES_NEW會(huì)暫停 a方法的事務(wù) )
- Propagation.NOT_SUPPORTED:以非事務(wù)的方式運(yùn)行,如果當(dāng)前存在事務(wù),暫停當(dāng)前的事務(wù)。
- Propagation.NEVER:以非事務(wù)的方式運(yùn)行,如果當(dāng)前存在事務(wù),則拋出異常。
- Propagation.NESTED :和 Propagation.REQUIRED 效果一樣。
isolation 屬性
isolation :事務(wù)的隔離級(jí)別,默認(rèn)值為 Isolation.DEFAULT。
TransactionDefinition.ISOLATION_DEFAULT
這是默認(rèn)值,表示使用底層數(shù)據(jù)庫(kù)的默認(rèn)隔離級(jí)別。對(duì)大部分?jǐn)?shù)據(jù)庫(kù)而言,通常這值就是
TransactionDefinition.ISOLATION_READ_UNCOMMITTED
該隔離級(jí)別表示一個(gè)事務(wù)可以讀取另一個(gè)事務(wù)修改但還沒(méi)有提交的數(shù)據(jù)。該級(jí)別不能防止臟讀,不可重復(fù)讀和幻讀,因此很少使用該隔離級(jí)別。比如PostgreSQL實(shí)際上并沒(méi)有此級(jí)別。
TransactionDefinition.ISOLATION_READ_COMMITTED
該隔離級(jí)別表示一個(gè)事務(wù)只能讀取另一個(gè)事務(wù)已經(jīng)提交的數(shù)據(jù)。該級(jí)別可以防止臟讀,這也是大多數(shù)情況下的推薦值。
TransactionDefinition.ISOLATION_REPEATABLE_READ
該隔離級(jí)別表示一個(gè)事務(wù)在整個(gè)過(guò)程中可以多次重復(fù)執(zhí)行某個(gè)查詢,并且每次返回的記錄都相同。該級(jí)別可以防止臟讀和不可重復(fù)讀。
TransactionDefinition.ISOLATION_SERIALIZABLE
所有的事務(wù)依次逐個(gè)執(zhí)行,這樣事務(wù)之間就完全不可能產(chǎn)生干擾,也就是說(shuō),該級(jí)別可以防止臟讀、不可重復(fù)讀以及幻讀。但是這將嚴(yán)重影響程序的性能。通常情況下也不會(huì)用到該級(jí)別。
timeout 屬性
timeout :事務(wù)的超時(shí)時(shí)間,默認(rèn)值為 -1。如果超過(guò)該時(shí)間限制但事務(wù)還沒(méi)有完成,則自動(dòng)回滾事務(wù)。
readOnly 屬性
readOnly :指定事務(wù)是否為只讀事務(wù),默認(rèn)值為 false;為了忽略那些不需要事務(wù)的方法,比如讀取數(shù)據(jù),可以設(shè)置 read-only 為 true。
rollbackFor 屬性
rollbackFor :用于指定能夠觸發(fā)事務(wù)回滾的異常類(lèi)型,可以指定多個(gè)異常類(lèi)型。
noRollbackFor屬性**
noRollbackFor:拋出指定的異常類(lèi)型,不回滾事務(wù),也可以指定多個(gè)異常類(lèi)型。
二、@Transactional什么時(shí)候會(huì)失效(WHEN)
面試官就直接問(wèn)我有沒(méi)有用過(guò)@Transactional,我肯定不能說(shuō)沒(méi)用過(guò)啊,十分自信的說(shuō),常用。
面試官又問(wèn)我,在實(shí)際開(kāi)發(fā)過(guò)程有沒(méi)有遇到過(guò)@Transactional失效的情況,我肯定不能說(shuō)沒(méi)有啊,再次十分自信的說(shuō)到,經(jīng)常。
面試官一臉問(wèn)號(hào),經(jīng)常???那你給我說(shuō)說(shuō)@Transactional在什么時(shí)候會(huì)失效呢?
下面的內(nèi)容是我將我面試時(shí)說(shuō)的失效場(chǎng)景整理了一下。
1、@Transactional 應(yīng)用在非 public 修飾的方法上
如果Transactional注解應(yīng)用在非public 修飾的方法上,Transactional將會(huì)失效。
之所以會(huì)失效是因?yàn)樵赟pring AOP 代理時(shí),TransactionInterceptor(事務(wù)攔截器)在目標(biāo)方法執(zhí)行前后進(jìn)行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內(nèi)部類(lèi))的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會(huì)間接調(diào)用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute`方法,獲取Transactional 注解的事務(wù)配置信息。
protected TransactionAttribute computeTransactionAttribute(Methodmethod,Class<?> targetClass) {// Don't allow no-public methods as required.if (allowPublicMethodsOnly() &&!Modifier.isPublic(method.getModifiers())) {return null; }Modifier.isPublic會(huì)檢查目標(biāo)方法的修飾符是否為 public,不是 public則不會(huì)獲取@Transactional 的屬性配置信息。
注意:protected、private 修飾的方法上使用 @Transactional 注解,雖然事務(wù)無(wú)效,但不會(huì)有任何報(bào)錯(cuò),這是我們很容犯錯(cuò)的一點(diǎn)。
2、數(shù)據(jù)庫(kù)引擎要不支持事務(wù)
數(shù)據(jù)庫(kù)引擎要支持事務(wù),如果是MySQL,注意表要使用支持事務(wù)的引擎,比如innodb,如果是myisam,事務(wù)是不起作用的。
3、@由于propagation 設(shè)置錯(cuò)誤,導(dǎo)致注解失效
在上面解讀propagation 屬性的時(shí)候,我們知道
TransactionDefinition.PROPAGATION_SUPPORTS
如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前沒(méi)有事務(wù),則以非事務(wù)的方式繼續(xù)運(yùn)行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
以非事務(wù)方式運(yùn)行,如果當(dāng)前存在事務(wù),則把當(dāng)前事務(wù)掛起。
TransactionDefinition.PROPAGATION_NEVER
以非事務(wù)方式運(yùn)行,如果當(dāng)前存在事務(wù),則拋出異常。
當(dāng)我們將propagation 屬性設(shè)置為上述三種時(shí),@Transactional 注解就不會(huì)產(chǎn)生效果
4、rollbackFor 設(shè)置錯(cuò)誤,@Transactional 注解失效
上述我們解讀rollbackFor 屬性的時(shí)候我們知道
rollbackFor 可以指定能夠觸發(fā)事務(wù)回滾的異常類(lèi)型。
Spring默認(rèn)拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務(wù);
其他異常不會(huì)觸發(fā)回滾事務(wù)。如果在事務(wù)中拋出其他類(lèi)型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定 rollbackFor屬性。
// 希望自定義的異常可以進(jìn)行回滾 @Transactional(propagation= Propagation.REQUIRED,rollbackFor=MyException.class若在目標(biāo)方法中拋出的異常是 rollbackFor 指定的異常的子類(lèi),事務(wù)同樣會(huì)回滾。Spring源碼如下:
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); }5、方法之間的互相調(diào)用也會(huì)導(dǎo)致@Transactional失效
我們來(lái)看下面的場(chǎng)景:
比如有一個(gè)類(lèi)User,它的一個(gè)方法A,A再調(diào)用本類(lèi)的方法B(不論方法B是用public還是private修飾),但方法A沒(méi)有聲明注解事務(wù),而B(niǎo)方法有。則外部調(diào)用方法A之后,方法B的事務(wù)是不會(huì)起作用的。這也是經(jīng)常犯錯(cuò)誤的一個(gè)地方。
那為啥會(huì)出現(xiàn)這種情況?其實(shí)這還是由于使用Spring AOP代理造成的,因?yàn)橹挥挟?dāng)事務(wù)方法被當(dāng)前類(lèi)以外的代碼調(diào)用時(shí),才會(huì)由Spring生成的代理對(duì)象來(lái)管理。
//@Transactional@GetMapping("/user")private Integer A() throws Exception {User user = new User();user.setName("javaHuang");/*** B 插入字段為 topJavaer的數(shù)據(jù)*/this.insertB();/*** A 插入字段為 2的數(shù)據(jù)*/int insert = userMapper.insert(user);return insert;}@Transactional()public Integer insertB() throws Exception {User user = new User();user.setName("topJavaer");return userMapper.insert(user);}6、異常被你的 catch“吃了”導(dǎo)致@Transactional失效
這種情況是最常見(jiàn)的一種@Transactional注解失效場(chǎng)景,
@Transactionalprivate Integer A() throws Exception {int insert = 0;try {User user = new User();user.setCityName("javaHuang");user.setUserId(1);/*** A 插入字段為 javaHuang的數(shù)據(jù)*/insert = userMapper.insert(user);/*** B 插入字段為 topJavaer的數(shù)據(jù)*/b.insertB();} catch (Exception e) {e.printStackTrace();}}如果B方法內(nèi)部拋了異常,而A方法此時(shí)try catch了B方法的異常,那這個(gè)事務(wù)就不能正常回滾,而是會(huì)報(bào)出異常
org.springframework.transaction.UnexpectedRollbackException: Transactionrolled back because it has been marked as rollback-only解決方法:
第一:聲明事務(wù)的時(shí)候加上rollback=‘exception’
第二 :cath代碼塊里面手動(dòng)回滾
總結(jié)
@Transactional 注解我們經(jīng)常使用,但是往往我們也只是知道它是一個(gè)事務(wù)注解,很多時(shí)候遇到事務(wù)注解失效的情況下,我們都是一頭霧水,看不出個(gè)所以然來(lái),花費(fèi)了很長(zhǎng)的時(shí)間都不能解決。
通過(guò)本文了解了@Transactional 注解的失效場(chǎng)景,在以后遇到這種情況時(shí),基本就能一眼看破,然后摸摸自己光滑的腦門(mén),soga,so easy!
媽媽再也不用擔(dān)心我找不到自己寫(xiě)的bug了。
分析spring事務(wù)@Transactional注解在同一個(gè)類(lèi)中的方法之間調(diào)用不生效的原因及解決方案
問(wèn)題:
在Spring管理的項(xiàng)目中,方法A使用了Transactional注解,試圖實(shí)現(xiàn)事務(wù)性。但當(dāng)同一個(gè)class中的方法B調(diào)用方法A時(shí),會(huì)發(fā)現(xiàn)方法A中的異常不再導(dǎo)致回滾,也即事務(wù)失效了。
當(dāng)這個(gè)方法被同一個(gè)類(lèi)調(diào)用的時(shí)候,spring無(wú)法將這個(gè)方法加到事務(wù)管理中。
我們來(lái)看一下生效時(shí)候和不生效時(shí)候調(diào)用堆棧日志的對(duì)比。
通過(guò)對(duì)比兩個(gè)調(diào)用堆棧可以看出,spring的@Transactional事務(wù)生效的一個(gè)前提是進(jìn)行方法調(diào)用前經(jīng)過(guò)攔截器TransactionInterceptor,也就是說(shuō)只有通過(guò)TransactionInterceptor攔截器的方法才會(huì)被加入到spring事務(wù)管理中,查看spring源碼可以看到,在AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice方法中會(huì)從調(diào)用方法中獲取@Transactional注解,如果有該注解,則啟用事務(wù),否則不啟用。
這個(gè)方法是通過(guò)spring的AOP類(lèi)CglibAopProxy的內(nèi)部類(lèi)DynamicAdvisedInterceptor調(diào)用的,而DynamicAdvisedInterceptor繼承了MethodInterceptor,用于攔截方法調(diào)用,并從中獲取調(diào)用鏈。
如果是在同一個(gè)類(lèi)中的方法調(diào)用,則不會(huì)被方法攔截器攔截到,因此事務(wù)不會(huì)起作用,必須將方法放入另一個(gè)類(lèi),并且該類(lèi)通過(guò)spring注入。
原因:
Transactional是Spring提供的事務(wù)管理注解。
重點(diǎn)在于,Spring采用動(dòng)態(tài)代理(AOP)實(shí)現(xiàn)對(duì)bean的管理和切片,它為我們的每個(gè)class生成一個(gè)代理對(duì)象。只有在代理對(duì)象之間進(jìn)行調(diào)用時(shí),可以觸發(fā)切面邏輯。
而在同一個(gè)class中,方法B調(diào)用方法A,調(diào)用的是原對(duì)象的方法,而不通過(guò)代理對(duì)象。所以Spring無(wú)法切到這次調(diào)用,也就無(wú)法通過(guò)注解保證事務(wù)性了。
也就是說(shuō),在同一個(gè)類(lèi)中的方法調(diào)用,則不會(huì)被方法攔截器攔截到,因此事務(wù)不會(huì)起作用。
解決方法1:
將事務(wù)方法放到另一個(gè)類(lèi)中(或者單獨(dú)開(kāi)啟一層,取名“事務(wù)層”)進(jìn)行調(diào)用,即符合了在對(duì)象之間調(diào)用的條件。
解決方法2:
獲取本對(duì)象的代理對(duì)象,再進(jìn)行調(diào)用。具體操作如:
Spring-content.xml上下文中,增加配置:<aop:aspectj-autoproxy expose-proxy=“true”/>
在xxxServiceImpl中,用(xxxService)(AopContext.currentProxy()),獲取到xxxService的代理類(lèi),再調(diào)用事務(wù)方法,強(qiáng)行經(jīng)過(guò)代理類(lèi),激活事務(wù)切面。
解決方法3:
很多時(shí)候,方法內(nèi)調(diào)用又希望激活事務(wù),是由于同一個(gè)方法既有DAO操作又有I/O等耗時(shí)操作,不想讓耗時(shí)的I/O造成事務(wù)的太長(zhǎng)耗時(shí)(比如新增商品同時(shí)需要寫(xiě)入庫(kù)存)。此時(shí),可以將I/O做成異步操作(如加入線程池),而加入線程池的操作即便加入事務(wù)也不會(huì)導(dǎo)致事務(wù)太長(zhǎng),問(wèn)題可以迎刃而解。
解決方法4:
用@Autowired 注入自己 然后在用注入的bean調(diào)用自己的方法也可以
參考:
https://blog.csdn.net/ligeforrent/article/details/79996797
https://www.jianshu.com/p/2e4e1007edf2
總結(jié)
以上是生活随笔為你收集整理的@Transactional注解的失效场景的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: spring源码阅读--@Transac
- 下一篇: erlang精要(10)-erl(2)