Spring事務(wù)失效的10種場景

最近在看spring事務(wù)源碼具篇,時(shí)不時(shí)回想起前幾年面試YY的場景纬霞,面試官拿出下面的一道面試題問我,updateStatus方法會不會生成事務(wù)驱显。我心想诗芜,這么簡單的問題還要問我,這是瞧不起我的水平嗎埃疫?但是我仔細(xì)看了看伏恐,想了想,這道題還真不容易回答栓霜,如果以前沒有特別注意或者研究過翠桦,很容易掉坑里。還好我當(dāng)時(shí)認(rèn)真想了想叙淌,回答了不會生成事務(wù)秤掌,但后面面試官繼續(xù)追問為什么不產(chǎn)生事務(wù),我就把具體的原因說明了一下鹰霍,事后回到家我再想了想這個(gè)問題,發(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ā)工作的同學(xué)來說茂洒,spring的事務(wù)肯定再熟悉不過了,上述的事務(wù)場景相信大家在工作中會遇到過瓶竭,但是我們有沒有認(rèn)真研究呢督勺,有多少人能夠講清楚updateStatus的事務(wù)問題呢渠羞,至少我當(dāng)時(shí)回答的就不是很好。

確實(shí)智哀,spring事務(wù)用起來賊爽次询,我們一般就用一個(gè)簡單的注解:@Transactional,就能輕松搞定事務(wù)瓷叫。我猜大部分小伙伴也是這樣用的屯吊,而且一直用一直爽。

但如果你使用不當(dāng)摹菠,它也會坑你于無形盒卸。

今天我們就一起聊聊,事務(wù)失效的一些場景次氨,說不定你已經(jīng)中招了蔽介。不信,讓我們一起看看煮寡。

image

一虹蓄、事務(wù)不生效

1.訪問權(quán)限問題

眾所周知,java的訪問權(quán)限主要有四種:public幸撕、protected武花、default、private杈帐,它們的權(quán)限從左到右体箕,依次變小。

但如果我們把某些事務(wù)方法挑童,定義了錯(cuò)誤的訪問權(quán)限級別累铅,就會導(dǎo)致事務(wù)功能出問題,例如:

@Service

public class UserService{

 @Transactional

 private void add(UserModel userModel){

       saveData(userModel); updateData(userModel);

 }

}

上面定義add方法的訪問權(quán)限為private站叼,這樣最終會導(dǎo)致事務(wù)失效娃兽,spring要求被代理方法必須是public的。

想要了解代理方法必須是public的尽楔,我們需要看spring事務(wù)的源碼投储,在

AbstractFallbackTransactionAttributeSource類的computeTransactionAttribute方法中有個(gè)判斷,如果目標(biāo)方法不是public阔馋,則TransactionAttribute返回null玛荞,即不支持事務(wù)。

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;

}

因此如果我們自定義方法呕寝,它的訪問權(quán)限不是public勋眯,而是protected、default或者private,spring則不會提供事務(wù)功能客蹋。

2. 方法用final修飾

如果某個(gè)方法不想被子類重寫塞蹭,這時(shí)我們可以將該方法定義成final。普通方法這樣定義是沒問題的讶坯,但如果將事務(wù)方法定義成final番电,例如:

@Service

public class UserService{

@Transactional

public final void add(UserModel userModel){

     saveData(userModel);

    updateData(userModel);

}

}

我們可以看到add方法被定義成了final的,這樣會導(dǎo)致事務(wù)失效辆琅。

為什么?

我看過spring事務(wù)的源碼漱办,就會知道spring事務(wù)底層使用了aop,也就是通過jdk動態(tài)代理或者cglib涎跨,幫我們生成了代理類洼冻,在代理類中實(shí)現(xiàn)的事務(wù)功能。

但如果某個(gè)方法用final修飾了隅很,那么在它的代理類中撞牢,就無法重寫該方法,而無法添加事務(wù)功能叔营。

注意:如果某個(gè)方法是static的屋彪,同樣無法通過動態(tài)代理,變成事務(wù)方法绒尊。

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

有時(shí)候我們需要在某個(gè)Service類的某個(gè)方法中畜挥,調(diào)用另外一個(gè)事務(wù)方法,比如:

@Service

public class OrderServiceImpl implements OrderService {

public void update(Order order) {

  updateOrder(order);

}

@Transactional

public void updateOrder(Order order) {

   // update order

}

}

我們看到在事務(wù)方法add中婴谱,直接調(diào)用事務(wù)方法updateOrder蟹但。從前面介紹的內(nèi)容可以知道,updateOrder方法擁有事務(wù)的能力是因?yàn)閟pring

aop生成代理了對象谭羔,但是這種方法直接調(diào)用了this對象的方法华糖,所以updateOrder方法不會生成事務(wù)。

由此可見瘟裸,在同一個(gè)類中的方法直接內(nèi)部調(diào)用客叉,會導(dǎo)致事務(wù)失效。

