1. 程式人生 > 遊戲 >《消逝的光芒2》4分鐘實機視訊展示舊主機畫面表現

《消逝的光芒2》4分鐘實機視訊展示舊主機畫面表現

聊聊spring事務失效的12種場景,太坑了

前言

對於從事java開發工作的同學來說,spring的事務肯定再熟悉不過了。

在某些業務場景下,如果一個請求中,需要同時寫入多張表的資料。為了保證操作的原子性(要麼同時成功,要麼同時失敗),避免資料不一致的情況,我們一般都會用到spring事務。

確實,spring事務用起來賊爽,就用一個簡單的註解:@Transactional,就能輕鬆搞定事務。我猜大部分小夥伴也是這樣用的,而且一直用一直爽。

但如果你使用不當,它也會坑你於無形。

今天我們就一起聊聊,事務失效的一些場景,說不定你已經中招了。不信,讓我們一起看看。

一 事務不生效

1.訪問許可權問題

眾所周知,java的訪問許可權主要有四種:private、default、protected、public,它們的許可權從左到右,依次變大。

但如果我們在開發過程中,把有某些事務方法,定義了錯誤的訪問許可權,就會導致事務功能出問題,例如:

@Service
publicclassUserService{

@Transactional
privatevoidadd(UserModeluserModel){
saveData(userModel);
updateData(userModel);
}
}

我們可以看到add方法的訪問許可權被定義成了private,這樣會導致事務失效,spring要求被代理方法必須是public

的。

說白了,在AbstractFallbackTransactionAttributeSource類的computeTransactionAttribute方法中有個判斷,如果目標方法不是public,則TransactionAttribute返回null,即不支援事務。

protectedTransactionAttributecomputeTransactionAttribute(Methodmethod,@NullableClass<?>targetClass){
//Don'tallowno-publicmethodsasrequired.
if(allowPublicMethodsOnly()&&!Modifier.isPublic(method.getModifiers())){
returnnull;
}

//Themethodmaybeonaninterface,butweneedattributesfromthetargetclass.
//Ifthetargetclassisnull,themethodwillbeunchanged.
MethodspecificMethod=AopUtils.getMostSpecificMethod(method,targetClass);

//Firsttryisthemethodinthetargetclass.
TransactionAttributetxAttr=findTransactionAttribute(specificMethod);
if(txAttr!=null){
returntxAttr;
}

//Secondtryisthetransactionattributeonthetargetclass.
txAttr=findTransactionAttribute(specificMethod.getDeclaringClass());
if(txAttr!=null&&ClassUtils.isUserLevelMethod(method)){
returntxAttr;
}

if(specificMethod!=method){
//Fallbackistolookattheoriginalmethod.
txAttr=findTransactionAttribute(method);
if(txAttr!=null){
returntxAttr;
}
//Lastfallbackistheclassoftheoriginalmethod.
txAttr=findTransactionAttribute(method.getDeclaringClass());
if(txAttr!=null&&ClassUtils.isUserLevelMethod(method)){
returntxAttr;
}
}
returnnull;
}

也就是說,如果我們自定義的事務方法(即目標方法),它的訪問許可權不是public,而是private、default或protected的話,spring則不會提供事務功能。

2. 方法用final修飾

有時候,某個方法不想被子類重新,這時可以將該方法定義成final的。普通方法這樣定義是沒問題的,但如果將事務方法定義成final,例如:

@Service
publicclassUserService{

@Transactional
publicfinalvoidadd(UserModeluserModel){
saveData(userModel);
updateData(userModel);
}
}

我們可以看到add方法被定義成了final的,這樣會導致事務失效。

為什麼?

如果你看過spring事務的原始碼,可能會知道spring事務底層使用了aop,也就是通過jdk動態代理或者cglib,幫我們生成了代理類,在代理類中實現的事務功能。

但如果某個方法用final修飾了,那麼在它的代理類中,就無法重寫該方法,而新增事務功能。

