【Spring】@Transactional 閑聊

菜瓜:上次的AOP理論知識看完收穫挺多的,雖然有一個自定義註解的demo,但還是覺得差點東西

水稻:我也覺得沒有跟一遍源碼還是差點意思,這次結合@Transactional註解深入源碼看一下

菜瓜:事務註解,這個平時用的挺多的

水稻:是嗎?來看看你的基礎咋樣

  1. 要保證一個方法中多個數據庫操作的原子性,要共用一個數據庫連接,但是coding時我們不用显示傳遞連接對象,這是咋弄的?
  2. 如果一個方法裏面只有查詢操作,是否不用開啟事務?
  3. 如何解決非事務方法調用本地事務方法失效的?
  4. 註解常用的傳播屬性,你知道他們的區別嗎

菜瓜:雖然沒看過源碼,我大膽猜測一下

  1. 隱式傳遞連接對象可以將其封裝到線程中,一般一次請求操作都是在一個線程中完成。使用ThreadLocal將連接和線程綁定
  2. 查詢操作也得看業務場景,如果多次查詢相同的數據要避免不可重複讀問題,可開啟只讀事務 (readOnly = true)
  3. 結合AOP的知識,這裏其實要解決調用事務方法的對象不是代理對象的問題。用代理對象調本地事務方法即可(注入自己)
    • /**
       * @author QuCheng on 2020/6/24.
       */
      @Service
      public class ItemServiceImpl implements ItemService {
      
          @Resource
          private IcbcItemMapper itemMapper;
      
          @Resource
          private ItemService itemService;
      
          @Override
          public void changeNameById(Long itemId) {
              // changeItemById(itemId);
              itemService.changeItemById(itemId);
          }
      
          @Transactional(rollbackFor = RuntimeException.class)
          @Override
          public void changeItemById(Long itemId) {
              itemMapper.updateNameById(itemId, "name4");
              int a = 10 / 0;
              itemMapper.updatePriceById(itemId, 100L);
          }
      }
  4. 傳播屬性這個沒了解過啊,數據庫事務裏面么得這個概念

水稻:可以啊,平時的代碼沒白寫

菜瓜:coding這種事情,easy啦!

水稻:這就飄了?來看這個問題

  • 如果我想在A事務方法中調用B事務方法,B方法如果回滾了,不能影響A事務繼續執行,但是A事務如果執行出問題了,B也要回滾,怎麼弄?

菜瓜:。。。這不就是大事務嵌套小事務嘛。。。我不會

水稻:不扯了,來看源碼吧,這個問題等解釋了傳播屬性你就知道了

  • 上回我們說到,@Transactional是AOP的典型應用,bean被實例化之後要創建代理(參考自定義註解),就少不了切面類Advisor對象。那麼它是誰,它在哪,它在干什麼?
  • 回到夢開始的地方,事務功能開啟的註解@EnableTransactionManagement
    • 沒錯,它肯定會有一個Import註解引入TransactionManagementConfigurationSelector類,它又引入了切面類
    • public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
       
         @Override
         protected String[] selectImports(AdviceMode adviceMode) {
            switch (adviceMode) {
               case PROXY:
                  return new String[] {AutoProxyRegistrar.class.getName(),
                        // 看這裏
                        ProxyTransactionManagementConfiguration.class.getName()};
               case ASPECTJ:
                  return new String[] {determineTransactionAspectClass()};
               default:
                  return null;
            }
         }
      。。。
      
      }
      
      
      @Configuration
      public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
      
         @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
         @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
         public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
            BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
            advisor.setTransactionAttributeSource(transactionAttributeSource());
            advisor.setAdvice(transactionInterceptor());
            if (this.enableTx != null) {
               advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
            }
            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()); if (this.txManager != null) { interceptor.setTransactionManager(this.txManager); } return interceptor; } } 
    • 切面類對象設置了事務的掃描器,也set了增強類TransactionInterceptor
    • public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
      。。。
      @Override
          @Nullable
          public Object invoke(MethodInvocation invocation) throws Throwable {
              // Work out the target class: may be {@code null}.
              // The TransactionAttributeSource should be passed the target class
              // as well as the method, which may be from an interface.
              Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
      
              // Adapt to TransactionAspectSupport's invokeWithinTransaction...
              return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
          }
      }
      
      public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
        
      。。。  
      @Nullable
      protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
            final InvocationCallback invocation) throws Throwable {
         。。。
      // ①創建事務,數據庫連接處理也在這 TransactionInfo txInfo
      = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try {
      // 調用目標方法 retVal
      = invocation.proceedWithInvocation(); } catch (Throwable ex) {
      // 異常後事務處理 completeTransactionAfterThrowing(txInfo, ex);
      throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); return retVal; } 。。。 } 

