1. 程式人生 > >Spring @Transactional踩坑記

Spring @Transactional踩坑記

然而 效果 記錄 dcl iso 如果 分庫分表 ignore mar

@Transactional踩坑記

總述

? Spring在1.2引入@Transactional註解, 該註解的引入使得我們可以簡單地通過在方法或者類上添加@Transactional註解,實現事務控制。 然而看起來越是簡單的東西,背後的實現可能存在很多默認規則和限制。而對於使用者如果只知道使用該註解,而不去考慮背後的限制,就可能事與願違,到時候線上出了問題可能根本都找不出啥原因。


踩坑記

1. 多數據源

事務不生效

背景介紹

? 由於數據量比較大,項目的初始設計是分庫分表的。於是在配置文件中就存在多個數據源配置。大致的配置類似下面:

<!-- 數據源A和事務配置 -->
<bean
id="dataSourceA" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> .... </bean> <bean id="dataSourceTxManagerA" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property
name="dataSource" ref="dataSourceA" /> </bean> <!-- mybatis自動掃描生成Dao類的代碼 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="annotationClass" value="com.rampage.mybatis.annotation.mapperDao" />
<property name="nameGenerator" ref="sourceANameGenerator" /> <property name="sqlSessionFactoryBeanName" value="sourceAsqlSessionFactory" /> <property name="basePackage" value="com.rampage" /> </bean> <!-- 自定義的Dao名稱生成器,prefix屬性指定在bean名稱前加上對應的前綴生成Dao --> <bean id="sourceANameGenerator" class="com.rampage.mybatis.dao.namegenerator.MyNameGenerator"> <property name="prefix" value="sourceA" /> </bean> <bean id="sourceAsqlSessionFactory" class="org.mybatis.spring.PathSqlSessionFactoryBean"> <property name="dataSource" ref="dataSourceA" /> </bean> <!-- 數據B和事務配置 --> <bean id="dataSourceB" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> .... </bean> <bean id="dataSourceTxManagerB" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSourceB" /> </bean>

? 但是在實際部署的時候,因為是單機部署的,多個數據源實際上對應的是同一個庫,不存在分布式事務的問題。所以在代碼編寫的時候,直接通過在@Transactional註解來實現事務。具體代碼樣例大致如下:

@Service
public class UserService {
  @Resource("sourceBUserDao")       // 其實這時候Dao對應的是sourceB
  private UserDao userDao;
  
  @Transactional
  public void update(User user) {
    userDao.update(user);
    // Other db operations
    ...
  }
}

? 這中寫法的代碼一直在線上運行了一兩年,沒有出過啥問題.....反而是我在做一個需求的時候,考慮到@Transactional註解裏面的 數據庫操作,如果沒有同時成功或者失敗的話,數據會出現混亂的情況。於是自己測試了一下,開啟了這段踩坑之旅.....

原因分析:

? 開始在網上搜了一下Transactional註解不支持多數據源, 於是我當時把所有數據庫操作都采用sourceB作為前綴的Dao進行操作。結果測試一遍發現還是沒有事務效果。沒有什麽是源碼解決不了的,於是就開始debug源碼,發現最終啟動的事務管理器竟然是dataSourceTxManagerA。 難道和事務管理器聲明的順序有關?於是我調整了下xml配置文件中,事務管理器聲明的順序,發現事務生效了,因此得證。

? 具體來說原因有以下兩點:

  • @Transactional註解不支持多數據源的情況
  • 如果存在多個數據源且未指定具體的事務管理器,那麽實際上啟用的事務管理器是最先在配置文件中指定的(即先加載的)
解決辦法:

? 對於多數據下的事務解決辦法如下:

  • @Transactional註解添加的方法內,數據庫更新操作統一使用一個數據源下的Dao,不要出現多個數據源下的Dao的情況
  • 統一了方法內的數據源之後,可以通過@Transactional(transactionManager = "dataSourceTxManagerB")顯示指定起作用的事務管理器,或者在xml中調節事務管理器的聲明順序

死循環問題

