1. 程式人生 > >Spring Boot事務管理詳解

Spring Boot事務管理詳解

什麼是事務?

我們在開發企業應用時,對於業務人員的一個操作實際是對資料讀寫的多步操作的結合。由於資料操作在順序執行的過程中,任何一步操作都有可能發生異常,異常會導致後續操作無法完成,此時由於業務邏輯並未正確的完成,之前成功操作資料的並不可靠,需要在這種情況下進行回退。

事務的作用就是為了保證使用者的每一個操作都是可靠的,事務中的每一步操作都必須成功執行,只要有發生異常就回退到事務開始未進行操作的狀態。

事務管理是Spring框架中最為常用的功能之一,我們在使用Spring Boot開發應用時,大部分情況下也都需要使用事務。

快速入門

在Spring Boot中,當我們使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依賴的時候,框 架會自動預設分別注入DataSourceTransactionManager或JpaTransactionManager。所以我們不需要任何額外 配置就可以用@Transactional註解進行事務的使用。

在該樣例工程中(若對該資料訪問方式不瞭解,可先閱讀該文章),我們引入了spring-data-jpa,並建立了User實體以及對User的資料訪 問物件UserRepository,在ApplicationTest類中實現了使用UserRepository進行資料讀寫的單元測試用例,如下:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void test() throws Exception {

        // 建立10條記錄
        userRepository.save(new User("AAA", 10));
        userRepository.save(new User("BBB", 20));
        userRepository.save(new User("CCC", 30));
        userRepository.save(new User("DDD", 40));
        userRepository.save(new User("EEE", 50));
        userRepository.save(new User("FFF", 60));
        userRepository.save(new User("GGG", 70));
        userRepository.save(new User("HHH", 80));
        userRepository.save(new User("III", 90));
        userRepository.save(new User("JJJ", 100));

        // 省略後續的一些驗證操作
    }


}

可以看到,在這個單元測試用例中,使用UserRepository物件連續建立了10個User實體到資料庫中,下面我們人為的來製造一些異常,看看會發生什麼情況。

通過定義User的name屬性長度為5,這樣通過建立時User實體的name屬性超長就可以觸發異常產生。

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false, length = 5)
    private String name;

    @Column(nullable = false)
    private Integer age;

    // 省略建構函式、getter和setter

}

修改測試用例中建立記錄的語句,將一條記錄的name長度超過5,如下:name為HHHHHHHHH的User物件將會丟擲異常。

// 建立10條記錄
userRepository.save(new User("AAA", 10));  
userRepository.save(new User("BBB", 20));  
userRepository.save(new User("CCC", 30));  
userRepository.save(new User("DDD", 40));  
userRepository.save(new User("EEE", 50));  
userRepository.save(new User("FFF", 60));  
userRepository.save(new User("GGG", 70));  
userRepository.save(new User("HHHHHHHHHH", 80));  
userRepository.save(new User("III", 90));  
userRepository.save(new User("JJJ", 100));

執行測試用例,可以看到控制檯中丟擲瞭如下異常,name欄位超長:

2016-05-27 10:30:35.948  WARN 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1406, SQLState: 22001  
2016-05-27 10:30:35.948 ERROR 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : Data truncation: Data too long for column 'name' at row 1  
2016-05-27 10:30:35.951  WARN 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Warning Code: 1406, SQLState: HY000  
2016-05-27 10:30:35.951  WARN 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : Data too long for column 'name' at row 1

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement  

此時查資料庫中,建立了name從AAA到GGG的記錄,沒有HHHHHHHHHH、III、JJJ的記錄。而若這是一個希望保證完整性操作的情況 下,AAA到GGG的記錄希望能在發生異常的時候被回退,這時候就可以使用事務讓它實現回退,做法非常簡單,我們只需要在test函式上新增 @Transactional 註解即可。

@Test
@Transactional
public void test() throws Exception {

    // 省略測試內容

}

這裡主要通過單元測試演示瞭如何使用 @Transactional 註解來宣告一個函式需要被事務管理,通常我們單元測試為了保證每個測試之間的資料獨立,會使用 @Rollback 註解讓每個單元測試都能在結束時回滾。而真正在開發業務邏輯時,我們通常在service層介面中使用 @Transactional 來對各個業務邏輯進行事務管理的配置,例如:

public interface UserService {

    @Transactional
    User login(String name, String password);

}
=====================================================
SpringBoot事物的使用

spring Boot 使用事務非常簡單,首先使用註解 @EnableTransactionManagement 開啟事務支援後,然後在訪問資料庫的Service方法上添加註解 @Transactional 便可。

關於事務管理器,不管是JPA還是JDBC等都實現自介面 PlatformTransactionManager 如果你新增的是 spring-boot-starter-jdbc 依賴,框架會預設注入 DataSourceTransactionManager 例項。如果你新增的是 spring-boot-starter-data-jpa 依賴,框架會預設注入 JpaTransactionManager 例項。

你可以在啟動類中新增如下方法,Debug測試,就能知道自動注入的是 PlatformTransactionManager 介面的哪個實現類。

@EnableTransactionManagement // 啟註解事務管理,等同於xml配置方式的 <tx:annotation-driven />
@SpringBootApplication
public class ProfiledemoApplication {

    @Bean
    public Object testBean(PlatformTransactionManager platformTransactionManager){
        System.out.println(">>>>>>>>>>" + platformTransactionManager.getClass().getName());
        return new Object();
    }

    public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
    }
}

 

 

這些SpringBoot為我們自動做了,這些對我們並不透明,如果你專案做的比較大,新增的持久化依賴比較多,我們還是會選擇人為的指定使用哪個事務管理器。 
程式碼如下:

@EnableTransactionManagement
@SpringBootApplication
public class ProfiledemoApplication {

    // 其中 dataSource 框架會自動為我們注入
    @Bean
    public PlatformTransactionManager txManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public Object testBean(PlatformTransactionManager platformTransactionManager) {
        System.out.println(">>>>>>>>>>" + platformTransactionManager.getClass().getName());
        return new Object();
    }

    public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
    }
}

 

 

在Spring容器中,我們手工註解@Bean 將被優先載入,框架不會重新例項化其他的 PlatformTransactionManager 實現類。

然後在Service中,被 @Transactional 註解的方法,將支援事務。如果註解在類上,則整個類的所有方法都預設支援事務。

對於同一個工程中存在多個事務管理器要怎麼處理,請看下面的例項,具體說明請看程式碼中的註釋。

@EnableTransactionManagement // 開啟註解事務管理,等同於xml配置檔案中的 <tx:annotation-driven />
@SpringBootApplication
public class ProfiledemoApplication implements TransactionManagementConfigurer {

    @Resource(name="txManager2")
    private PlatformTransactionManager txManager2;

    // 建立事務管理器1
    @Bean(name = "txManager1")
    public PlatformTransactionManager txManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // 建立事務管理器2
    @Bean(name = "txManager2")
    public PlatformTransactionManager txManager2(EntityManagerFactory factory) {
        return new JpaTransactionManager(factory);
    }

    // 實現介面 TransactionManagementConfigurer 方法,其返回值代表在擁有多個事務管理器的情況下預設使用的事務管理器
    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return txManager2;
    }

    public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
    }

}

@Component
public class DevSendMessage implements SendMessage {

    // 使用value具體指定使用哪個事務管理器
    @Transactional(value="txManager1")
    @Override
    public void send() {
        System.out.println(">>>>>>>>Dev Send()<<<<<<<<");
        send2();
    }

    // 在存在多個事務管理器的情況下,如果使用value具體指定
    // 則預設使用方法 annotationDrivenTransactionManager() 返回的事務管理器
    @Transactional
    public void send2() {
        System.out.println(">>>>>>>>Dev Send2()<<<<<<<<");
    }

}

 

注: 
如果Spring容器中存在多個 PlatformTransactionManager 例項,並且沒有實現介面 TransactionManagementConfigurer 指定預設值,在我們在方法上使用註解 @Transactional 的時候,就必須要用value指定,如果不指定,則會丟擲異常。

對於系統需要提供預設事務管理的情況下,實現介面 TransactionManagementConfigurer 指定。

對有的系統,為了避免不必要的問題,在業務中必須要明確指定 @Transactional 的 value 值的情況下。不建議實現介面 TransactionManagementConfigurer,這樣控制檯會明確丟擲異常,開發人員就不會忘記主動指定。