注意:如果某個方法是static的,同樣無法通過動態代理,變成事務方法。

3.方法內部呼叫

有時候我們需要在某個Service類的某個方法中,呼叫另外一個事務方法,比如:

@Service
publicclassUserService{

@Autowired
privateUserMapperuserMapper;

publicvoidadd(UserModeluserModel){
userMapper.insertUser(userModel);
updateStatus(userModel);
}

@Transactional
publicvoidupdateStatus(UserModeluserModel){
doSameThing();
}
}

我們看到在事務方法add中,直接呼叫事務方法updateStatus。從前面介紹的內容可以知道,updateStatus方法擁有事務的能力是因為spring aop生成代理了物件,但是這種方法直接呼叫了this物件的方法,所以updateStatus方法不會生成事務。

由此可見,在同一個類中的方法直接內部呼叫,會導致事務失效。

那麼問題來了,如果有些場景,確實想在同一個類的某個方法中,呼叫它自己的另外一個方法,該怎麼辦呢?

3.1 新加一個Service方法

這個方法非常簡單,只需要新加一個Service方法,把@Transactional註解加到新Service方法上,把需要事務執行的程式碼移到新方法中。具體程式碼如下:

@Servcie
publicclassServiceA{
@Autowired
prvateServiceBserviceB;

publicvoidsave(Useruser){
queryData1();
queryData2();
serviceB.doSave(user);
}
}

@Servcie
publicclassServiceB{

@Transactional(rollbackFor=Exception.class)
publicvoiddoSave(Useruser){
addData1();
updateData2();
}

}

3.2 在該Service類中注入自己

如果不想再新加一個Service類,在該Service類中注入自己也是一種選擇。具體程式碼如下:

@Servcie
publicclassServiceA{
@Autowired
prvateServiceAserviceA;

publicvoidsave(Useruser){
queryData1();
queryData2();
serviceA.doSave(user);
}

@Transactional(rollbackFor=Exception.class)
publicvoiddoSave(Useruser){
addData1();
updateData2();
}
}

可能有些人可能會有這樣的疑問:這種做法會不會出現迴圈依賴問題?

答案:不會。

其實spring ioc內部的三級快取保證了它,不會出現迴圈依賴問題。

3.3 通過AopContent類

在該Service類中使用AopContext.currentProxy()獲取代理物件

上面的方法2確實可以解決問題,但是程式碼看起來並不直觀,還可以通過在該Service類中使用AOPProxy獲取代理物件,實現相同的功能。具體程式碼如下:

@Servcie
publicclassServiceA{

publicvoidsave(Useruser){
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}

@Transactional(rollbackFor=Exception.class)
publicvoiddoSave(Useruser){
addData1();
updateData2();
}
}

4.未被spring管理

在我們平時開發過程中,有個細節很容易被忽略。即使用spring事務的前提是:物件要被spring管理,需要建立bean例項。

通常情況下,我們通過@Controller、@Service、@Component、@Repository等註解,可以自動實現bean例項化和依賴注入的功能。

如果有一天,你匆匆忙忙的開發了一個Service類,但忘了加@Service註解,比如:

//@Service
publicclassUserService{

@Transactional
publicvoidadd(UserModeluserModel){
saveData(userModel);
updateData(userModel);
}
}

從上面的例子,我們可以看到UserService類沒有加@Service註解,那麼該類不會交給spring管理,所以它的add方法也不會生成事務。

5.多執行緒呼叫

在實際專案開發中,多執行緒的使用場景還是挺多的。如果spring事務用在多執行緒場景中,會有問題嗎?

@Slf4j
@Service
publicclassUserService{

@Autowired
privateUserMapperuserMapper;
@Autowired
privateRoleServiceroleService;

@Transactional
publicvoidadd(UserModeluserModel)throwsException{
userMapper.insertUser(userModel);
newThread(()->{
roleService.doOtherThing();
}).start();
}
}

@Service
publicclassRoleService{

@Transactional
publicvoiddoOtherThing(){
System.out.println("儲存role表資料");
}
}

