Spring事務(Transaction)管理高階篇一棧式解決開發中遇到的事務問題

程式設計師xiaozhang發表於2023-02-18

Spring是目前Java開發中最流行的框架了,它的事務管理我們在開發中常常的用到,但是很多人不理解它事務的原理,導致開發中遇到事務方面的問題往往都要用很長的時間才能解決,下面就帶著大家去深入瞭解Spring的事務,然後文章的最後還會給出開發中常常遇到的問題以及解決方案。

如果單純的用Spring框架進行開發(PS使用註解開發,不是XML配置的方式)。那麼要使用Spring事物我們首先要加的就是Spring的這個【EnableTransactionManagement】註解(PS如果直接使用了Spingboot框架了,它已經使用自動配置相關的原理自動加了這個註解了)。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

上面註解的主要作用就是幫我們Import了TransactionManagementConfigurationSelector這個類,它的主要功能如下:

這個類目前就是幫我們在Spring中注入了ProxyTransactionManagementConfiguration,大家一看名稱就知道這是一個配置類(配置類一般都是幫我們注入各種需要的Bean)如下它幫我們做的事情。

  @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
    BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
    advisor.setTransactionAttributeSource(transactionAttributeSource());
    advisor.setAdvice(transactionInterceptor());
    return advisor;
  }

  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource();
  }

  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public TransactionInterceptor transactionInterceptor() {
    TransactionInterceptor interceptor = new TransactionInterceptor();
    interceptor.setTransactionAttributeSource(transactionAttributeSource());
    return interceptor;
  }

上面的程式碼可以看出它幫我們做了三件事,1:匯入了一個增強器。2:匯入了一個事務屬性的資源。3:匯入一個事務的攔截器。這三個類後面講事務的時候都會用到。

如果方法上加了事物註解的類,Spring會建立它的一個代理類放到容器中,如果沒加註解(PS本文只考慮事物註解不考慮其他的)那麼Spring就會把它原始的物件放到容器中這個很重要,後面總結事物為什麼會失效。(下面第一張圖沒加Transaction註解的物件,第二張圖是加了的,然後Sping就把它轉換為代理物件,具體怎麼轉換的是AOP相關功能,AOP很多功能點本文不做具體講解)【上面說的一段話很是重要】。

 

重點來了,下面是我UserService中的主要的方法,很簡單如下:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao ;
    @Autowired
    private ApplicationContext applicationContext ;
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void addUser(User user) {
        user.setPersonName("xiaozhang");
        userDao.insert(user) ;
        user.setPersonName("xiaozhang02");
        UserService userService = applicationContext.getBean(UserService.class);
        userService.addUser02(user);
        int i= 1/0 ;  //讓程式丟擲異常

    }
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void addUser02(User user02) {
        userDao.insert(user02) ;
    }
}

然後就開始帶著大家看相關的原始碼,首先打斷點會進入到如下的方法(你如果想嘗試就在這個類【TransactionAspectSupport】的如下方法中打一個斷點)。

 

分析上面程式碼主要功能。

--1:這段程式碼是獲取事務的屬性,上面配置中配置了
final TransactionAttribute txAttr = (tas != null ? 
tas.getTransactionAttribute(method, targetClass) : null);
--2:決定使用那個事務管理器
final PlatformTransactionManager tm = 
determineTransactionManager(txAttr);
--3:找方法的切入點也就是加了事務註解的方法
final String joinpointIdentification = 
methodIdentification(method, targetClass, txAttr);

