廣東建設(shè)網(wǎng)工程信息網(wǎng)站無(wú)錫網(wǎng)站建設(shè)seo
一、事務(wù)的概念
百度百科:
事務(wù)(Transaction),一般是指要做的或所做的事情。在計(jì)算機(jī)術(shù)語(yǔ)中是指訪問(wèn)并可能更新數(shù)據(jù)庫(kù)中各種數(shù)據(jù)項(xiàng)的一個(gè)程序執(zhí)
行單元(unit)。事務(wù)通常由高級(jí)數(shù)據(jù)庫(kù)操縱語(yǔ)言或編程語(yǔ)言(如SQL,C++或Java)書(shū)寫(xiě)的用戶程序的執(zhí)行所引起,并用形如
begin transaction和end transaction語(yǔ)句(或函數(shù)調(diào)用)來(lái)界定。事務(wù)由事務(wù)開(kāi)始(begin transaction)和事務(wù)結(jié)
束(end transaction)之間執(zhí)行的全體操作組成。
事務(wù)有四個(gè)特點(diǎn):原子性、一致性、隔離性和持久性。
事務(wù)管理在系統(tǒng)開(kāi)發(fā)中是不可缺少的一部分,Spring提供了很好事務(wù)管理機(jī)制,主要分為編程式事務(wù)和聲明式事務(wù)兩種。
1、編程式事務(wù)
是指在代碼中手動(dòng)的管理事務(wù)的提交、回滾等操作,代碼侵入性比較強(qiáng)。
編程式事務(wù)方式需要開(kāi)發(fā)者在代碼中手動(dòng)的管理事務(wù)的開(kāi)啟、提交、回滾等操作。
public void test() {TransactionDefinition def = new DefaultTransactionDefinition();TransactionStatus status = transactionManager.getTransaction(def);try {// 事務(wù)操作// 事務(wù)提交transactionManager.commit(status);} catch (DataAccessException e) {// 事務(wù)提交transactionManager.rollback(status);throw e;}}
如以上代碼,開(kāi)發(fā)者可以通過(guò)API自己控制事務(wù)。
2、聲明式事務(wù)
基于AOP(面向切面),它將具體業(yè)務(wù)與事務(wù)處理部分解耦,代碼侵入性很低,所以在實(shí)際開(kāi)發(fā)中聲明式事務(wù)用的比較多。聲明式事務(wù)也有兩種實(shí)現(xiàn)方式,一是基于TX和AOP的xml配置文件方式,二種就是基于@Transactional注解了。
聲明式事務(wù)管理方法允許開(kāi)發(fā)者配置的幫助下來(lái)管理事務(wù),而不需要依賴底層API進(jìn)行硬編碼。開(kāi)發(fā)者可以只使用注解或基于配置的 XML 來(lái)管理事務(wù)。
@Transactional@GetMapping("/myTest")public void test() {Student student = new Student();int insert = myMapper.insert(student);}
聲明式事務(wù)對(duì)代碼沒(méi)有侵入性,方法內(nèi)只需要寫(xiě)業(yè)務(wù)邏輯就可以了,幫助我們節(jié)省了很多代碼,他會(huì)自動(dòng)幫我們進(jìn)行事務(wù)的開(kāi)啟、提交以及回滾等操作,把程序員從事務(wù)管理中解放出來(lái)??梢哉f(shuō)優(yōu)點(diǎn)很明顯,但是這種方式的缺點(diǎn)也很明顯。
問(wèn)題一:聲明式事務(wù)的粒度問(wèn)題
首先,聲明式事務(wù)有一個(gè)局限,那就是他的最小粒度要作用在方法上。
也就是說(shuō),如果想要給一部分代碼塊增加事務(wù)的話,那就需要把這個(gè)部分代碼塊單獨(dú)獨(dú)立出來(lái)作為一個(gè)方法。
在大事務(wù)耗時(shí)過(guò)長(zhǎng)需要高并發(fā)優(yōu)化的情況下不建議使用聲明式事務(wù),建議采用上面提到的編程式事務(wù)方式。
問(wèn)題二:聲明式事務(wù)容易被開(kāi)發(fā)者忽略
而事務(wù)一旦被忽略就容易造成很多故障
首先,如果開(kāi)發(fā)者沒(méi)有注意到一個(gè)方法是被事務(wù)嵌套的,那么就可能會(huì)再方法中加入一些如RPC遠(yuǎn)程調(diào)用、消息發(fā)送、緩存更新、文件寫(xiě)入等操作。
我們知道,這些操作如果被包在事務(wù)中,有兩個(gè)問(wèn)題:
1、這些操作自身是無(wú)法回滾的,這就會(huì)導(dǎo)致數(shù)據(jù)的不一致??赡躌PC調(diào)用成功了,但是本地事務(wù)回滾了,可是PRC調(diào)用無(wú)法回滾了(這里不討論分布式事務(wù))。
2、在事務(wù)中有遠(yuǎn)程調(diào)用,就會(huì)拉長(zhǎng)整個(gè)事務(wù)。那么久會(huì)導(dǎo)致本事務(wù)的數(shù)據(jù)庫(kù)連接一直被占用,那么如果類似操作過(guò)多,就會(huì)導(dǎo)致數(shù)據(jù)庫(kù)連接池耗盡。
有些時(shí)候,即使沒(méi)有在事務(wù)中進(jìn)行遠(yuǎn)程操作,但是有些人還是可能會(huì)不經(jīng)意的進(jìn)行一些內(nèi)存操作,如運(yùn)算?;蛘呷绻龅椒謳?kù)分表的情況,有可能不經(jīng)意間進(jìn)行跨庫(kù)操作。
問(wèn)題三:聲明式事務(wù)用不對(duì)在某些場(chǎng)景下容易失效
這個(gè)問(wèn)題后面我們著重講述一下失效的幾種場(chǎng)景。
下面我們就講講聲明式事務(wù)使用的@Transactional注解。
二、@Transactional介紹
1、@Transactional 可以作用在接口、類、類方法
作用于類:當(dāng)把@Transactional 注解放在類上時(shí),表示所有該類的public方法都配置相同的事務(wù)屬性信息。
作用于方法:當(dāng)類配置了@Transactional,方法也配置了@Transactional,方法的事務(wù)會(huì)覆蓋類的事務(wù)配置信息。
作用于接口:不推薦這種使用方法,因?yàn)橐坏?biāo)注在Interface上并且配置了Spring AOP 使用CGLib動(dòng)態(tài)代理,將會(huì)導(dǎo)致@Transactional注解失效。
@Transactional@RestController@RequestMappingpublic class MyController {@Autowiredprivate StudentMapper studentMapper;@Transactional(rollbackFor = Exception.class)@GetMapping("/test")public void test() throws Exception { Student student = new Student();student.setName("chamption-Dai")int insert = studentMapper.insert(cityInfoDict);}
}
2、@Transactional注有哪些屬性?
屬性 | 類型 | 描述 |
---|---|---|
value | String | 可選的限定描述符,指定使用的事務(wù)管理器 |
propagation | enum: Propagation | 可選的事務(wù)傳播行為設(shè)置 |
isolation | enum: Isolation | 可選的事務(wù)隔離級(jí)別設(shè)置 |
readOnly | boolean | 讀寫(xiě)或只讀事務(wù),默認(rèn)讀寫(xiě) |
timeout | int (in seconds granularity) | 事務(wù)超時(shí)時(shí)間設(shè)置 |
rollbackFor | Class對(duì)象數(shù)組,必須繼承自Throwable | 導(dǎo)致事務(wù)回滾的異常類數(shù)組 |
rollbackForClassName | 類名數(shù)組,必須繼承自Throwable | 導(dǎo)致事務(wù)回滾的異常類名字?jǐn)?shù)組 |
noRollbackFor | Class對(duì)象數(shù)組,必須繼承自Throwable | 不會(huì)導(dǎo)致事務(wù)回滾的異常類數(shù)組 |
noRollbackForClassName | 類名數(shù)組,必須繼承自Throwable | 不會(huì)導(dǎo)致事務(wù)回滾的異常類名字?jǐn)?shù)組 |
propagation屬性
propagation 代表事務(wù)的傳播行為,默認(rèn)值為 Propagation.REQUIRED,其他的屬性信息如下:
屬性 | 說(shuō)明 |
---|---|
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)類A中的 a 方法用默認(rèn)Propagation.REQUIRED模式,類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。
Isolation.DEFAULT:使用底層數(shù)據(jù)庫(kù)默認(rèn)的隔離級(jí)別。
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
timeout 屬性
timeout :事務(wù)的超時(shí)時(shí)間,默認(rèn)值為 -1。如果超過(guò)該時(shí)間限制但事務(wù)還沒(méi)有完成,則自動(dòng)回滾事務(wù)。
示例:
@Service
public class UserServiceImpl implements UserService{// 加載mapper
@Autowired
private UserMapper usermapper;@Transactional(propagation=Propagation.REQUIRED,timeout=5) // 啟用事務(wù)管理
public void myTransaction(User addUser, User updateuser) {System.out.println("---- 方法開(kāi)始 ");usermapper.add(addUser);try {Thread.sleep(6000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}usermapper.update(updateuser);System.out.println("----方法結(jié)束 ");
}
}
運(yùn)行結(jié)果:
最終,由于事務(wù)超時(shí),事務(wù)回滾。
運(yùn)行的時(shí)候,由于事務(wù)超時(shí),就會(huì)報(bào)錯(cuò) TransactionTimedOutException:…
readOnly 屬性
readOnly :指定事務(wù)是否為只讀事務(wù),默認(rèn)值為 false;為了忽略那些不需要事務(wù)的方法,比如讀取數(shù)據(jù),可以設(shè)置 read-only 為 true。
rollbackFor 屬性
rollbackFor :用于指定能夠觸發(fā)事務(wù)回滾的異常類型,可以指定多個(gè)異常類型。
noRollbackFor屬性
noRollbackFor:拋出指定的異常類型,不回滾事務(wù),也可以指定多個(gè)異常類型。
三、@Transactional失效場(chǎng)景
接下來(lái)我們結(jié)合具體的代碼分析一下哪些場(chǎng)景下,@Transactional 注解會(huì)失效。
1.@Transactional 應(yīng)用在非 public 修飾的方法上
如果Transactional注解應(yīng)用在非public 修飾的方法上,Transactional將會(huì)失效。
圖片來(lái)自“程序員那點(diǎn)事”
之所以會(huì)失效是因?yàn)樵赟pring AOP 代理時(shí),如上圖所示 TransactionInterceptor (事務(wù)攔截器)在目標(biāo)方法執(zhí)行前后進(jìn)行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內(nèi)部類)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會(huì)間接調(diào)用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,獲取Transactional 注解的事務(wù)配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method,Class<?> targetClass) {// Don't allow no-public methods as required.if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;
}
此方法會(huì)檢查目標(biāo)方法的修飾符是否為 public,不是 public則不會(huì)獲取@Transactional 的屬性配置信息。
注意:protected、private 修飾的方法上使用 @Transactional 注解,雖然事務(wù)無(wú)效,但不會(huì)有任何報(bào)錯(cuò),這是我們很容犯錯(cuò)的一點(diǎn)。
2、@Transactional 注解屬性 propagation 設(shè)置錯(cuò)誤
這種失效是由于配置錯(cuò)誤,若是錯(cuò)誤的配置以下三種 propagation,事務(wù)將不會(huì)發(fā)生回滾。
- 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ù),則拋出異常。
3、@Transactional 注解屬性 rollbackFor 設(shè)置錯(cuò)誤
rollbackFor 可以指定能夠觸發(fā)事務(wù)回滾的異常類型。Spring默認(rèn)拋出了未檢查unchecked異常(繼承自 RuntimeException的異常)或者 Error才回滾事務(wù);其他異常不會(huì)觸發(fā)回滾事務(wù)。如果在事務(wù)中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定 rollbackFor屬性。
// 希望自定義的異常可以進(jìn)行回滾
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
若在目標(biāo)方法中拋出的異常是 rollbackFor 指定的異常的子類,事務(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);
}
4、同一個(gè)類中方法調(diào)用,導(dǎo)致@Transactional失效、
開(kāi)發(fā)中避免不了會(huì)對(duì)同一個(gè)類里面的方法調(diào)用,比如有一個(gè)類Test,它的一個(gè)方法A,A再調(diào)用本類的方法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)前類以外的代碼調(diào)用時(shí),才會(huì)由Spring生成的代理對(duì)象來(lái)管理。
//@Transactional 去掉事務(wù)@GetMapping("/test")private Integer A() throws Exception {CityInfoDict cityInfoDict = new CityInfoDict();cityInfoDict.setCityName("2");/*** B 插入字段為 3的數(shù)據(jù)*/this.insertB();/*** A 插入字段為 2的數(shù)據(jù)*/int insert = cityInfoDictMapper.insert(cityInfoDict);return insert;}@Transactional()public Integer insertB() throws Exception {CityInfoDict cityInfoDict = new CityInfoDict();cityInfoDict.setCityName("3");cityInfoDict.setParentCityId(3);return cityInfoDictMapper.insert(cityInfoDict);}
5、異常被你的 catch“吃了”導(dǎo)致@Transactional失效
這種情況是最常見(jiàn)的一種@Transactional注解失效場(chǎng)景
1 @Transactional2 private Integer A() throws Exception {3 int insert = 0;4 try {5 CityInfoDict cityInfoDict = new CityInfoDict();6 cityInfoDict.setCityName("Champtional-Dai");7 cityInfoDict.setParentCityId(1111);8 /**9 * A 插入字段為 2的數(shù)據(jù)
10 */
11 insert = cityInfoDictMapper.insert(cityInfoDict);
12 /**
13 * B 插入字段為 3的數(shù)據(jù)
14 */
15 b.insertB();
16 } catch (Exception e) {
17 e.printStackTrace();
18 }
19 }
問(wèn):如果B方法內(nèi)部拋了異常,而A方法此時(shí)try catch了B方法的異常,那這個(gè)事務(wù)還能正?;貪L嗎?
答案:不能!
會(huì)拋出異常:
org.springframework.transaction.UnexpectedRollbackException:
Transaction rolled back because it has been marked as rollback-only
因?yàn)楫?dāng)ServiceB中拋出了一個(gè)異常以后,ServiceB標(biāo)識(shí)當(dāng)前事務(wù)需要rollback。但是ServiceA中由于你手動(dòng)的捕獲這個(gè)異常并進(jìn)行處理,ServiceA認(rèn)為當(dāng)前事務(wù)應(yīng)該正常commit。此時(shí)就出現(xiàn)了前后不一致,也就是因?yàn)檫@樣,拋出了前面的UnexpectedRollbackException異常。
spring的事務(wù)是在調(diào)用業(yè)務(wù)方法之前開(kāi)始的,業(yè)務(wù)方法執(zhí)行完畢之后才執(zhí)行commit or rollback,事務(wù)是否執(zhí)行取決于是否拋出runtime異常。如果拋出runtime exception 并在你的業(yè)務(wù)方法中沒(méi)有catch到的話,事務(wù)會(huì)回滾。
在業(yè)務(wù)方法中一般不需要catch異常,如果非要catch一定要手動(dòng)拋出throw new RuntimeException(),否則會(huì)導(dǎo)致事務(wù)失效,數(shù)據(jù)commit造成數(shù)據(jù)不一致,所以有些時(shí)候try catch反倒會(huì)畫(huà)蛇添足。
6、數(shù)據(jù)庫(kù)引擎不支持事務(wù)
這種情況出現(xiàn)的概率并不高,事務(wù)能否生效數(shù)據(jù)庫(kù)引擎是否支持事務(wù)是關(guān)鍵。常用的MySQL數(shù)據(jù)庫(kù)默認(rèn)使用支持事務(wù)的innodb引擎。一旦數(shù)據(jù)庫(kù)引擎切換成不支持事務(wù)的myisam,那事務(wù)就從根本上失效了。