歡迎光臨
每天分享高質量文章

Spring 事務用法示例與實現原理

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

來源:https://dwz.cn/KgGqPOUk

  • 1. 使用示例

  • 2. 標簽解析

  • 3. 實現原理

  • 4. 小結


關於事務,簡單來說,就是為了保證資料完整性而存在的一種工具,其主要有四大特性:原子性,一致性,隔離性和永續性。對於Spring事務,其最終還是在資料庫層面實現的,而Spring只是以一種比較優雅的方式對其進行封裝支援。本文首先會透過一個簡單的示例來講解Spring事務是如何使用的,然後會講解Spring是如何解析xml中的標簽,並對事務進行支援的。

1. 使用示例

關於事務最簡單的示例,就是其一致性,比如在整個事務執行過程中,如果任何一個位置報錯了,那麼都會導致事務回滾,回滾之後資料的狀態將和事務執行之前完全一致。這裡我們以使用者資料為例,在插入使用者資料的時候,如果程式報錯了,那麼插入的動作就會回滾。如下是使用者的物體:

public class User {
  private long id;
  private String name;
  private int age;

  // getter, setter...
}

如下是模擬插入使用者資料的業務程式碼:

public interface UserService {
  void insert(User user);
}

@Service
@Transactional
public class UserServiceImpl implements UserService {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Override
  public void insert(User user) {
    jdbcTemplate.update("insert into user (name, age) value (?, ?)",
        user.getName(), user.getAge());
  }
}

在進行事務支援時,Spring只需要使用者在需要事務支援的bean上使用@Transactional註解即可,如果需要修改事務的隔離級別和傳播特性的屬性,則使用該註解中的屬性進行指定。這裡預設的隔離級別與各個資料庫一致,比如MySQL是Repeatable Read,而傳播特性預設則為Propagation.REQUIRED,即只需要當前操作具有事務即可。如下是xml檔案的配置:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="url" value="jdbc:mysql://localhost/test?useUnicode=true"/>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="username" value="****"/>
    <property name="password" value="******"/>
bean>



<bean id=“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”>
    <property name=“dataSource” ref=“dataSource”/>
bean>

<bean id=“transactionManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”>
    <property name=“dataSource” ref=“dataSource”/>
bean>

<context:component-scan base-package=“com.transaction”/>
<tx:annotation-driven/>

上述資料庫配置使用者按照各自的設定進行配置即可。可以看到,這裡對於資料庫的配置,主要包括四個方面:

  • DataSource配置:設定當前應用所需要連線的資料庫,包括連結,使用者名稱,密碼等;

  • JdbcTemplate宣告:封裝了客戶端呼叫資料庫的方式,使用者可以使用其他的方式,比如JpaRepository,Mybatis等等;

  • TransactionManager配置:指定了事務的管理方式,這裡使用的是DataSourceTransactionManager,對於不同的連結方式,也可以進行不同的配置,比如對於JpaRepository使用JpaTransactionManager,對於Hibernate,使用HibernateTransactionManager;

  • tx:annotation-driven:主要用於事務驅動,其會透過AOP的方式宣告一個為事務支援的Advisor,透過該Advisor和事務的相關配置進行事務相關操作。

按照上述配置,我們的事務功能即配置完成,如下是我們的驅動類程式:

public class TransactionApp {
  @Test
  public void testTransaction() {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = ac.getBean(UserService.class);
    User user = getUser();
    userService.insert(user);
  }

  private User getUser() {
    User user = new User();
    user.setName("Mary");
    user.setAge(27);
    return user;
  }
}

執行上述程式之後,可以看到資料庫中成功新增了一條資料。這裡如果我們將業務程式碼的插入陳述句之後手動丟擲一個異常,那麼,理論上插入陳述句是會回滾的。如下是修改後的service程式碼:

@Service
@Transactional
public class UserServiceImpl implements UserService {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Override
  public void insert(User user) {
    jdbcTemplate.update("insert into user (name, age) value (?, ?)",
        user.getName(), user.getAge());
    throw new RuntimeException();
  }
}

這裡我們手動丟擲了一個RuntimeException,再次執行上述程式之後我們發現資料庫中是沒有新增的資料的,這說明我們的事務在程式出錯後是能夠保證資料一致性的。

2. 標簽解析

關於事務的實現原理,我們首先講解Spring是如何解析標簽,並且封裝相關bean的,後面我們會深入講解Spring是如何封裝資料庫事務的。

根據上面的示例,我們發現,其主要有三個部分:DataSource,TransactionManager和tx:annotation-driven標簽。這裡前面兩個部分主要是宣告了兩個bean,分別用於資料庫連線的管理和事務的管理,而tx:annotation-driven才是Spring事務的驅動。根據本人前面對Spring自定義標簽的講解(Spring自定義標簽解析與實現),可以知道,這裡tx:annotation-driven是一個自定義標簽,我們根據其名稱空間(www.springframework.org/schema/tx)在全域性範圍內搜尋,可以找到其處理器指定檔案spring.handlers,該檔案內容如下:

