聊聊Spring事務失效的10種場景熄求,太坑人了

前言

最近在看spring事務源碼,時不時回想起前幾年面試YY的場景逗概,面試官拿出下面的一道面試題問我弟晚,updateStatus方法會不會生成事務。我心想,這么簡單的問題還要問我卿城,這是瞧不起我的水平嗎淑履?但是我仔細看了看,想了想藻雪,這道題還真不容易回答,如果以前沒有特別注意或者研究過狸吞,很容易掉坑里勉耀。還好我當時認真想了想,回答了不會生成事務蹋偏,但后面面試官繼續(xù)追問為什么不產(chǎn)生事務便斥,我就把具體的原因說明了一下,事后回到家我再想了想這個問題威始,發(fā)現(xiàn)我回答的并不是很好枢纠。

@Service

public class UserService {

? ? @Autowired

? ? private UserMapper userMapper;

? ? @Transactional

? ? public void add(UserModel userModel) {

? ? ? ? userMapper.insertUser(userModel);

? ? ? ? updateStatus(userModel);

? ? }

? ? @Transactional

? ? public void updateStatus(UserModel userModel) {

? ? ? ? doSameThing();

? ? }

}

對于從事java開發(fā)工作的同學來說,spring的事務肯定再熟悉不過了黎棠,上述的事務場景相信大家在工作中會遇到過晋渺,但是我們有沒有認真研究呢,有多少人能夠講清楚updateStatus的事務問題呢脓斩,至少我當時回答的就不是很好木西。

確實,spring事務用起來賊爽随静,我們一般就用一個簡單的注解:@Transactional八千,就能輕松搞定事務。我猜大部分小伙伴也是這樣用的燎猛,而且一直用一直爽恋捆。

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

今天我們就一起聊聊沸停,事務失效的一些場景,說不定你已經(jīng)中招了论寨。不信星立,讓我們一起看看。


一葬凳、事務不生效

1.訪問權限問題

眾所周知绰垂,java的訪問權限主要有四種:public、protected火焰、default劲装、private,它們的權限從左到右,依次變小占业。

但如果我們把某些事務方法绒怨,定義了錯誤的訪問權限級別,就會導致事務功能出問題谦疾,例如:

@Service

public class UserService{

???? @Transactional

???? private void add(UserModel userModel){

?????????? saveData(userModel); updateData(userModel);

???? }

}

上面定義add方法的訪問權限為private南蹂,這樣最終會導致事務失效,spring要求被代理方法必須是public的念恍。

想要了解代理方法必須是public的六剥,我們需要看spring事務的源碼,在

AbstractFallbackTransactionAttributeSource類的computeTransactionAttribute方法中有個判斷峰伙,如果目標方法不是public疗疟,則TransactionAttribute返回null,即不支持事務瞳氓。

