天天看點

Spring的事務那些場景會失效?

Spring的事務10種常見失效場景總結

文章目錄

  • ​​Spring的事務10種常見失效場景總結​​
  • ​​1.錯誤的通路權限​​
  • ​​2.方法被定義成final的​​
  • ​​3.方法内部調用​​
  • ​​4.目前實體沒有被spring管理​​
  • ​​5.錯誤的spring事務傳播特性​​
  • ​​6.資料庫不支援事務​​
  • ​​7.自己吞掉了異常​​
  • ​​8.抛出的異常不正确​​
  • ​​9.多線程調用​​
  • ​​10.嵌套事務多復原了​​
  • ​​結語​​

對于從事java開發工作的同學來說,spring的事務肯定再熟悉不過了。在某些業務場景下,如果同時有多張表的寫入操作,為了保證操作的原子性(要麼同時成功,要麼同時失敗)避免資料不一緻的情況,我們一般都會使用spring事務。

沒錯,spring事務大多數情況下,可以滿足我們的業務需求。但是今天我要告訴大家的是,它有很多坑,稍不注意事務就會失效。

不信,我們一起看看。

1.錯誤的通路權限

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    private void add(UserModel userModel) {
        userMapper.insertUser(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;
  }      

2.方法被定義成final的

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public final void add(UserModel userModel) {
        userMapper.insertUser(userModel);
    }
}      

我們可以看到add方法被定義成了final的,這樣會導緻spring aop生成的代理對象不能複寫該方法,而讓事務失效。

3.方法内部調用

@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方法不會生成事務。

4.目前實體沒有被spring管理

//@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

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

我們可以看到UserService類沒有定義@Service注解,即沒有交給spring管理bean執行個體,是以它的add方法也不會生成事務。

5.錯誤的spring事務傳播特性

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
    }

}      

我們可以看到add方法的事務傳播特性定義成了Propagation.NEVER,這種類型的傳播特性不支援事務,如果有事務則會抛異常。隻有這三種傳播特性才會建立新事務:PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED。

6.資料庫不支援事務

msql8以前的版本資料庫引擎是支援myslam和innerdb的。我以前也用過,對應查多寫少的單表操作,可能會把表的資料庫引擎定義成myslam,這樣可以提升查詢效率。但是,要千萬記得一件事情,myslam隻支援表鎖,并且不支援事務。是以,對這類表的寫入操作事務會失效。

7.自己吞掉了異常

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            userMapper.insertUser(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}      

這種情況下事務不會復原,因為開發者自己捕獲了異常,又沒有抛出。事務的AOP無法捕獲異常,導緻即使出現了異常,事務也不會復原。

8.抛出的異常不正确

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
            userMapper.insertUser(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }

}      

這種情況下,開發人員自己捕獲了異常,又抛出了異常:Exception,事務也不會復原。因為spring事務,預設情況下隻會復原RuntimeException(運作時異常)和Error(錯誤),不會復原Exception。

9.多線程調用

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

我們說的同一個事務,其實是指同一個資料庫連接配接,隻有擁有同一個資料庫連接配接才能同時送出和復原。如果在不同的線程,拿到的資料庫連接配接肯定是不一樣的,是以是不同的事務。

10.嵌套事務多復原了

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中,并且不繼續往抛異常。

結語

繼續閱讀