http\://www.springframework.org/schema/tx=org.springframework.transaction.config.TxNamespaceHandler

這裡也就是說tx:annotation-driven標簽的解析在TxNamespaceHandler中,我們繼續開啟該檔案可以看到起init()方法如下:

@Override
public void init() {
    registerBeanDefinitionParser("advice"new TxAdviceBeanDefinitionParser());
    registerBeanDefinitionParser("annotation-driven",
        new AnnotationDrivenBeanDefinitionParser());
    registerBeanDefinitionParser("jta-transaction-manager",
        new JtaTransactionManagerBeanDefinitionParser());
}

可以看到,這裡的annotation-driven是在AnnotationDrivenBeanDefinitionParser中進行處理的,其parse()方法就是解析標簽,並且註冊相關bean的方法,如下是該方法的實現:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 註冊事務相關的監聽器,如果某個方法標註了TransactionalEventListener註解,
    // 那麼該方法就是一個事務事件觸發方法,即發生某種事務事件後,將會根據該註解的設定,回呼指定
    // 型別的方法。常見的事務事件有:事務執行前和事務完成(包括報錯後的完成)後的事件。
    registerTransactionalEventListenerFactory(parserContext);
    String mode = element.getAttribute("mode");
    // 獲取當前事務驅動程式的樣式,如果使用了aspectj樣式,則會註冊一個AnnotationTransactionAspect
    // 型別的bean,使用者可以以aspectj的方式使用該bean對事務進行更多的配置
    if ("aspectj".equals(mode)) {
        registerTransactionAspect(element, parserContext);
    } else {
        // 一般使用的是當前這種方式,這種方式將會在Spring中註冊三個bean,分別是
        // AnnotationTransactionAttributeSource,TransactionInterceptor
        // 和BeanFactoryTransactionAttributeSourceAdvisor,並透過Aop的方式實現事務
        AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
    }
    return null;
}

可以看到,對於事務的驅動,這裡主要做了兩件事:①註冊事務相關的事件觸發器,這些觸發器由使用者自行提供,在事務進行提交或事務完成時會觸發相應的方法;②判斷當前事務驅動的樣式,有預設樣式和aspectj樣式,對於aspectj樣式,Spring會註冊一個AnnotationTransactionAspect型別的bean,用於使用者使用更親近於aspectj的方式進行事務處理;對於預設樣式,這裡主要是宣告了三個bean,最終透過Aop的方式進行事務切入。下麵我們看一下Spring是如何註冊這三個bean的,如下是AopAutoProxyConfigurer.configureAutoProxyCreator的原始碼:

public static void configureAutoProxyCreator(Element element,
        ParserContext parserContext)
 
{
    // 這個方法主要是在當前BeanFactory中註冊InfrastructureAdvisorAutoProxyCreator這個
    // bean,這個bean繼承了AbstractAdvisorAutoProxyCreator,也就是其實現原理與我們前面
    // 講解的Spring Aop的實現原理幾乎一致
    AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);

    // 這裡的txAdvisorBeanName就是我們最終要註冊的bean,其型別就是下麵註冊的
    // BeanFactoryTransactionAttributeSourceAdvisor,可以看到,其本質是一個
    // Advisor型別的物件,因而Spring Aop會將其作為一個切麵織入到指定的bean中
    String txAdvisorBeanName = TransactionManagementConfigUtils
        .TRANSACTION_ADVISOR_BEAN_NAME;
    // 如果當前BeanFactory中已經存在了標的bean,則不進行註冊
    if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) {
        Object eleSource = parserContext.extractSource(element);
        // 註冊AnnotationTransactionAttributeSource,這個bean的主要作用是封裝
        // @Transactional註解中宣告的各個屬性
        RootBeanDefinition sourceDef = new RootBeanDefinition(
       "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource");
        sourceDef.setSource(eleSource);
        sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        String sourceName = parserContext.getReaderContext()
            .registerWithGeneratedName(sourceDef);

        // 註冊TransactionInterceptor型別的bean,並且將上面的封裝屬性的bean設定為其一個屬性。
        // 這個bean本質上是一個Advice(可檢視其繼承結構),Spring Aop使用Advisor封裝實現切麵
        // 邏輯織入所需的所有屬性,但真正的切麵邏輯卻是儲存在其Advice屬性中的,也就是說這裡的
        // TransactionInterceptor才是真正封裝了事務切麵邏輯的bean
        RootBeanDefinition interceptorDef =
            new RootBeanDefinition(TransactionInterceptor.class);
        interceptorDef.setSource(eleSource);
        interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registerTransactionManager(element, interceptorDef);
        interceptorDef.getPropertyValues().add("transactionAttributeSource",
            new RuntimeBeanReference(sourceName));
        String interceptorName = parserContext.getReaderContext()
            .registerWithGeneratedName(interceptorDef);

        // 註冊BeanFactoryTransactionAttributeSourceAdvisor型別的bean,這個bean實現了
        // Advisor介面,實際上就是封裝了當前需要織入的切麵的所有所需的屬性
        RootBeanDefinition advisorDef =
            new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class);
        advisorDef.setSource(eleSource);
        advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        advisorDef.getPropertyValues().add("transactionAttributeSource",
            new RuntimeBeanReference(sourceName));
        advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
        if (element.hasAttribute("order")) {
            advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
        }
        parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef);

        // 將需要註冊的bean封裝到CompositeComponentDefinition中,並且進行註冊
        CompositeComponentDefinition compositeDef =
            new CompositeComponentDefinition(element.getTagName(), eleSource);
        compositeDef.addNestedComponent(
            new BeanComponentDefinition(sourceDef, sourceName));
        compositeDef.addNestedComponent(
            new BeanComponentDefinition(interceptorDef, interceptorName));
        compositeDef.addNestedComponent(
            new BeanComponentDefinition(advisorDef, txAdvisorBeanName));
        parserContext.registerComponent(compositeDef);
    }
}