菜瓜:懂,接下來的代碼邏輯就是在增強類TransactionInterceptor的invoke方法里

水稻:對

  • 先看數據庫連接的處理 – 驗證ThreadLocal
  • protected void doBegin(Object transaction, TransactionDefinition definition) {
       。。。
    // 如果連接是新的,就進行綁定
    if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder()); } 。。。 } class TransactionSynchronizationManager public static void bindResource(Object key, Object value) throws IllegalStateException { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Assert.notNull(value, "Value must not be null"); Map<Object, Object> map = resources.get(); // set ThreadLocal Map if none found if (map == null) { map = new HashMap<>(); resources.set(map); } Object oldValue = map.put(actualKey, value); // Transparently suppress a ResourceHolder that was marked as void... if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { oldValue = null; } if (oldValue != null) { throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } if (logger.isTraceEnabled()) { logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]"); } }
  • 回過頭來看AB方法調用的回滾問題,直接給出答案(突然發現這個問題要講清楚篇幅會很大,就。。挺突然的。。挺突然der)
    • 在B方法上設置傳播屬性為NESTED即可,然後在A中catch住B的異常
    • 你肯定會問我不加NESTED去catch不行嗎?不行,非NESTED的方法拋出的異常是無法回滾的。
    • 不信你看
    • @Transactional(rollbackFor = RuntimeException.class)
          @Override
          public void changeNameById(Long itemId) {
              itemMapper.updateNameById(itemId, "A");
              try {
                  itemService.changeItemById(itemId);
      //            itemService.changeItemByIdNested(itemId);
              } catch (Exception e) {
                  System.out.println("我想繼續執行,不影響修改A");
              }
              itemMapper.updatePriceById(itemId, 1L);
          }
      
          @Transactional(rollbackFor = RuntimeException.class)
          @Override
          public void changeItemById(Long itemId) {
              itemMapper.updateNameById(itemId, "B+REQUIRED");
              itemMapper.updatePriceById(itemId, 10L);
              int a = 10 / 0;
          }
      
          @Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.NESTED)
          @Override
          public void changeItemByIdNested(Long itemId) {
              itemMapper.updateNameById(itemId, "B+NESTED");
              itemMapper.updatePriceById(itemId, 100L);
              int a = 10 / 0;
          }
      
      
      -- 測試結果
      //①  itemService.changeItemById(itemId);  數據庫所有數據都不會改變
      我想繼續執行,不影響修改A
      org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
      
      // ② itemService.changeItemByIdNested(itemId); 第一個方法的修改會生效
      我想繼續執行,不影響修改A

菜瓜:這就是傳播屬性NESTED?默認的是REQUIRED,還有一個常用的REQUIRES_NEW呢?

水稻:搞清楚這個其實從數據庫連接入手其實就很清楚

  • REQUIRED修飾的方法和A使用同一個連接,A和B是掛一起的,誰回滾都會影響對方,且B方法的異常會被事務管理器標記為必須回滾
  • NESTED修飾的方法和A使用同一個連接,但是用到了數據庫的savePoint特性,它可以回滾到指定的點,如果是有回滾點的操作,拋出的異常可以被處理
  • REQUIRES_NEW修飾的方法和A使用的就不是一個連接了,回不回滾都不會影響對方,當然,要捕捉異常

菜瓜:傳播屬性了解。回滾的問題還得再看看,篇幅很大是很複雜嗎?

水稻:其實不複雜,就是要跟蹤源碼斷點調試。。。截圖搞來搞去,篇幅就很長,你自己去調的話其實很快

菜瓜:那我下去康康

 

總結

  • 這裏提到Transactional註解其實是為了鞏固AOP的,當然提到了一些注意點。譬如本地調用,譬如ThreadLocal的應用,還譬如傳播屬性
  • 傳播屬性其實用的少,但是聊起來就比較多了,可以聊事務的隔離級別,聊ACID的實現,聊MySQL的鎖

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?