從上面的例子中,我們可以看到事務方法add中,呼叫了事務方法doOtherThing,但是事務方法doOtherThing是在另外一個執行緒中呼叫的。

這樣會導致兩個方法不在同一個執行緒中,獲取到的資料庫連線不一樣,從而是兩個不同的事務。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。

如果看過spring事務原始碼的朋友,可能會知道spring的事務是通過資料庫連線來實現的。當前執行緒中儲存了一個map,key是資料來源,value是資料庫連線。

privatestaticfinalThreadLocal<Map<Object,Object>>resources=

newNamedThreadLocal<>("Transactionalresources");

我們說的同一個事務,其實是指同一個資料庫連線,只有擁有同一個資料庫連線才能同時提交和回滾。如果在不同的執行緒,拿到的資料庫連線肯定是不一樣的,所以是不同的事務。

6.表不支援事務

周所周知,在mysql5之前,預設的資料庫引擎是myisam

它的好處就不用多說了:索引檔案和資料檔案是分開儲存的,對於查多寫少的單表操作,效能比innodb更好。

有些老專案中,可能還在用它。

在建立表的時候,只需要把ENGINE引數設定成MyISAM即可:

CREATETABLE`category`(
`id`bigintNOTNULLAUTO_INCREMENT,
`one_category`varchar(20)COLLATEutf8mb4_binDEFAULTNULL,
`two_category`varchar(20)COLLATEutf8mb4_binDEFAULTNULL,
`three_category`varchar(20)COLLATEutf8mb4_binDEFAULTNULL,
`four_category`varchar(20)COLLATEutf8mb4_binDEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=MyISAMAUTO_INCREMENT=4DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_bin

myisam好用,但有個很致命的問題是:不支援事務

如果只是單表操作還好,不會出現太大的問題。但如果需要跨多張表操作,由於其不支援事務,資料極有可能會出現不完整的情況。

此外,myisam還不支援行鎖和外來鍵。

所以在實際業務場景中,myisam使用的並不多。在mysql5以後,myisam已經逐漸退出了歷史的舞臺,取而代之的是innodb。

有時候我們在開發的過程中,發現某張表的事務一直都沒有生效,那不一定是spring事務的鍋,最好確認一下你使用的那張表,是否支援事務。

7.未開啟事務

有時候,事務沒有生效的根本原因是沒有開啟事務。

你看到這句話可能會覺得好笑。

開啟事務不是一個專案中,最最最基本的功能嗎?

為什麼還會沒有開啟事務?

沒錯,如果專案已經搭建好了,事務功能肯定是有的。

但如果你是在搭建專案demo的時候,只有一張表,而這張表的事務沒有生效。那麼會是什麼原因造成的呢?

當然原因有很多,但沒有開啟事務,這個原因極其容易被忽略。

如果你使用的是springboot專案,那麼你很幸運。因為springboot通過DataSourceTransactionManagerAutoConfiguration類,已經默默的幫你開啟了事務。

你所要做的事情很簡單,只需要配置spring.datasource相關引數即可。

但如果你使用的還是傳統的spring專案,則需要在applicationContext.xml檔案中,手動配置事務相關引數。如果忘了配置,事務肯定是不會生效的。

具體配置如下資訊:


<!--配置事務管理器-->
<beanclass="org.springframework.jdbc.datasource.DataSourceTransactionManager"id="transactionManager">
<propertyname="dataSource"ref="dataSource"></property>
</bean>
<tx:adviceid="advice"transaction-manager="transactionManager">
<tx:attributes>
<tx:methodname="*"propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--用切點把事務切進去-->
<aop:config>
<aop:pointcutexpression="execution(*com.susan.*.*(..))"id="pointcut"/>
<aop:advisoradvice-ref="advice"pointcut-ref="pointcut"/>
</aop:config>

默默的說一句,如果在pointcut標籤中的切入點匹配規則,配錯了的話,有些類的事務也不會生效。

二 事務不回滾

1.錯誤的傳播特性

其實,我們在使用@Transactional註解時,是可以指定propagation引數的。

該引數的作用是指定事務的傳播特性,spring目前支援7種傳播特性:

  • REQUIRED如果當前上下文中存在事務,那麼加入該事務,如果不存在事務,建立一個事務,這是預設的傳播屬性值。
  • SUPPORTS如果當前上下文存在事務,則支援事務加入事務,如果不存在事務,則使用非事務的方式執行。
  • MANDATORY如果當前上下文中存在事務,否則丟擲異常。
  • REQUIRES_NEW每次都會新建一個事務,並且同時將上下文中的事務掛起,執行當前新建事務完成以後,上下文事務恢復再執行。
  • NOT_SUPPORTED如果當前上下文中存在事務,則掛起當前事務,然後新的方法在沒有事務的環境中執行。
  • NEVER如果當前上下文中存在事務,則丟擲異常,否則在無事務環境上執行程式碼。
  • NESTED如果當前上下文中存在事務,則巢狀事務執行,如果不存在事務,則新建事務。

如果我們在手動設定propagation引數的時候,把傳播特性設定錯了,比如:

@Service
publicclassUserService{

@Transactional(propagation=Propagation.NEVER)
publicvoidadd(UserModeluserModel){
saveData(userModel);
updateData(userModel);
}
}

我們可以看到add方法的事務傳播特性定義成了Propagation.NEVER,這種型別的傳播特性不支援事務,如果有事務則會拋異常。

目前只有這三種傳播特性才會建立新事務:REQUIRED,REQUIRES_NEW,NESTED。

2.自己吞了異常

事務不會回滾,最常見的問題是:開發者在程式碼中手動try...catch了異常。比如:

@Slf4j
@Service
publicclassUserService{

@Transactional
publicvoidadd(UserModeluserModel){
try{
saveData(userModel);
updateData(userModel);
}catch(Exceptione){
log.error(e.getMessage(),e);
}
}
}

這種情況下spring事務當然不會回滾,因為開發者自己捕獲了異常,又沒有手動丟擲,換句話說就是把異常吞掉了。

如果想要spring事務能夠正常回滾,必須丟擲它能夠處理的異常。如果沒有拋異常,則spring認為程式是正常的。

3.手動拋了別的異常

即使開發者沒有手動捕獲異常,但如果拋的異常不正確,spring事務也不會回滾。

@Slf4j
@Service
publicclassUserService{

@Transactional
publicvoidadd(UserModeluserModel)throwsException{
try{
saveData(userModel);
updateData(userModel);
}catch(Exceptione){
log.error(e.getMessage(),e);
thrownewException(e);
}
}
}

上面的這種情況,開發人員自己捕獲了異常,又手動丟擲了異常:Exception,事務同樣不會回滾。

因為spring事務,預設情況下只會回滾RuntimeException(執行時異常)和Error(錯誤),對於普通的Exception(非執行時異常),它不會回滾。

4.自定義了回滾異常

在使用@Transactional註解宣告事務時,有時我們想自定義回滾的異常,spring也是支援的。可以通過設定rollbackFor引數,來完成這個功能。

但如果這個引數的值設定錯了,就會引出一些莫名其妙的問題,例如:

@Slf4j
@Service
publicclassUserService{

@Transactional(rollbackFor=BusinessException.class)
publicvoidadd(UserModeluserModel)throwsException{
saveData(userModel);
updateData(userModel);
}
}

如果在執行上面這段程式碼,儲存和更新資料時,程式報錯了,拋了SqlException、DuplicateKeyException等異常。而BusinessException是我們自定義的異常,報錯的異常不屬於BusinessException,所以事務也不會回滾。

即使rollbackFor有預設值,但阿里巴巴開發者規範中,還是要求開發者重新指定該引數。

這是為什麼呢?

因為如果使用預設值,一旦程式丟擲了Exception,事務不會回滾,這會出現很大的bug。所以,建議一般情況下,將該引數設定成:Exception或Throwable。

5.巢狀事務回滾多了

publicclassUserService{

@Autowired
privateUserMapperuserMapper;

@Autowired
privateRoleServiceroleService;

@Transactional
publicvoidadd(UserModeluserModel)throwsException{
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}

@Service
publicclassRoleService{

@Transactional(propagation=Propagation.NESTED)
publicvoiddoOtherThing(){
System.out.println("儲存role表資料");
}
}

這種情況使用了巢狀的內部事務,原本是希望呼叫roleService.doOtherThing方法時,如果出現了異常,只回滾doOtherThing方法裡的內容,不回滾 userMapper.insertUser裡的內容,即回滾儲存點。。但事實是,insertUser也回滾了。

why?

因為doOtherThing方法出現了異常,沒有手動捕獲,會繼續往上拋,到外層add方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個事務,不只回滾單個儲存點。

怎麼樣才能只回滾儲存點呢?

@Slf4j
@Service
publicclassUserService{

@Autowired
privateUserMapperuserMapper;

@Autowired
privateRoleServiceroleService;

@Transactional
publicvoidadd(UserModeluserModel)throwsException{

userMapper.insertUser(userModel);
try{
roleService.doOtherThing();
}catch(Exceptione){
log.error(e.getMessage(),e);
}
}
}

可以將內部巢狀事務放在try/catch中,並且不繼續往上拋異常。這樣就能保證,如果內部巢狀事務中出現異常,只回滾內部事務,而不影響外部事務。

三 其他

1 大事務問題

在使用spring事務時,有個讓人非常頭疼的問題,就是大事務問題。

通常情況下,我們會在方法上@Transactional註解,填加事務功能,比如:

@Service
publicclassUserService{

@Autowired
privateRoleServiceroleService;

@Transactional
publicvoidadd(UserModeluserModel)throwsException{
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}

@Service
publicclassRoleService{

@Autowired
privateRoleServiceroleService;

@Transactional
publicvoidsave(UserModeluserModel)throwsException{
query4();
query5();
query6();
saveData(userModel);
}
}

@Transactional註解,如果被加到方法上,有個缺點就是整個方法都包含在事務當中了。

上面的這個例子中,在UserService類中,其實只有這兩行才需要事務:

roleService.save(userModel);
update(userModel);

在RoleService類中,只有這一行需要事務:

saveData(userModel);

現在的這種寫法,會導致所有的query方法也被包含在同一個事務當中。

如果query方法非常多,呼叫層級很深,而且有部分查詢方法比較耗時的話,會造成整個事務非常耗時,而從造成大事務問題。

data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==

2.程式設計式事務

上面聊的這些內容都是基於@Transactional註解的,主要說的是它的事務問題,我們把這種事務叫做:宣告式事務

其實,spring還提供了另外一種建立事務的方式,即通過手動編寫程式碼實現的事務,我們把這種事務叫做:程式設計式事務。例如:


@Autowired
privateTransactionTemplatetransactionTemplate;

...

publicvoidsave(finalUseruser){
queryData1();
queryData2();
transactionTemplate.execute((status)=>{
addData1();
updateData2();
returnBoolean.TRUE;
})
}

在spring中為了支援程式設計式事務,專門提供了一個類:TransactionTemplate,在它的execute方法中,就實現了事務的功能。

相較於@Transactional註解宣告式事務,我更建議大家使用,基於TransactionTemplate的程式設計式事務。主要原因如下:

  1. 避免由於spring aop問題,導致事務失效的問題。
  2. 能夠更小粒度的控制事務的範圍,更直觀。

建議在專案中少使用@Transactional註解開啟事務。但並不是說一定不能用它,如果專案中有些業務邏輯比較簡單,而且不經常變動,使用@Transactional註解開啟事務開啟事務也無妨,因為它更簡單,開發效率更高,但是千萬要小心事務失效的問題。