什么是事務?
我們在開發企業應用時,通常業務人員的一個操作實際上是對資料庫讀寫的多步操作的結合,由于資料操作在順序執行的程序中,任何一步操作都有可能發生例外,例外會導致后續操作無法完成,此時由于業務邏輯并未正確的完成,之前成功操作的資料并不可靠,如果要讓這個業務正確的執行下去,通常有實作方式:
- 記錄失敗的位置,問題修復之后,從上一次執行失敗的位置開始繼續執行后面要做的業務邏輯
- 在執行失敗的時候,回退本次執行的所有程序,讓操作恢復到原始狀態,帶問題修復之后,重新執行原來的業務邏輯
事務就是針對上述方式2的實作,事務,一般是指要做的或所做的事情,就是上面所說的業務人員的一個操作(比如電商系統中,一個創建訂單的操作包含了創建訂單、商品庫存的扣減兩個基本操作,如果創建訂單成功,庫存扣減失敗,那么就會出現商品超賣的問題,所以最基本的最發就是需要為這兩個操作用事務包括起來,保證這兩個操作要么都成功,要么都失敗),
這樣的場景在實際開發程序中非常多,所以今天就來一起學習一下Spring Boot中的事務管理如何使用!
快速入門
在Spring Boot中,當我們使用了spring-boot-starter-jdbc或spring-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="https://www.cnblogs.com/didispace/p/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目錄:
- Github:https://github.com/dyc87112/SpringBoot-Learning/
- Gitee:https://gitee.com/didispace/SpringBoot-Learning/
如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!
本文首發:Spring Boot 2.x基礎教程:事務管理入門,轉載請注明出處,
歡迎關注我的公眾號:程式猿DD,獲得獨家整理的學習資源和日常干貨推送,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/146892.html
標籤:Java
上一篇:TP5.1解決跨域
下一篇:開發者必備——IDEA配置清單