那么問題來了话告,如果有些場景兼搏,確實(shí)想在同一個(gè)類的某個(gè)方法中,調(diào)用它自己的另外一個(gè)方法沙郭,該怎么辦呢?

3.1 新加一個(gè)Service方法

這個(gè)方法非常簡單佛呻,、需要新加一個(gè)Service方法棠绘,把@Transactional注解加到新Service方法上件相,把需要事務(wù)執(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類中注入自己

如果不想再新加一個(gè)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)依賴問題?

答案:不會。

其實(shí)spring ioc內(nèi)部的三級緩存保證了它让虐,不會出現(xiàn)循環(huán)依賴問題紊撕。但有些坑,如果你想進(jìn)一步了解循環(huán)依賴問題赡突,可以在網(wǎng)上查找spring是如何解決循環(huán)依賴的对扶。

3.3 通過AopContent類

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

上面的方法2確實(shí)可以解決問題,但是代碼看起來并不直觀惭缰,還可以通過在該Service類中使用AOPProxy獲取代理對象浪南,實(shí)現(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容器管理

在我們平時(shí)開發(fā)過程中漱受,有個(gè)細(xì)節(jié)很容易被忽略络凿。即使用spring事務(wù)的前提是:對象要被spring管理,需要創(chuàng)建bean實(shí)例昂羡。

通常情況下絮记,我們通過@Controller、@Service虐先、@Component怨愤、@Repository等注解,可以自動實(shí)現(xiàn)bean實(shí)例化和依賴注入的功能蛹批。

// @Service

public class OrderServiceImpl implements OrderService {

@Transactional

public void updateOrder(Order order) {

  // update order

}

}

從上面的例子撰洗,如果此時(shí)把 @Service 注解注釋掉,這個(gè)類就不會被加載成一個(gè) Bean腐芍,那這個(gè)類就不會被 Spring 管理了差导,事務(wù)自然就失效了。

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

在實(shí)際項(xiàng)目開發(fā)中甸赃,多線程的使用場景還是挺多的柿汛。如果spring事務(wù)用在多線程場景中,會有問題嗎?

@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ù)");

 }

}

從上面的例子中埠对,我們可以看到事務(wù)方法add中络断,調(diào)用了事務(wù)方法doOtherThing,但是事務(wù)方法doOtherThing是在另外一個(gè)線程中調(diào)用的项玛。

這樣會導(dǎo)致兩個(gè)方法不在同一個(gè)線程中貌笨,獲取到的數(shù)據(jù)庫連接不一樣,從而是兩個(gè)不同的事務(wù)襟沮。如果想doOtherThing方法中拋了異常锥惋,add方法也回滾是不可能的昌腰。

如果看過spring事務(wù)源碼的朋友,可能會知道spring的事務(wù)是通過數(shù)據(jù)庫連接來實(shí)現(xiàn)的膀跌。當(dāng)前線程中保存了一個(gè)map遭商,key是數(shù)據(jù)源,value是數(shù)據(jù)庫連接捅伤。

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

我們說的同一個(gè)事務(wù)劫流,其實(shí)是指同一個(gè)數(shù)據(jù)庫連接,只有擁有同一個(gè)數(shù)據(jù)庫連接才能同時(shí)提交和回滾丛忆。如果在不同的線程祠汇,拿到的數(shù)據(jù)庫連接肯定是不一樣的,所以是不同的事務(wù)熄诡。

二可很、事務(wù)不回滾

1.錯(cuò)誤的傳播特性

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

@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方法的事務(wù)傳播特性定義成了Propagation.NOT_SUPPORTED我抠,這種類型的傳播特性不支持事務(wù),如果有事務(wù)則會拋異常导坟。

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

2.自己吞了異常

這個(gè)也是出現(xiàn)比較多的場景:把異常吃了,然后又不拋出來惫周,事務(wù)也不會回滾尘惧!比如:

@Service

public class OrderServiceImpl implements OrderService {

@Transactional

public void updateOrder(Order order) {

    try {

       // update order

    } catch {

   }

}

}

3.手動拋了別的異常

即使開發(fā)者沒有手動捕獲異常,但如果拋的異常不正確递递,spring事務(wù)也不會回滾喷橙。

@Service

public class OrderServiceImpl implements OrderService {

@Transactional

public void updateOrder(Order order) {

   try {

      // update order

  } catch {

      throw new Exception("更新錯(cuò)誤");

  }

}

}

這樣事務(wù)也是不生效的,因?yàn)槟J(rèn)回滾的是:RuntimeException登舞,如果你想觸發(fā)其他異常的回滾贰逾,需要在注解上配置一下,如:@Transactional(rollbackFor= Exception.class), 這個(gè)配置僅限于 Throwable 異常類及其子類菠秒。

4.自定義了回滾異常

在使用@Transactional注解聲明事務(wù)時(shí)疙剑,有時(shí)我們想自定義回滾的異常,spring也是支持的践叠⊙早停可以通過設(shè)置rollbackFor參數(shù),來完成這個(gè)功能禁灼。

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