? 這個問題其實也是多數據源導致的,只是更難分析原因。具體場景是:假設我的貨倉裏有1000個貨物,我現在要給用戶發貨。每批次只能發100個。我的貨物有一個字段來標識是否已經發過了,對於已經發過的貨不能重新發(否則只能哭暈在廁所)!代碼的實現是外層有一個while(true)循環去掃描是否還有未發過的貨物,而發貨作為整體的一個事務,具體代碼如下:

@Transactional
public void deliverGoods(List<Goods> goodsList) {   // 傳入的參數是前面循環查出來的未發貨的100個貨物,作為一個批次統一發貨
  updateBatchId(goodsList, batchId);    // 更新同批次貨物的批次號字段
  // do other things
  updateGoodsStatusByBatchId(batchId, delivered);   // 根據前面更新的批次號取修改數據庫相關貨物的發送狀態為已發送
}

? 從整體上來看,這段代碼邏輯上沒有任何問題。實際運行的時候卻發現出現了死循環。還好測試及時發現,沒有最終上線。那麽具體原因是咋樣的呢?

? 出現這個問題的時候,配置文件的配置還是同前面一個問題一樣的配置。即實際上@Transactional註解默認起作用的事務是針對dataSourceA的。然後跟進updateBatchId方法,發現其最終調用的方法采用的Dao是sourceA為前綴的Dao,而updateGoodsStatusByBatchId方法最終調用的Dao是sourceB為前綴的Dao。細細分析,我終於知道為啥了 ??

  • 發貨方法最終起作用的事務是針對sourceA的, 也就是updateBatchId方法實際上作為一個事務,他是要在方法執行完成之後才提交的

  • oracle默認的事務隔離級別是READ_COMMITTED, 所以在updateGoodsStatusByBatchId方法去更新的時候

其實還讀取不到對應批次號的記錄,也就不會做更新

? 解決辦法這裏就不說了,最終還是同前面一個問題,或者更新的時候根據貨物列表去更新。


2. 內部調用

? 內部調用不生效的問題其實大部分大家都知道。舉一個簡單的例子:

? 假設我有一個類的定義如下:

@Service
public class UserService {
  
  public void updateUser(User user){
     // do somothing
    updateWithTransaction(user);
  }
  
  @Transactional
  public void updateWithTransaction(User user) {
    
  }
}

@Service
public class BusinessService {
  @Autowired
  private UserService userService;
  
  public void doUserUpdate(User user) {
    // do somothing
    userService.updateUser(user);
  }
}

? 這種情況下大家都知道事務最終是不會生效的。因為對於updateWithTransaction方法是通過內部調用的,這時候@Transactional註解壓根就不會生效。但是有時候情況並不這麽明顯,考慮下面的代碼:

@Service
public class UserService extends AbstractUserService {
  
  public void updateUser(User user){
     // do somothing
    updateWithTransaction(user);
  }

}

public abstract class AbstractUserService {
  protected abstract void updateUser(User user);
  
   @Transactional
  public void updateWithTransaction(User user) {
    // do update
  }
}

@Service
public class BusinessService {
  
  public void doUserUpdate(User user) {
    // do somothing
    AbstractUserService userService = getUserService();  // 假設最終得到是UserService類的實例
    userService.updateUser(user);
  }
}

? 這段代碼初一分析,最終調用的updateUser方法是UserService的方法, 然後調用的updateWithTransaction是屬於AbstractUserService類的。 好像是調用的不是同一個類的方法,按道理事務應該是可以生效的。其實並沒有..... 原因其實還是是內部調用。其實這種場景我也是在項目中發現的(坑太多),當時的代碼比這個復雜的多,Abstract類包含了一堆可以被子類重寫的方法。原來的代碼大致如下:

public class AbstractService {
  
  // 被外部調用的方法
  public void outMethod() {
    if (A) {
      transactionalMethod1();
    } else if (B) {
      transactionalMethod2();
    } else {
      transactionalMethod3();
    }  
  }
  
  @Transactional
  public void transactionalMethod1() {
    // do something
  }
  
   @Transactional
  public void transactionalMethod2() {
    // do something
  }
  
   @Transactional
  public void transactionalMethod3() {
    // do something
  }
}

? 其中三個事務方法都可能被子類重寫,修改必須兼容老代碼。思考了兼容和接口改造的方式,我最終實現如下:

public class AbstractService implements TransactionIntf {
  
  @Autowired
  private TransactionService transactionService;
  
  public void outMethod() {
     transactionService.setTransactionIntf(this);  // 最終數據庫服務類註冊為當前實現類
     transactionService.processIntransaction(); // 調用數據庫操作
  }
  
  @Override
  public void transactionalMethod1() {
    // do something
  }
  
   @Override
  public void transactionalMethod2() {
    // do something
  }
  
   @Override
  public void transactionalMethod3() {
    // do something
  }
}

public interface TransactionIntf {
  void transactionalMethod1();
  void transactionalMethod2();
  void transactionalMethod3();
}

@Service
public class TransactionService {
  // 定義局部線程變量,存儲對應的服務
  ThreadLocal<TransactionIntf> serviceLocal = new ThreadLocal<TransactionIntf>();
  
  @Transactional  // 在此處加註解
  public void processIntransaction() {
    try {
        if (A) {
          serviceLocal.get().transactionalMethod1();
        } else if (B) {
          serviceLocal.get().transactionalMethod2();
        } else {
          serviceLocal.get().transactionalMethod3();
        }  
    } finally {
      // 最終在本地線程局部變量移除
      serviceLocal.remove();
    }
  }
  
  // 設置服務到本地線程局部變量
  public void setTransactionIntf(TransactionIntf service) {
    this.serviceLocal.set(service);
  }
}

填坑總結

? 下面直接給出網站上關於@Transactional使用的註意點:

  • @Transactional annotations only work on public methods. If you have a private or protected method with this annotation there’s no (easy) way for Spring AOP to see the annotation. It doesn’t go crazy trying to find them so make sure all of your annotated methods are public.
  • Transaction boundaries are only created when properly annotated (see above) methods are called through a Spring proxy. This means that you need to call your annotated method directly through an @Autowired bean or the transaction will never start. If you call a method on an @Autowired bean that isn’t annotated which itself calls a public method that is annotated YOUR ANNOTATION IS IGNORED. This is because Spring AOP is only checking annotations when it first enters the @Autowired code.
  • Never blindly trust that your @Transactional annotations are actually creating transaction boundaries. When in doubt test whether a transaction really is active (see below)

? 另外附上驗證是否是否開啟的工具類源碼(我只是搬運工):

class TransactionTestUtils {
  private static final boolean transactionDebugging = true;
  private static final boolean verboseTransactionDebugging = true;

  public static void showTransactionStatus(String message) {
      System.out.println(((transactionActive()) ? "[+] " : "[-] ") + message);
  }

  // Some guidance from: http://java.dzone.com/articles/monitoring-declarative-transac?page=0,1
  public static boolean transactionActive() {
      try {
          ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
          Class tsmClass = contextClassLoader.loadClass("org.springframework.transaction.support.TransactionSynchronizationManager");
          Boolean isActive = (Boolean) tsmClass.getMethod("isActualTransactionActive", null).invoke(null, null);

          return isActive;
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      } catch (IllegalArgumentException e) {
          e.printStackTrace();
      } catch (SecurityException e) {
          e.printStackTrace();
      } catch (IllegalAccessException e) {
          e.printStackTrace();
      } catch (InvocationTargetException e) {
          e.printStackTrace();
      } catch (NoSuchMethodException e) {
          e.printStackTrace();
      }

      // If we got here it means there was an exception
      throw new IllegalStateException("ServerUtils.transactionActive was unable to complete properly");
  }

  public static void transactionRequired(String message) {
      // Are we debugging transactions?
      if (!transactionDebugging) {
          // No, just return
          return;
      }

      // Are we doing verbose transaction debugging?
      if (verboseTransactionDebugging) {
          // Yes, show the status before we get to the possibility of throwing an exception
          showTransactionStatus(message);
      }

      // Is there a transaction active?
      if (!transactionActive()) {
          // No, throw an exception
          throw new IllegalStateException("Transaction required but not active [" + message + "]");
      }
  }
}

參考鏈接

http://blog.timmattison.com/archives/2012/04/19/tips-for-debugging-springs-transactional-annotation/

Spring @Transactional踩坑記