protected TransactionAttribute computeTransactionAttribute(Method method,@NullableClass targetClass) {

??? // Don't allow no-public methods as required.

??? if(allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {

??????? returnnull;

??? }

??? Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

??? // First try is the method in the target class.

?? TransactionAttribute txAttr = findTransactionAttribute(specificMethod);

?? if(txAttr !=null) {

????? returntxAttr;

?? }

?? ...................................

?? returnnull;

}

因此如果我們自定義方法策彤,它的訪問權限不是public,而是protected匣摘、default或者private店诗,spring則不會提供事務功能。

2. 方法用final修飾

如果某個方法不想被子類重寫恋沃,這時我們可以將該方法定義成final必搞。普通方法這樣定義是沒問題的,但如果將事務方法定義成final囊咏,例如:

@Service

public class UserService{

??? @Transactional

??? public final void add(UserModel userModel){

???????? saveData(userModel);

??????? updateData(userModel);

??? }

}

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

為什么?

我看過spring事務的源碼梅割,就會知道spring事務底層使用了aop霜第,也就是通過jdk動態(tài)代理或者cglib,幫我們生成了代理類户辞,在代理類中實現(xiàn)的事務功能泌类。

但如果某個方法用final修飾了,那么在它的代理類中底燎,就無法重寫該方法刃榨,而無法添加事務功能。

注意:如果某個方法是static的双仍,同樣無法通過動態(tài)代理枢希,變成事務方法。

3.方法內(nèi)部調(diào)用

有時候我們需要在某個Service類的某個方法中朱沃,調(diào)用另外一個事務方法苞轿,比如:

@Service

public class OrderServiceImpl implements OrderService {

?? public void update(Order order) {

????? updateOrder(order);

?? }

?? @Transactional

?? public void updateOrder(Order order) {

?????? // update order

?? }

}

我們看到在事務方法add中茅诱,直接調(diào)用事務方法updateOrder。從前面介紹的內(nèi)容可以知道搬卒,updateOrder方法擁有事務的能力是因為spring

aop生成代理了對象瑟俭,但是這種方法直接調(diào)用了this對象的方法,所以updateOrder方法不會生成事務契邀。

由此可見摆寄,在同一個類中的方法直接內(nèi)部調(diào)用,會導致事務失效坯门。

那么問題來了椭迎,如果有些場景,確實想在同一個類的某個方法中田盈,調(diào)用它自己的另外一個方法,該怎么辦呢?

3.1 新加一個Service方法

這個方法非常簡單缴阎,允瞧、需要新加一個Service方法,把@Transactional注解加到新Service方法上蛮拔,把需要事務執(zhí)行的代碼移到新方法中述暂。具體代碼如下:

@Servcie

public class ServiceA{

??? @Autowired

??? prvate ServiceB serviceB;

??? public void save(User user){

???????? queryData1();

???????? queryData2();

???????? serviceB.doSave(user);

?? }

}

@Servcie

public class ServiceB{

??? @Transactional(rollbackFor=Exception.class)

???? public void doSave(Useruser){

??????? addData1();

??????? updateData2();

??? }

}

3.2 在該Service類中注入自己

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

@Servcie

public class ServiceA{

??? @Autowired

??? private ServiceA serviceA;

??? public void save(User user){

??????? queryData1();

??????? queryData2();

??????? serviceA.doSave(user);

??? }

??? @Transactional(rollbackFor=Exception.class)

??? public void doSave(Useruser){

? ? ? ? addData1();

? ? ? ? updateData2();

??? }

}

可能有些人可能會有這樣的疑問:這種做法會不會出現(xiàn)循環(huán)依賴問題?

答案:不會畦韭。

其實spring ioc內(nèi)部的三級緩存保證了它,不會出現(xiàn)循環(huán)依賴問題肛跌。但有些坑艺配,如果你想進一步了解循環(huán)依賴問題,可以在網(wǎng)上查找spring是如何解決循環(huán)依賴的衍慎。

3.3 通過AopContent類

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

上面的方法2確實可以解決問題转唉,但是代碼看起來并不直觀,還可以通過在該Service類中使用AOPProxy獲取代理對象稳捆,實現(xiàn)相同的功能赠法。具體代碼如下:

@Servcie

public class ServiceA{

?? public void save(User user){

?????? queryData1();

?????? queryData2();

? ? ? ? ((ServiceA)AopContext.currentProxy()).doSave(user);

?? }

?? @Transactional(rollbackFor=Exception.class)

?? public void doSave(Useruser){

?????? addData1();

????? updateData2();

??? }

}

4.未被spring容器管理

在我們平時開發(fā)過程中,有個細節(jié)很容易被忽略乔夯。即使用spring事務的前提是:對象要被spring管理砖织,需要創(chuàng)建bean實例。

通常情況下末荐,我們通過@Controller侧纯、@Service、@Component鞠评、@Repository等注解茂蚓,可以自動實現(xiàn)bean實例化和依賴注入的功能。

// @Service

public class OrderServiceImpl implements OrderService {

?? @Transactional

?? public void updateOrder(Order order) {

????? // update order

?? }

}

從上面的例子,如果此時把 @Service 注解注釋掉聋涨,這個類就不會被加載成一個 Bean晾浴,那這個類就不會被 Spring 管理了,事務自然就失效了牍白。

5.多線程調(diào)用

在實際項目開發(fā)中脊凰,多線程的使用場景還是挺多的。如果spring事務用在多線程場景中茂腥,會有問題嗎?

@Slf4j

@Service

public class UserService{

?? @Autowired

?? private UserMapper userMapper;

?? @Autowired

?? private RoleService roleService;

?? @Transactional

?? public void add(UserModel userModel)throwsException{

????? userMapper.insertUser(userModel);

????? newThread(() -> {

????????? roleService.doOtherThing();

????? }).start();

?? }

}

@Service

public class RoleService{

??? @Transactional

???? public void doOtherThing(){

???????? System.out.println("保存role表數(shù)據(jù)");

???? }

}

從上面的例子中狸涌,我們可以看到事務方法add中,調(diào)用了事務方法doOtherThing最岗,但是事務方法doOtherThing是在另外一個線程中調(diào)用的帕胆。

這樣會導致兩個方法不在同一個線程中,獲取到的數(shù)據(jù)庫連接不一樣般渡,從而是兩個不同的事務懒豹。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的驯用。

如果看過spring事務源碼的朋友脸秽,可能會知道spring的事務是通過數(shù)據(jù)庫連接來實現(xiàn)的。當前線程中保存了一個map蝴乔,key是數(shù)據(jù)源记餐,value是數(shù)據(jù)庫連接。

privatestaticfinal ThreadLocal> resources =newNamedThreadLocal<>("Transactional resources");

我們說的同一個事務薇正,其實是指同一個數(shù)據(jù)庫連接片酝,只有擁有同一個數(shù)據(jù)庫連接才能同時提交和回滾。如果在不同的線程挖腰,拿到的數(shù)據(jù)庫連接肯定是不一樣的钠怯,所以是不同的事務。

二曙聂、事務不回滾

1.錯誤的傳播特性

其實晦炊,我們在使用@Transactional注解時,是可以指定propagation參數(shù)的,擴展其配置不支持事務

@Service

publicclass OrderServiceImpl implements OrderService{

? ?@Transactional

?? public void update(Order order) {

?????? updateOrder(order);

??? }

? ? @Transactional(propagation = Propagation.NOT_SUPPORTED)

??? public void updateOrder(Order order) {

???????? // update order

???? }

}

我們可以看到add方法的事務傳播特性定義成了Propagation.NOT_SUPPORTED宁脊,這種類型的傳播特性不支持事務断国,如果有事務則會拋異常。

目前只有這三種傳播特性才會創(chuàng)建新事務:NESTED,REQUIRES_NEW,REQUIRED榆苞。

2.自己吞了異常

這個也是出現(xiàn)比較多的場景:把異常吃了稳衬,然后又不拋出來,事務也不會回滾坐漏!比如:

@Service

public class OrderServiceImpl implements OrderService {

??? @Transactional

??? public void updateOrder(Order order) {

??????? try {

?????????? // update order

??????? } catch {

?????? }

? }

}

3.手動拋了別的異常

即使開發(fā)者沒有手動捕獲異常薄疚,但如果拋的異常不正確碧信,spring事務也不會回滾。

@Service

public class OrderServiceImpl implements OrderService {

??? @Transactional

??? public void updateOrder(Order order) {

?????? try {

????????? // update order

????? } catch {

????????? throw new Exception("更新錯誤");

????? }

? }

}

這樣事務也是不生效的街夭,因為默認回滾的是:RuntimeException砰碴,如果你想觸發(fā)其他異常的回滾,需要在注解上配置一下板丽,如:@Transactional(rollbackFor= Exception.class), 這個配置僅限于 Throwable 異常類及其子類呈枉。

4.自定義了回滾異常

在使用@Transactional注解聲明事務時,有時我們想自定義回滾的異常埃碱,spring也是支持的猖辫。可以通過設置rollbackFor參數(shù)砚殿,來完成這個功能啃憎。

但如果這個參數(shù)的值設置錯了,就會引出一些莫名其妙的問題似炎,例如:

@Slf4j

@Service

public class UserService {

???? @Transactional(rollbackFor = BusinessException.class)

????? public void add(UserModel userModel) throws Exception {

????????? saveData(userModel);

????????? updateData(userModel);

????? }

}

如果在執(zhí)行上面這段代碼荧飞,保存和更新數(shù)據(jù)時,程序報錯了名党,拋了SqlException、DuplicateKeyException等異常挠轴。而BusinessException是我們自定義的異常传睹,報錯的異常不屬于BusinessException,所以事務也不會回滾岸晦。

即使rollbackFor有默認值欧啤,但阿里巴巴開發(fā)者規(guī)范中,還是要求開發(fā)者重新指定該參數(shù)启上。

這是為什么呢?

因為如果使用默認值邢隧,一旦程序拋出了Exception,事務不會回滾冈在,這會出現(xiàn)很大的bug倒慧。所以,建議一般情況下包券,將該參數(shù)設置成:Exception或Throwable纫谅。

5.嵌套事務回滾多了

public class UserService{

??? @Autowired

??? private UserMapper userMapper;

?? @Autowired

?? private RoleService roleService;

?? @Transactional

?? public void add(UserModel userModel) throwsException{

????? userMapper.insertUser(userModel);

????? roleService.doOtherThing();

? }

}

@Service

public class RoleService{

??? @Transactional(propagation = Propagation.NESTED)

??? public void doOtherThing(){

?????? System.out.println("保存role表數(shù)據(jù)");

?? }

}

這種情況使用了嵌套的內(nèi)部事務,原本是希望調(diào)用roleService.doOtherThing方法時溅固,如果出現(xiàn)了異常付秕,只回滾doOtherThing方法里的內(nèi)容,不回滾

userMapper.insertUser里的內(nèi)容侍郭,即回滾保存點询吴。但事實是掠河,insertUser也回滾了。

why?

因為doOtherThing方法出現(xiàn)了異常猛计,沒有手動捕獲唠摹,會繼續(xù)往上拋,到外層add方法的代理方法中捕獲了異常有滑。所以跃闹,這種情況是直接回滾了整個事務,不只回滾單個保存點毛好。

怎么樣才能只回滾保存點呢?

@Slf4j

@Service

public class UserService {

?? @Autowired

?? private UserMapper userMapper;

?? @Autowired

??? private RoleService roleService;

??? @Transactional

??? public void add(UserModel userModel) throws Exception {

??????? userMapper.insertUser(userModel);

??? try{

???? ?? roleService.doOtherThing();

??? }catch(Exception e) {

????? log.error(e.getMessage(), e);

?? }

?}

}

可以將內(nèi)部嵌套事務放在try/catch中望艺,并且不繼續(xù)往上拋異常。這樣就能保證肌访,如果內(nèi)部嵌套事務中出現(xiàn)異常找默,只回滾內(nèi)部事務,而不影響外部事務吼驶。

沐子總結(jié)了10種事務失效的場景惩激,而在我們工作中經(jīng)常發(fā)生的場景是方法內(nèi)部調(diào)用、自已吞了異常蟹演、手動拋出了別的異常风钻,大家可以著重關注這三種場景,多看幾遍酒请。最后骡技,如果我的文章對你有所幫助或者有所啟發(fā),歡迎關注公眾號(微信搜索公眾號:首席架構(gòu)師專欄)羞反,里面有許多技術干貨布朦,也有我對技術的思考和感悟,還有作為架構(gòu)師的驗驗分享昼窗;關注后回復 【面試題】是趴,有我準備的面試題、架構(gòu)師大型項目實戰(zhàn)視頻等福利 澄惊, 小編會帶著你一起學習唆途、成長,讓我們一起加油5>焦!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末亭敢,一起剝皮案震驚了整個濱河市滚婉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌帅刀,老刑警劉巖让腹,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件远剩,死亡現(xiàn)場離奇詭異,居然都是意外死亡骇窍,警方通過查閱死者的電腦和手機瓜晤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腹纳,“玉大人痢掠,你說我怎么就攤上這事〕盎校” “怎么了足画?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佃牛。 經(jīng)常有香客問我淹辞,道長,這世上最難降的妖魔是什么俘侠? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任象缀,我火速辦了婚禮,結(jié)果婚禮上爷速,老公的妹妹穿的比我還像新娘央星。我一直安慰自己,他們只是感情好惫东,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布莉给。 她就那樣靜靜地躺著,像睡著了一般凿蒜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胁黑,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天废封,我揣著相機與錄音,去河邊找鬼丧蘸。 笑死漂洋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的力喷。 我是一名探鬼主播刽漂,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弟孟!你這毒婦竟也來了贝咙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拂募,失蹤者是張志新(化名)和其女友劉穎庭猩,沒想到半個月后窟她,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡蔼水,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年震糖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趴腋。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡吊说,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出优炬,到底是詐尸還是另有隱情颁井,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布穿剖,位于F島的核電站蚤蔓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏糊余。R本人自食惡果不足惜秀又,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贬芥。 院中可真熱鬧吐辙,春花似錦、人聲如沸蘸劈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抱环。三九已至伤哺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棒掠,已是汗流浹背孵构。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烟很,地道東北人颈墅。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像雾袱,于是被迫代替她去往敵國和親恤筛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容