事務詳解

上面的例子中我們使用了預設的事務配置,可以滿足一些基本的事務需求,但是當我們專案較大較複雜時(比如,有多個數據源等),這時候需要在宣告事務時,指定不同的事務管理器。對於不同資料來源的事務管理配置可以見 《Spring Boot多資料來源配置與使用》 中的設定。在宣告事務時,只需要通過value屬性指定配置的事務管理器名即可,例如:@Transactional(value="transactionManagerPrimary") 。

除了指定不同的事務管理器之後,還能對事務進行隔離級別和傳播行為的控制,下面分別詳細解釋:

隔離級別

隔離級別是指若干個併發的事務之間的隔離程度,與我們開發時候主要相關的場景包括:髒讀取、重複讀、幻讀。

我們可以看 org.springframework.transaction.annotation.Isolation 列舉類中定義了五個表示隔離級別的值:

public enum Isolation {  
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  • DEFAULT :這是預設值,表示使用底層資料庫的預設隔離級別。對大部分資料庫而言,通常這值就是: READ_COMMITTED 。
  • READ_UNCOMMITTED :該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的資料。該級別不能防止髒讀和不可重複讀,因此很少使用該隔離級別。
  • READ_COMMITTED :該隔離級別表示一個事務只能讀取另一個事務已經提交的資料。該級別可以防止髒讀,這也是大多數情況下的推薦值。
  • REPEATABLE_READ :該隔離級別表示一個事務在整個過程中可以多次重複執行某個查詢,並且每次返回的記錄都相同。即使在多次查詢之間有新增的資料滿足該查詢,這些新增的記錄也會被忽略。該級別可以防止髒讀和不可重複讀。
  • SERIALIZABLE :所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。但是這將嚴重影響程式的效能。通常情況下也不會用到該級別。

指定方法:通過使用 isolation 屬性設定,例如:

@Transactional(isolation = Isolation.DEFAULT)

傳播行為

所謂事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。

我們可以看 org.springframework.transaction.annotation.Propagation 列舉類中定義了6個表示傳播行為的列舉值:

public enum Propagation {  
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
  • REQUIRED :如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
  • SUPPORTS :如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
  • MANDATORY :如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常。
  • REQUIRES_NEW :建立一個新的事務,如果當前存在事務,則把當前事務掛起。
  • NOT_SUPPORTED :以非事務方式執行,如果當前存在事務,則把當前事務掛起。
  • NEVER :以非事務方式執行,如果當前存在事務,則丟擲異常。
  • NESTED :如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於 REQUIRED 。

指定方法:通過使用 propagation 屬性設定,例如:

@Transactional(propagation = Propagation.REQUIRED)

 

@Transactional 事務實現機制
在應用系統呼叫聲明瞭 @Transactional 的目標方法時,Spring Framework 預設使用 AOP 代理,在程式碼執行時生成一個代理物件,根據 @Transactional 的屬性配置資訊,這個代理物件決定該宣告 @Transactional 的目標方法是否由攔截器 TransactionInterceptor 來使用攔截,在 TransactionInterceptor 攔截時,會在目標方法開始執行之前建立並加入事務,並執行目標方法的邏輯, 最後根據執行情況是否出現異常,利用抽象事務管理器 AbstractPlatformTransactionManager 操作資料來源 DataSource 提交或回滾事務。

Spring-transaction-mechanis

Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 兩種,以 CglibAopProxy 為例,對於 CglibAopProxy,需要呼叫其內部類的 DynamicAdvisedInterceptor 的 intercept 方法。對於 JdkDynamicAopProxy,需要呼叫其 invoke 方法。

Spring-TransactionManager-hierarchy-subtypes

正如上文提到的,事務管理的框架是由抽象事務管理器 AbstractPlatformTransactionManager 來提供的,而具體的底層事務處理實現,由 PlatformTransactionManager 的具體實現類來實現,如事務管理器 DataSourceTransactionManager。不同的事務管理器管理不同的資料資源 DataSource,比如 DataSourceTransactionManager 管理 JDBC 的 Connection。

原始碼地址
https://github.com/nextyu/spring-transaction-demo

參考資料