如此,Spring事務相關的標簽即解析完成,這裡主要是宣告了一個BeanFactoryTransactionAttributeSourceAdvisor型別的bean到BeanFactory中,其實際為Advisor型別,這也是Spring事務能夠透過Aop實現事務的根本原因。

3. 實現原理

關於Spring事務的實現原理,這裡需要抓住的就是,其是使用Aop實現的,我們知道,Aop在進行解析的時候,最終會生成一個Adivsor物件,這個Advisor物件中封裝了切麵織入所需要的所有資訊,其中就包括最重要的兩個部分就是PointcutAdivce屬性。這裡Pointcut用於判斷標的bean是否需要織入當前切麵邏輯;Advice則封裝了需要織入的切麵邏輯。如下是這三個部分的簡要關係圖:

Advisor

同樣的,對於Spring事務,其既然是使用Spring Aop實現的,那麼也同樣會有這三個成員。我們這裡我們只看到了註冊的Advisor和Advice(即BeanFactoryTransactionAttributeSourceAdvisor和TransactionInterceptor),那麼Pointcut在哪裡呢?這裡我們檢視BeanFactoryTransactionAttributeSourceAdvisor的原始碼可以發現,其內部宣告了一個TransactionAttributeSourcePointcut型別的屬性,並且直接在內部進行了實現,這就是我們需要找的Pointcut。這裡這三個物件對應的關係如下:

Transaction

這樣,用於實現Spring事務的Advisor,Pointcut以及Advice都已經找到了。關於這三個類的具體作用,我們這裡進行整體的上的講解,後面我們將會深入其內部講解其是如何進行bean的過濾以及事務邏輯的織入的。

  • BeanFactoryTransactionAttributeSourceAdvisor:封裝了實現事務所需的所有屬性,包括Pointcut,Advice,TransactionManager以及一些其他的在Transactional註解中宣告的屬性;

  • TransactionAttributeSourcePointcut:用於判斷哪些bean需要織入當前的事務邏輯。這裡可想而知,其判斷的基本邏輯就是判斷其方法或類宣告上有沒有使用@Transactional註解,如果使用了就是需要織入事務邏輯的bean;

  • TransactionInterceptor:這個bean本質上是一個Advice,其封裝了當前需要織入標的bean的切麵邏輯,也就是Spring事務是如果藉助於資料庫事務來實現對標的方法的環繞的。

4. 小結

本文首先透過一個簡單的例子講解了Spring事務是如何使用的,然後講解了Spring事務進行標簽解析的時候做了哪些工作,最後講解了Spring事務是如何與Spring Aop進行一一對應的,並且是如何透過Spring Aop實現將事務邏輯織入標的bean的。



如果你對 Dubbo / Netty 等等原始碼與原理感興趣,歡迎加入我的知識星球一起交流。長按下方二維碼噢

目前在知識星球更新了《Dubbo 原始碼解析》目錄如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 伺服器

16. P2P 伺服器

17. HTTP 伺服器

18. 序列化 Serialization

19. 叢集容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing

… 一共 69+ 篇

目前在知識星球更新了《Netty 原始碼解析》目錄如下:

01. 除錯環境搭建
02. NIO 基礎
03. Netty 簡介
04. 啟動 Bootstrap

05. 事件輪詢 EventLoop

06. 通道管道 ChannelPipeline

07. 通道 Channel

08. 位元組緩衝區 ByteBuf

09. 通道處理器 ChannelHandler

10. 編解碼 Codec

11. 工具類 Util

… 一共 61+ 篇

目前在知識星球更新了《資料庫物體設計》目錄如下:


01. 商品模組
02. 交易模組
03. 營銷模組
04. 公用模組

… 一共 17+ 篇


目前在知識星球更新了《Spring 原始碼解析》目錄如下:


01. 除錯環境搭建
02. IoC Resource 定位
03. IoC BeanDefinition 載入

04. IoC BeanDefinition 註冊

05. IoC Bean 獲取

06. IoC Bean 生命週期

… 一共 35+ 篇


原始碼不易↓↓↓

點贊支援老艿艿↓↓

贊(0)

分享創造快樂