什么是事務(wù)闷供?
我們在開發(fā)企業(yè)應(yīng)用時碎赢,對于業(yè)務(wù)人員的一個操作實際是對數(shù)據(jù)讀寫的多步操作的結(jié)合。由于數(shù)據(jù)操作在順序執(zhí)行的過程中账蓉,任何一步操作都有可能發(fā)生異常,異常會導(dǎo)致后續(xù)操作無法完成逾一,此時由于業(yè)務(wù)邏輯并未正確的完成铸本,之前成功操作數(shù)據(jù)的并不可靠,需要在這種情況下進(jìn)行回退遵堵。
事務(wù)的作用就是為了保證用戶的每一個操作都是可靠的箱玷,事務(wù)中的每一步操作都必須成功執(zhí)行,只要有發(fā)生異常就回退到事務(wù)開始未進(jìn)行操作的狀態(tài)陌宿。
事務(wù)管理是Spring框架中最為常用的功能之一锡足,我們在使用Spring Boot開發(fā)應(yīng)用時,大部分情況下也都需要使用事務(wù)限番。
快速入門
在Spring Boot中舱污,當(dāng)我們使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依賴的時候,框架會自動默認(rèn)分別注入DataSourceTransactionManager或JpaTransactionManager弥虐。所以我們不需要任何額外配置就可以用@Transactional注解進(jìn)行事務(wù)的使用扩灯。
我們以之前實現(xiàn)的《用spring-data-jpa訪問數(shù)據(jù)庫》的示例Chapter3-2-2作為基礎(chǔ)工程進(jìn)行事務(wù)的使用常識。
在該樣例工程中(若對該數(shù)據(jù)訪問方式不了解霜瘪,可先閱讀該文章)珠插,我們引入了spring-data-jpa,并創(chuàng)建了User實體以及對User的數(shù)據(jù)訪問對象UserRepository颖对,在ApplicationTest類中實現(xiàn)了使用UserRepository進(jìn)行數(shù)據(jù)讀寫的單元測試用例捻撑,如下:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
public void test() throws Exception {
// 創(chuàng)建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));
// 省略后續(xù)的一些驗證操作
}
}
可以看到,在這個單元測試用例中缤底,使用UserRepository對象連續(xù)創(chuàng)建了10個User實體到數(shù)據(jù)庫中顾患,下面我們?nèi)藶榈膩碇圃煲恍┊惓#纯磿l(fā)生什么情況个唧。
通過定義User的name屬性長度為5江解,這樣通過創(chuàng)建時User實體的name屬性超長就可以觸發(fā)異常產(chǎn)生。
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, length = 5)
private String name;
@Column(nullable = false)
private Integer age;
// 省略構(gòu)造函數(shù)徙歼、getter和setter
}
修改測試用例中創(chuàng)建記錄的語句犁河,將一條記錄的name長度超過5鳖枕,如下:name為HHHHHHHHH的User對象將會拋出異常。
// 創(chuàng)建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));
執(zhí)行測試用例桨螺,可以看到控制臺中拋出了如下異常宾符,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
此時查數(shù)據(jù)庫中,創(chuàng)建了name從AAA到GGG的記錄灭翔,沒有HHHHHHHHHH魏烫、III、JJJ的記錄缠局。而若這是一個希望保證完整性操作的情況下则奥,AAA到GGG的記錄希望能在發(fā)生異常的時候被回退考润,這時候就可以使用事務(wù)讓它實現(xiàn)回退狭园,做法非常簡單,我們只需要在test函數(shù)上添加@Transactional
注解即可糊治。
@Test
@Transactional
public void test() throws Exception {
// 省略測試內(nèi)容
}
再來執(zhí)行該測試用例唱矛,可以看到控制臺中輸出了回滾日志(Rolled back transaction for test context),
2016-05-27 10:35:32.210 WARN 5672 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1406, SQLState: 22001
2016-05-27 10:35:32.210 ERROR 5672 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data truncation: Data too long for column 'name' at row 1
2016-05-27 10:35:32.213 WARN 5672 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Warning Code: 1406, SQLState: HY000
2016-05-27 10:35:32.213 WARN 5672 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data too long for column 'name' at row 1
2016-05-27 10:35:32.221 INFO 5672 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test context [DefaultTestContext@1d7a715 testClass = ApplicationTests, testInstance = com.didispace.ApplicationTests@95a785, testMethod = test@ApplicationTests, testException = org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@11f39f9 testClass = ApplicationTests, locations = '{}', classes = '{class com.didispace.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextLoader = 'org.springframework.boot.test.SpringApplicationContextLoader', parent = [null]]].
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement
再看數(shù)據(jù)庫中井辜,User表就沒有AAA到GGG的用戶數(shù)據(jù)了绎谦,成功實現(xiàn)了自動回滾。
這里主要通過單元測試演示了如何使用@Transactional
注解來聲明一個函數(shù)需要被事務(wù)管理粥脚,通常我們單元測試為了保證每個測試之間的數(shù)據(jù)獨立窃肠,會使用@Rollback
注解讓每個單元測試都能在結(jié)束時回滾。而真正在開發(fā)業(yè)務(wù)邏輯時刷允,我們通常在service層接口中使用@Transactional
來對各個業(yè)務(wù)邏輯進(jìn)行事務(wù)管理的配置冤留,例如:
public interface UserService {
@Transactional
User login(String name, String password);
}
事務(wù)詳解
上面的例子中我們使用了默認(rèn)的事務(wù)配置,可以滿足一些基本的事務(wù)需求树灶,但是當(dāng)我們項目較大較復(fù)雜時(比如纤怒,有多個數(shù)據(jù)源等),這時候需要在聲明事務(wù)時天通,指定不同的事務(wù)管理器泊窘。對于不同數(shù)據(jù)源的事務(wù)管理配置可以見《Spring Boot多數(shù)據(jù)源配置與使用》中的設(shè)置。在聲明事務(wù)時像寒,只需要通過value屬性指定配置的事務(wù)管理器名即可烘豹,例如:@Transactional(value="transactionManagerPrimary")
。
除了指定不同的事務(wù)管理器之后诺祸,還能對事務(wù)進(jìn)行隔離級別和傳播行為的控制携悯,下面分別詳細(xì)解釋:
隔離級別
隔離級別是指若干個并發(fā)的事務(wù)之間的隔離程度,與我們開發(fā)時候主要相關(guān)的場景包括:臟讀取序臂、重復(fù)讀蚌卤、幻讀实束。
我們可以看org.springframework.transaction.annotation.Isolation
枚舉類中定義了五個表示隔離級別的值:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
-
DEFAULT
:這是默認(rèn)值,表示使用底層數(shù)據(jù)庫的默認(rèn)隔離級別逊彭。對大部分?jǐn)?shù)據(jù)庫而言咸灿,通常這值就是:READ_COMMITTED
。 -
READ_UNCOMMITTED
:該隔離級別表示一個事務(wù)可以讀取另一個事務(wù)修改但還沒有提交的數(shù)據(jù)侮叮。該級別不能防止臟讀和不可重復(fù)讀避矢,因此很少使用該隔離級別。 -
READ_COMMITTED
:該隔離級別表示一個事務(wù)只能讀取另一個事務(wù)已經(jīng)提交的數(shù)據(jù)囊榜。該級別可以防止臟讀审胸,這也是大多數(shù)情況下的推薦值。 -
REPEATABLE_READ
:該隔離級別表示一個事務(wù)在整個過程中可以多次重復(fù)執(zhí)行某個查詢卸勺,并且每次返回的記錄都相同砂沛。即使在多次查詢之間有新增的數(shù)據(jù)滿足該查詢,這些新增的記錄也會被忽略曙求。該級別可以防止臟讀和不可重復(fù)讀碍庵。 -
SERIALIZABLE
:所有的事務(wù)依次逐個執(zhí)行,這樣事務(wù)之間就完全不可能產(chǎn)生干擾悟狱,也就是說静浴,該級別可以防止臟讀、不可重復(fù)讀以及幻讀挤渐。但是這將嚴(yán)重影響程序的性能苹享。通常情況下也不會用到該級別。
指定方法:通過使用isolation
屬性設(shè)置浴麻,例如:
@Transactional(isolation = Isolation.DEFAULT)
傳播行為
所謂事務(wù)的傳播行為是指得问,如果在開始當(dāng)前事務(wù)之前,一個事務(wù)上下文已經(jīng)存在白胀,此時有若干選項可以指定一個事務(wù)性方法的執(zhí)行行為椭赋。
我們可以看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
:如果當(dāng)前存在事務(wù),則加入該事務(wù)或杠;如果當(dāng)前沒有事務(wù)哪怔,則創(chuàng)建一個新的事務(wù)。 -
SUPPORTS
:如果當(dāng)前存在事務(wù)向抢,則加入該事務(wù)认境;如果當(dāng)前沒有事務(wù),則以非事務(wù)的方式繼續(xù)運行挟鸠。 -
MANDATORY
:如果當(dāng)前存在事務(wù)叉信,則加入該事務(wù);如果當(dāng)前沒有事務(wù)艘希,則拋出異常硼身。 -
REQUIRES_NEW
:創(chuàng)建一個新的事務(wù)硅急,如果當(dāng)前存在事務(wù),則把當(dāng)前事務(wù)掛起佳遂。 -
NOT_SUPPORTED
:以非事務(wù)方式運行营袜,如果當(dāng)前存在事務(wù),則把當(dāng)前事務(wù)掛起丑罪。 -
NEVER
:以非事務(wù)方式運行荚板,如果當(dāng)前存在事務(wù),則拋出異常吩屹。 -
NESTED
:如果當(dāng)前存在事務(wù)跪另,則創(chuàng)建一個事務(wù)作為當(dāng)前事務(wù)的嵌套事務(wù)來運行;如果當(dāng)前沒有事務(wù)煤搜,則該取值等價于REQUIRED
免绿。
指定方法:通過使用propagation
屬性設(shè)置,例如:
@Transactional(propagation = Propagation.REQUIRED)
完整示例Chapter3-3-1