上面圖中的三個方法就是註釋中寫的作用,後面就是事務的重點了,然後會走下面的方法。

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
  @Nullable TransactionAttribute txAttr,  final String joinpointIdentification) {
    TransactionStatus status = null;
    if (txAttr != null) {
      if (tm != null) {
        status = tm.getTransaction(txAttr); //這個重點方法
      }

tm.getTransaction(txAttr),這個方法會呼叫如下方法:

//AbstractPlatformTransactionManager這個類的方法
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
  //獲取事務管理器,第一次獲取的為空
    Object transaction = doGetTransaction();
   //看看是否存在事務,第一次不存在。第二次會進入到這個方法,後面會重點講。
    if (isExistingTransaction(transaction)) {
      // Existing transaction found -> check propagation behavior to find out how to behave.
      return handleExistingTransaction(definition, transaction, debugEnabled);
    }
    //我們addUser的方法事務傳播行為是PROPAGATION_REQUIRED,所以走這個分支
    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
        definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
        definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
      //掛起事務,後面PROPAGATION_REQUIRES_NEW這個方法很重要
      SuspendedResourcesHolder suspendedResources = suspend(null);
      try {
        boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
       //建立一個事務狀態
        DefaultTransactionStatus status = newTransactionStatus(
            definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
       //重點來了,開啟事務
        doBegin(transaction, definition);
        prepareSynchronization(status, definition);
        return status;
      }

開啟事務方法(doBegin)(這個方法做了如下6件很重要的事情)

protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
   //1:獲取事務連線,我們寫JDBC的時候也是需要自己獲取事務連線的
    Connection con = null;
      if (!txObject.hasConnectionHolder() ||
          txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
        Connection newCon = obtainDataSource().getConnection();
        txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
      }
      //2:設定資料庫的隔離級別
      Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
      txObject.setPreviousIsolationLevel(previousIsolationLevel);
       //3:關閉資料庫的自動提交
      // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
      // so we don't want to do it unnecessarily (for example if we've explicitly
      // configured the connection pool to set it already).
      if (con.getAutoCommit()) {
        txObject.setMustRestoreAutoCommit(true);
        con.setAutoCommit(false);
      }
      prepareTransactionalConnection(con, definition);
      // 4:啟用事務
      txObject.getConnectionHolder().setTransactionActive(true);
      // 5:設定超時的時間
      int timeout = determineTimeout(definition);
      if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
        txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
      }
       //6:繫結資料來源和連線持有器。
      // Bind the connection holder to the thread.
      if (txObject.isNewConnectionHolder()) {
        TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
      }
    }

上面方法完成後,開始執行下面這個方法:

 try {
        這個方法的主要意思是去執行目標方法,也就是(addUser)
        // This is an around advice: 
        //Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        retVal = invocation.proceedWithInvocation();
      }

去執行目標方法(addUser)後,就又回到前面那個第一次進入Spring原始碼中的方法(因為從上面的程式碼可知我們的addUser方法又呼叫了其他事務方法)如下圖:

 

注意此時的addUser()方法還沒執行完,又進入同樣的方法如下:

上面很多重複的就不講了,前面由於已經存在了事務所以會進入如下方法。

//前面說要重點說的
private TransactionStatus handleExistingTransaction(
      TransactionDefinition definition, Object transaction, boolean debugEnabled)
      throws TransactionException {
  //不允許有事務就丟擲異常 
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
      throw new IllegalTransactionStateException(
          "Existing transaction found for transaction marked with propagation 'never'");
    }
    //重點也是我們開發中常用的事務傳播行為(新建一個事務) 
    // PROPAGATION_REQUIRES_NEW
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
      //先把舊的事務掛起,並且快取起來,
      // 怎麼快取舊的事務的自己可走進去看一下,因為前面事務還沒提交,還需要用
      SuspendedResourcesHolder suspendedResources = suspend(transaction);
      try {
        boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
        DefaultTransactionStatus status = newTransactionStatus(
            definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
       //又開啟新的事務了,和上面講過的同樣的套路了
        doBegin(transaction, definition);
        prepareSynchronization(status, definition);
        return status;
      }

後面就和前面一樣了流程。最後資料庫結果如下(PS忽略我的資料庫表設計,只是為了簡單,用的Mysql資料庫也是為了自己電腦方便安裝)。

 

結果addUser插入失敗了,addUser02插入成功了。(這也就是事務傳播屬性,Propagation.REQUIRED和Propagation.REQUIRES_NEW的區別)。

好了我們進行總結在工作中事務方面的問題。

1:事務註解使用不當,造成事務不生效(PS只舉列開發中常用的)。

1.1 :沒有用代理物件呼叫,直接用原始物件(文章開始說了加了事務註解的會生成一個代理物件),如下的使用事務會失效(也是大多數開發者常犯的錯誤)。

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao ;
    @Autowired
    private ApplicationContext applicationContext ;
    @Override
    public void addUser(User user) {
        user.setPersonName("xiaozhang");
        userDao.insert(user) ;
        user.setPersonName("xiaozhang03");
        //用this沒有AOP代理物件,造成事務失效,2個user都會插入資料庫
        this.addUser02(user) ;
    }
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void addUser02(User user02) {
        userDao.insert(user02) ;
        int i= 1/0 ;  //讓程式丟擲異常
    }
}

1.2 rollbackFor = Exception.class如果不加,Spring只回滾執行時候的異常和程式中的Error(這是很細節的程式碼) 。

上面2個問題解決方案大家看完也就很明白怎樣解決了。

2:事務巢狀使用不當導致事務都回滾了,如下有時候會寫下面的業務。

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao ;
    @Autowired
    private ApplicationContext applicationContext ;
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void addUser(User user) {
        user.setPersonName("xiaozhang");
        userDao.insert(user) ;
        user.setPersonName("xiaozhang02");
        UserServiceImpl userService = (UserServiceImpl)applicationContext.getBean(UserService.class);
        try {
            userService.addUser02(user);
        }catch (Exception e){
            System.out.println(e);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void addUser02(User user02) {
        userDao.insert(user02) ;
        int i= 1/0 ;  //讓程式丟擲異常
    }
}

很多時候我們一看程式碼也沒什麼問題addUser()這個方法會新增資料的,因為把addUser02()方法丟擲的異常捕獲了。開發的時候(PS開發的時候呼叫的其他服務的業務方法,我為了演示簡單這樣寫了)我遇到這方面的問題找了很久才找到原因。會丟擲如下異常:

然後我們拿Spring原始碼分析,上面寫的兩個事務傳播屬性都是propagation = Propagation.REQUIRED (PS如果存在事務就使用原來的事務,不存在就新啟一個事務)。

簡單的說一下為什麼會出現上面的原因,addUser02()這個方法丟擲異常了它會把一個全域性的rollback-only變數標記為true(預設為false)。由於它和addUser()方法共用事務,所以後面這個方法事務提交的時候會檢查這個變數,如果看到這個變數為true。就把它自己也回滾了。如下:

下面addUser()方法要進行提交事務瞭如下:

然後會進入AbstractPlatformTransactionManager這個類的commit方法如下圖。

 

然後會進入processRollback這個方法,為什麼會進入這個方法還是因為rollback-only這個變數上一步設定為true了。(圖片中可以看到是那個類的這個方法,自己Debug打斷點的時候可參考),這個就是我們前面圖中丟擲的異常(Transaction rolled back because it has been marked as rollback-only)。

 private void processRollback(DefaultTransactionStatus status, boolean unexpected){
  try {
    //unexpected 為true 。
      boolean unexpectedRollback = unexpected;
          // Unexpected rollback only matters here if we're asked to fail early
         //這個方法不會進入
          if (!isFailEarlyOnGlobalRollbackOnly()) { 
            unexpectedRollback = false;
          }
        }
      }
      triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
      // Raise UnexpectedRollbackException if we had a global rollback-only marker
      // unexpectedRollback 為true 就會丟擲開始說的那個異常。
      if (unexpectedRollback) {
        throw new UnexpectedRollbackException(
            "Transaction rolled back because it has been marked as rollback-only");
      }
    }

遇到上面的這個問題怎麼解決的?

1:addUser02()這個方法新建一個事物也就是使用事物傳播屬性propagation = Propagation.REQUIRES_NEW(PS如果你寫的業務邏輯可以這樣做)。

2:不用Transaction這個事物註解,事物的提交和回滾自己控制,Sping提供的有事物手動提交和回滾的方法,自己可以查詢一下。

文章中很多地方我都標註了那個類的那個方法,跟著上面一步步看完原始碼,事物方面開發中如果再遇到問題應該很快就能解決,事物那些傳播屬性也很快就能理解,開發中用巢狀事物也不必太擔心有Bug了。

 

相關文章