這12種場景Spring事務會失效!

前言

對於從事java開發工作的同學來說,spring的事務肯定再熟悉不過了。在某些業務場景下,如果一個請求中,需要同時寫入多張表的數據。為了保證操作的原子性

(要麼同時成功,要麼同時失敗),避免數據不一致的情況,我們一般都會用到spring事務。

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

但如果你使用不當,它也會坑你於無形。今天我們就一起聊聊,事務失效的一些場景,說不定你已經中招了。


一、事務不生效

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;


  public void add(UserModel userModel) {
      userMapper.insertUser(userModel);
      updateStatus(userModel);
  }

  @Transactional
  public void updateStatus(UserModel userModel) {
      doSameThing();
  }
}

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

但是這種方法直接調用了this對象的方法,所以updateStatus方法不會生成事務。由此可見,在同一個類中的方法直接內部調用,會導致事務失效。

4、未被spring管理

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

@Component、@Repository等註解,可以自動實現bean實例化和依賴注入的功能。

如果有一天,你匆匆忙忙的開發了一個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使用

的並不多。在mysql5以後,myisam已經逐漸退出了歷史的舞台,取而代之的是innodb。

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

二、事務不回滾

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。

三、其它

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方法非常多,調用層級很深,而且有部分查詢方法比較耗時的話,會造成整個事務非常耗時,而從造成大事務問題。

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的編程式事務。主要原因如下:

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

參考

微信公眾號: 蘇三說技術 (非常感謝)