@Slf4j

@Service

public class UserService {

 @Transactional(rollbackFor = BusinessException.class)

  public void add(UserModel userModel) throws Exception {

      saveData(userModel);

      updateData(userModel);

  }

}

如果在執(zhí)行上面這段代碼弄捕,保存和更新數(shù)據(jù)時(shí)僻孝,程序報(bào)錯(cuò)了导帝,拋了SqlException、DuplicateKeyException等異常穿铆。而BusinessException是我們自定義的異常您单,報(bào)錯(cuò)的異常不屬于BusinessException,所以事務(wù)也不會回滾悴务。

即使rollbackFor有默認(rèn)值睹限,但阿里巴巴開發(fā)者規(guī)范中譬猫,還是要求開發(fā)者重新指定該參數(shù)讯檐。

這是為什么呢?

因?yàn)槿绻褂媚J(rèn)值,一旦程序拋出了Exception染服,事務(wù)不會回滾别洪,這會出現(xiàn)很大的bug。所以柳刮,建議一般情況下挖垛,將該參數(shù)設(shè)置成:Exception或Throwable。

5.嵌套事務(wù)回滾多了

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)部事務(wù)秉颗,原本是希望調(diào)用roleService.doOtherThing方法時(shí)痢毒,如果出現(xiàn)了異常,只回滾doOtherThing方法里的內(nèi)容蚕甥,不回滾

userMapper.insertUser里的內(nèi)容哪替,即回滾保存點(diǎn)。但事實(shí)是菇怀,insertUser也回滾了凭舶。

why?

因?yàn)閐oOtherThing方法出現(xiàn)了異常,沒有手動捕獲爱沟,會繼續(xù)往上拋帅霜,到外層add方法的代理方法中捕獲了異常。所以呼伸,這種情況是直接回滾了整個(gè)事務(wù)身冀,不只回滾單個(gè)保存點(diǎn)。

怎么樣才能只回滾保存點(diǎn)呢?

@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)部嵌套事務(wù)放在try/catch中括享,并且不繼續(xù)往上拋異常搂根。這樣就能保證,如果內(nèi)部嵌套事務(wù)中出現(xiàn)異常奶浦,只回滾內(nèi)部事務(wù)兄墅,而不影響外部事務(wù)。

沐子總結(jié)了10種事務(wù)失效的場景澳叉,而在我們工作中經(jīng)常發(fā)生的場景是方法內(nèi)部調(diào)用隙咸、自已吞了異常沐悦、手動拋出了別的異常,大家可以著重關(guān)注這三種場景五督,多看幾遍藏否。最后,****如果我的文章對你有所幫助或者有所啟發(fā)充包,****歡迎關(guān)注公眾號(微信搜索公眾號:首席架構(gòu)師專欄)副签,里面有許多技術(shù)干貨,也有我對技術(shù)的思考和感悟基矮,還有作為架構(gòu)師的驗(yàn)驗(yàn)分享淆储;關(guān)注后回復(fù) 【面試題】,有我準(zhǔn)備的面試題家浇、架構(gòu)師大型項(xiàng)目實(shí)戰(zhàn)視頻等福利 本砰, 小編會帶著你一起學(xué)習(xí)、成長钢悲,讓我們一起加油5愣睢!莺琳!

作者:首席架構(gòu)師專欄
鏈接:http://www.reibang.com/p/866ca9210e21
來源:簡書
著作權(quán)歸作者所有还棱。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處惭等。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末珍手,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子咕缎,更是在濱河造成了極大的恐慌珠十,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凭豪,死亡現(xiàn)場離奇詭異焙蹭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)嫂伞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門孔厉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帖努,你說我怎么就攤上這事撰豺。” “怎么了拼余?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵污桦,是天一觀的道長。 經(jīng)常有香客問我匙监,道長凡橱,這世上最難降的妖魔是什么小作? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮稼钩,結(jié)果婚禮上顾稀,老公的妹妹穿的比我還像新娘。我一直安慰自己坝撑,他們只是感情好静秆,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巡李,像睡著了一般抚笔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上击儡,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天塔沃,我揣著相機(jī)與錄音,去河邊找鬼阳谍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛螃概,可吹牛的內(nèi)容都是我干的矫夯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼吊洼,長吁一口氣:“原來是場噩夢啊……” “哼训貌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冒窍,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤递沪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后综液,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體款慨,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年谬莹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了檩奠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡附帽,死狀恐怖埠戳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蕉扮,我是刑警寧澤整胃,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站喳钟,受9級特大地震影響屁使,放射性物質(zhì)發(fā)生泄漏欠啤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一屋灌、第九天 我趴在偏房一處隱蔽的房頂上張望洁段。 院中可真熱鬧,春花似錦共郭、人聲如沸祠丝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽写半。三九已至,卻和暖如春尉咕,著一層夾襖步出監(jiān)牢的瞬間叠蝇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工年缎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悔捶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓单芜,卻偏偏與公主長得像蜕该,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子洲鸠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

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