一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 一篇文章帶你了解spring事務失效的多種場景

一篇文章帶你了解spring事務失效的多種場景

2021-12-20 13:56蘇三說技術 Java教程

在日常編碼過程中常常涉及到事務,在前兩天看到一篇文章提到了Spring事務,那么在此總結下在Spring環境下事務失效的幾種原因.

前言

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

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

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

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

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

一篇文章帶你了解spring事務失效的多種場景

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得算法沒有想象中那么難了。

BAT大佬寫的刷題筆記,讓我offer拿到手軟

 

一 事務不生效

1.訪問權限問題

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

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

@Service
public class UserService {
  @Transactional
  private void add(UserModel userModel) {
       saveData(userModel);
       updateData(userModel);
  }
}

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

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

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
  // Don't allow no-public methods as required.
  if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null;
  }
  // The method may be on an interface, but we need attributes from the target class.
  // If the target class is null, the method will be unchanged.
  Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
  // First try is the method in the target class.
  TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
  if (txAttr != null) {
    return txAttr;
  }
  // Second try is the transaction attribute on the target class.
  txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
  if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
    return txAttr;
  }
  if (specificMethod != method) {
    // Fallback is to look at the original method.
    txAttr = findTransactionAttribute(method);
    if (txAttr != null) {
      return txAttr;
    }
    // Last fallback is the class of the original method.
    txAttr = findTransactionAttribute(method.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }
  }
  return null;
}

也就是說,如果我們自定義的事務方法(即目標方法),它的訪問權限不是public,而是private、default或protected的話,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動態代理或者cglib,幫我們生成了代理類,在代理類中實現的事務功能。

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

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

3.方法內部調用

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

@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();
  }
}

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

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

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

3.1 新加一個Service方法

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

@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(User user) {
     addData1();
     updateData2();
  }
}

3.2 在該Service類中注入自己

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

@Servcie
public class ServiceA {
 @Autowired
 prvate ServiceA serviceA;
 public void save(User user) {
       queryData1();
       queryData2();
       serviceA.doSave(user);
 }
 @Transactional(rollbackFor=Exception.class)
 public void doSave(User user) {
     addData1();
     updateData2();
  }
}

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

答案:不會。

3.3 通過AopContent類

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

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

@Servcie
public class ServiceA {
 public void save(User user) {
       queryData1();
       queryData2();
       ((ServiceA)AopContext.currentProxy()).doSave(user);
 }
 @Transactional(rollbackFor=Exception.class)
 public void doSave(User user) {
     addData1();
     updateData2();
  }
}

4.未被spring管理

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

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

當然創建bean實例的方法還有很多,有興趣的小伙伴可以看看我之前寫的另一篇文章《@Autowired的這些騷操作,你都知道嗎?》

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

//@Service
public class UserService {
  @Transactional
  public void add(UserModel userModel) {
       saveData(userModel);
       updateData(userModel);
  }    
}

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

5.多線程調用

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

@Slf4j
@Service
public class UserService {
  @Autowired
  private UserMapper userMapper;
  @Autowired
  private RoleService roleService;
  @Transactional
  public void add(UserModel userModel) throws Exception {
      userMapper.insertUser(userModel);
      new Thread(() -> {
          roleService.doOtherThing();
      }).start();
  }
}
@Service
public class RoleService {
  @Transactional
  public void doOtherThing() {
      System.out.println("保存role表數據");
  }
}

從上面的例子中,我們可以看到事務方法add中,調用了事務方法doOtherThing,但是事務方法doOtherThing是在另外一個線程中調用的。

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

如果看過spring事務源碼的朋友,可能會知道spring的事務是通過數據庫連接來實現的。當前線程中保存了一個map,key是數據源,value是數據庫連接。

private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");

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

6.表不支持事務

周所周知,在mysql5之前,默認的數據庫引擎是myisam

它的好處就不用多說了:索引文件和數據文件是分開存儲的,對于查多寫少的單表操作,性能比innodb更好。

有些老項目中,可能還在用它。

在創建表的時候,只需要把ENGINE參數設置成MyISAM即可:

CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

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

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

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

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

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

7.未開啟事務

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

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

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

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

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

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

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

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

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

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

具體配置如下信息:

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

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

 

二 事務不回滾

1.錯誤的傳播特性

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

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

  • REQUIRED 如果當前上下文中存在事務,那么加入該事務,如果不存在事務,創建一個事務,這是默認的傳播屬性值。
  • SUPPORTS 如果當前上下文存在事務,則支持事務加入事務,如果不存在事務,則使用非事務的方式執行。
  • MANDATORY 如果當前上下文中存在事務,否則拋出異常。
  • REQUIRES_NEW 每次都會新建一個事務,并且同時將上下文中的事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。
  • NOT_SUPPORTED 如果當前上下文中存在事務,則掛起當前事務,然后新的方法在沒有事務的環境中執行。
  • NEVER 如果當前上下文中存在事務,則拋出異常,否則在無事務環境上執行代碼。
  • NESTED 如果當前上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。

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

@Service
public class UserService {
  @Transactional(propagation = Propagation.NEVER)
  public void add(UserModel userModel) {
      saveData(userModel);
      updateData(userModel);
  }
}

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

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

2.自己吞了異常

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

@Slf4j
@Service
public class UserService {
  @Transactional
  public void add(UserModel userModel) {
      try {
          saveData(userModel);
          updateData(userModel);
      } catch (Exception e) {
          log.error(e.getMessage(), e);
      }
  }
}

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

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

3.手動拋了別的異常

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

@Slf4j
@Service
public class UserService {
  @Transactional
  public void add(UserModel userModel) throws Exception {
      try {
           saveData(userModel);
           updateData(userModel);
      } catch (Exception e) {
          log.error(e.getMessage(), e);
          throw new Exception(e);
      }
  }
}

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

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

4.自定義了回滾異常

在使用@Transactional注解聲明事務時,有時我們想自定義回滾的異常,spring也是支持的。可以通過設置rollbackFor參數,來完成這個功能。

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

@Slf4j
@Service
public class UserService {
  @Transactional(rollbackFor = BusinessException.class)
  public void add(UserModel userModel) throws Exception {
     saveData(userModel);
     updateData(userModel);
  }
}

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

即使rollbackFor有默認值,但阿里巴巴開發者規范中,還是要求開發者重新指定該參數。

這是為什么呢?

因為如果使用默認值,一旦程序拋出了Exception,事務不會回滾,這會出現很大的bug。所以,建議一般情況下,將該參數設置成:Exception或Throwable。

5.嵌套事務回滾多了

public class UserService {
  @Autowired
  private UserMapper userMapper;
  @Autowired
  private RoleService roleService;
  @Transactional
  public void add(UserModel userModel) throws Exception {
      userMapper.insertUser(userModel);
      roleService.doOtherThing();
  }
}
@Service
public class RoleService {
  @Transactional(propagation = Propagation.NESTED)
  public void doOtherThing() {
      System.out.println("保存role表數據");
  }
}

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

why?

因為doOtherThing方法出現了異常,沒有手動捕獲,會繼續往上拋,到外層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);
      }
  }
}

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

 

三 其他

1 大事務問題

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

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

@Service
public class UserService {
  @Autowired 
  private RoleService roleService;
  @Transactional
  public void add(UserModel userModel) throws Exception {
     query1();
     query2();
     query3();
     roleService.save(userModel);
     update(userModel);
  }
}

@Service
public class RoleService {
  @Autowired 
  private RoleService roleService;
  @Transactional
  public void save(UserModel userModel) throws Exception {
     query4();
     query5();
     query6();
     saveData(userModel);
  }
}

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

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

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

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

saveData(userModel);

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

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

關于大事務問題的危害,可以閱讀一下我的另一篇文章《讓人頭痛的大事務問題到底要如何解決?》,上面有詳細的講解。

一篇文章帶你了解spring事務失效的多種場景

2.編程式事務

