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

什麼是事務?

咱們在開發企業應用時,一般業務人員的一個操做其實是對數據庫讀寫的多步操做的結合。因爲數據操做在順序執行的過程當中,任何一步操做都有可能發生異常,異常會致使後續操做沒法完成,此時因爲業務邏輯並未正確的完成,以前成功操做的數據並不可靠,若是要讓這個業務正確的執行下去,一般有實現方式:java

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

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

這樣的場景在實際開發過程當中很是多,因此今天就來一塊兒學習一下Spring Boot中的事務管理如何使用!github

快速入門

在Spring Boot中,當咱們使用了spring-boot-starter-jdbcspring-boot-starter-data-jpa依賴的時候,框架會自動默認分別注入DataSourceTransactionManager或JpaTransactionManager。因此咱們不須要任何額外配置就能夠用@Transactional註解進行事務的使用。web

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

@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的時候就能夠觸發異常產生。json

@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字段的錯誤:bash

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註解便可。app

@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
相關文章
相關標籤/搜索