Spring Boot 2.x基礎教程:事務管理入門

程式猿DD發表於2020-07-10

什麼是事務?

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

  1. 記錄失敗的位置,問題修復之後,從上一次執行失敗的位置開始繼續執行後面要做的業務邏輯
  2. 在執行失敗的時候,回退本次執行的所有過程,讓操作恢復到原始狀態,帶問題修復之後,重新執行原來的業務邏輯

事務就是針對上述方式2的實現。事務,一般是指要做的或所做的事情,就是上面所說的業務人員的一個操作(比如電商系統中,一個建立訂單的操作包含了建立訂單、商品庫存的扣減兩個基本操作。如果建立訂單成功,庫存扣減失敗,那麼就會出現商品超賣的問題,所以最基本的最發就是需要為這兩個操作用事務包括起來,保證這兩個操作要麼都成功,要麼都失敗)。

這樣的場景在實際開發過程中非常多,所以今天就來一起學習一下Spring Boot中的事務管理如何使用!

快速入門

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

我們以之前實現的《使用Spring Data JPA訪問MySQL》的示例作為基礎工程進行事務的使用學習。在該樣例工程中(若對該資料訪問方式不瞭解,可先閱讀該前文),我們引入了spring-data-jpa,並建立了User實體以及對User的資料訪問物件UserRepository,在單元測試類中實現了使用UserRepository進行資料讀寫的單元測試用例,如下:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
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實體到資料庫中,下面我們人為的來製造一些異常,看看會發生什麼情況。

通過@Max(50)來為User的age設定最大值為50,這樣通過建立時User實體的age屬性超過50的時候就可以觸發異常產生。

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Max(50)
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

}

執行測試用例,可以看到控制檯中丟擲瞭如下異常,關於age欄位的錯誤:

2020-07-09 11:55:29.581 ERROR 24424 --- [           main] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [Validation failed for classes [com.didispace.chapter310.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='最大不能超過50', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'}
]]

此時查資料庫中的User表:

可以看到,測試用例執行到一半之後因為異常中斷了,前5條資料正確插入而後5條資料沒有成功插入,如果這10條資料需要全部成功或者全部失敗,那麼這時候就可以使用事務來實現,做法非常簡單,我們只需要在test函式上新增@Transactional註解即可。

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

    // 省略測試內容

}

再來執行該測試用例,可以看到控制檯中輸出了回滾日誌(Rolled back transaction for test context),

2020-07-09 12:48:23.831  INFO 24889 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@f6efaab testClass = Chapter310ApplicationTests, testInstance = com.didispace.chapter310.Chapter310ApplicationTests@60816371, testMethod = test@Chapter310ApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@3c19aaa5 testClass = Chapter310ApplicationTests, locations = '{}', classes = '{class com.didispace.chapter310.Chapter310Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@34cd072c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@528931cf, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2353b3e6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7ce6a65d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4b85edeb]; rollback [true]
2020-07-09 12:48:24.011  INFO 24889 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@f6efaab testClass = Chapter310ApplicationTests, testInstance = com.didispace.chapter310.Chapter310ApplicationTests@60816371, testMethod = test@Chapter310ApplicationTests, testException = javax.validation.ConstraintViolationException: Validation failed for classes [com.didispace.chapter310.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='最大不能超過50', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'}
], mergedContextConfiguration = [WebMergedContextConfiguration@3c19aaa5 testClass = Chapter310ApplicationTests, locations = '{}', classes = '{class com.didispace.chapter310.Chapter310Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@34cd072c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@528931cf, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2353b3e6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7ce6a65d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

再看資料庫中,User表就沒有AAA到EEE的使用者資料了,成功實現了自動回滾。

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

public interface UserService {
    
    @Transactional
    User update(String name, String password);
    
}

事務詳解

上面的例子中我們使用了預設的事務配置,可以滿足一些基本的事務需求,但是當我們專案較大較複雜時(比如,有多個資料來源等),這時候需要在宣告事務時,指定不同的事務管理器。對於不同資料來源的事務管理配置可以見《Spring Data JPA的多資料來源配置》中的設定。在宣告事務時,只需要通過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)

程式碼示例

本文的相關例子可以檢視下面倉庫中的chapter3-10目錄:

如果您覺得本文不錯,歡迎Star支援,您的關注是我堅持的動力!

本文首發:Spring Boot 2.x基礎教程:事務管理入門,轉載請註明出處。
歡迎關注我的公眾號:程式猿DD,獲得獨家整理的學習資源和日常乾貨推送。
如果您對我的其他專題內容感興趣,直達我的個人部落格:didispace.com

相關文章