上面聊的這些內容都是基于@Transactional注解的,主要說的是它的事務問題,我們把這種事務叫做:聲明式事務。

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

 @Autowired
 private TransactionTemplate transactionTemplate;
 ...
 public void save(final User user) {
       queryData1();
       queryData2();
       transactionTemplate.execute((status) => {
          addData1();
          updateData2();
          return Boolean.TRUE;
       })
 }

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

相較于@Transactional注解聲明式事務,我更建議大家使用,基于TransactionTemplate的編程式事務。主要原因如下:

避免由于spring aop問題,導致事務失效的問題。能夠更小粒度的控制事務的范圍,更直觀。

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

 

總結

本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注服務器之家的更多內容!

原文鏈接:https://blog.csdn.net/lisu061714112/article/details/120098743

延伸 · 閱讀

精彩推薦
  • Java教程小米推送Java代碼

    小米推送Java代碼

    今天小編就為大家分享一篇關于小米推送Java代碼,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧...

    富貴穩中求8032021-07-12
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    這篇文章主要介紹了Java使用SAX解析xml的示例,幫助大家更好的理解和學習使用Java,感興趣的朋友可以了解下...

    大行者10067412021-08-30
  • Java教程Java BufferWriter寫文件寫不進去或缺失數據的解決

    Java BufferWriter寫文件寫不進去或缺失數據的解決

    這篇文章主要介紹了Java BufferWriter寫文件寫不進去或缺失數據的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望...

    spcoder14552021-10-18
  • Java教程xml與Java對象的轉換詳解

    xml與Java對象的轉換詳解

    這篇文章主要介紹了xml與Java對象的轉換詳解的相關資料,需要的朋友可以參考下...

    Java教程網2942020-09-17
  • Java教程升級IDEA后Lombok不能使用的解決方法

    升級IDEA后Lombok不能使用的解決方法

    最近看到提示IDEA提示升級,尋思已經有好久沒有升過級了。升級完畢重啟之后,突然發現好多錯誤,本文就來介紹一下如何解決,感興趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java實現搶紅包功能

    Java實現搶紅包功能

    這篇文章主要為大家詳細介紹了Java實現搶紅包功能,采用多線程模擬多人同時搶紅包,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙...

    littleschemer13532021-05-16
  • Java教程20個非常實用的Java程序代碼片段

    20個非常實用的Java程序代碼片段

    這篇文章主要為大家分享了20個非常實用的Java程序片段,對java開發項目有所幫助,感興趣的小伙伴們可以參考一下 ...

    lijiao5352020-04-06
  • Java教程Java8中Stream使用的一個注意事項

    Java8中Stream使用的一個注意事項

    最近在工作中發現了對于集合操作轉換的神器,java8新特性 stream,但在使用中遇到了一個非常重要的注意點,所以這篇文章主要給大家介紹了關于Java8中S...

    阿杜7482021-02-04
主站蜘蛛池模板: 四虎论坛| 国产精品一二三 | 日本在线亚州精品视频在线 | 91拍拍 | 亚洲精品视频在线 | 边打电话边操 | 久久精品国产视频澳门 | 国产日韩视频一区 | 欧美专区视频 | 日本www色| 欧美在线一级片 | 亚洲免费视频在线观看 | 色综合视频在线观看 | 国产欧美综合精品一区二区 | 91精品国产色综合久久不卡蜜 | 久久久乱码精品亚洲日韩 | 欧美成人影院免费观 | 99久久国产视频 | 俄罗斯bbbbbbxxxxxx | 桥本有菜作品在线 | 九九热视频免费 | 久久精品在现线观看免费15 | 国产成人精品在线观看 | 成人网18免费网 | 武侠艳妇屈辱的张开双腿 | 东京干福利 | 日本视频在线观看 | 国产午夜亚洲精品理论片不卡 | 免费视频大全 | 俺去俺来也在线www色官网 | 999任你躁在线精品免费不卡 | 男人女人日皮 | 青柠影院在线观看免费完整版1 | 国产精品1 | bnb998八度免费影院丫意浓 | 国产一区二区在线看 | 涩涩屋在线播放 | 精品亚洲视频在线观看 | 成人国产精品一级毛片视频 | 欧美日韩一区二区综合在线视频 | 欧美一级特黄特色大片 |