一篇文章徹底吃透 Spring 的事務實現
一、背景介紹
今天我們接著來聊聊 SpringBoot 事務相關的具體使用方式。
何謂事務?熟悉數據庫的同學可能比較熟悉,對于任意一組 SQL 語句,要么全部執行成功,要么全部執行失敗;如果中途因為某個原因其中一條語句執行失敗,那么先前執行過的語句將全部撤回。
事務控制的好處在于:可以保證數據操作的安全性。比如,當你給某個客戶 A 轉賬 100 塊,此時銀行需要從你的個人賬戶中扣除 100 塊,然后再給客戶 A 賬戶增加 100 塊,這其實是兩個動作,假如銀行在操作客戶 A 的賬戶時出現故障,此時你個人賬戶的錢已經被扣除,但對方的賬戶并沒有到賬,這將會給客戶產生重大損失。有了事務控制之后,當操作對方的賬戶發生異常時,可以將個人賬戶中扣除的錢進行撤回,從而保證用戶資金賬戶的安全性。
Java 作為一個高級開發語言,同樣支持數據庫的事務控制。在上文中,我們了解到所有涉及到數據庫的操作,都需要通過數據庫連接對象來完成。當我們在操作數據庫時,如果想要開啟手動事務控制(默認是自動提交),其實通過連接對象的autoCommit參數就可以完成,例如如下示例:
// 1.加載數據庫驅動包
Class.forName(DRIVER_CLASS);
// 2.創建一個數據庫連接實例
Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASSWORD);
Statement statement = null;
try {
// 3.設置手動事務提交,默認是true
conn.setAutoCommit(false);
// 4.執行多條SQL語句
statement = conn.createStatement();
statement.executeUpdate("insert into tb_user(id, name) values(1, 'tom') ");
statement.executeUpdate("insert into tb_role(id, name) values(1, 'sys') ");
...
// 5.提交事務
conn.commit();
} catch (SQLException e) {
// 如果SQL執行異常,回滾事務
conn.rollback();
}
// 6.關閉連接
statement.close();
conn.close();
了解了 JDBC 的事務控制之后,再來學習 SpringBoot 事務控制就要容易的多,下面我們一起來看看相關的使用方式。
二、SpringBoot 事務
在 Spring 中事務有兩種實現方式,分別是編程式事務管理和聲明式事務管理。
- 編程式事務管理:利用的是TransactionTemplate類或者更底層的PlatformTransactionManager事務管理器來控制事務操作,用戶可以手動提交或者回滾事務,編程上比較靈活
- 聲明式事務管理:利用的是@Transactional注解對事務進行管理,本質是通過 AOP 對方法前后進行攔截,在目標方法開始之前創建或者加入一個事務,目標方法執行完成之后根據情況進行提交或者回滾事務,使用上比較簡單,易上手
當我們使用 SpringBoot 框架來開發項目的時候,SpringBoot 會自動將 Spring 對數據庫事務支持的依賴庫加載到工程中,無需再次添加相關依賴包。
下面我們以之前介紹的 SpringBoot 整合 mybatis 的工程為例子,利用事務控制來執行多表數據插入操作,一起來看看這兩種事務管理的應用方式。
2.1、編程式事務管理
編程式事務管理主要有兩種實現方式,第一種是利用TransactionTemplate類來提交事務,編程簡單靈活,也是常用的方式之一;另一種是采用PlatformTransactionManager事務管理器來控制事務的提交和回滾。
我們先看看更底層的PlatformTransactionManager接口應用方式。
2.1.1、PlatformTransactionManager 事務管理器
利用PlatformTransactionManager事務管理器來實現事務的操作,示例如下:
@Service
publicclass ApiService {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(ApiService.class);
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private PlatformTransactionManager transactionManager;
public void insert(Role role, Menu menu){
//手動開啟事務
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
// 提交事務
transactionManager.commit(status);
} catch (Exception e) {
// 回滾事務
transactionManager.rollback(status);
LOGGER.error("提交數據異常",e);
}
}
}
在執行角色信息插入和菜單信息插入的時候,如果都成功,則提交事務;如果任意其中一個方法失敗,整個操作進行回滾。
關于事務管理器,無論采用的是 JPA 還是 JDBC 等,底層的事務管理器都實現自PlatformTransactionManager接口。如果采用的是spring-boot-starter-jdbc或者Mybatis操作數據庫,Spring Boot 框架會默認將DataSourceTransactionManager實例作為實現類;如果采用的是spring-boot-starter-data-jpa,框架會默認將JpaTransactionManager實例作為實現類。
關于這一點,我們可以寫一個測試方法來查看PlatformTransactionManager接口的實現類,具體如下:
@MapperScan("com.example.mybatis.mapper")
@SpringBootApplication
publicclass Application {
@Bean
public Object testBean(PlatformTransactionManager platformTransactionManager){
System.out.println("transactionManager:" + platformTransactionManager.getClass().getName());
returnnew Object();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
啟動服務,輸出結果如下:
transactionManager:org.springframework.jdbc.datasource.DataSourceTransactionManager
2.1.2、TransactionTemplate 事務模板類
除了采用事務管理器來實現事務手動控制,Spring 事務框架還為用戶提供了TransactionTemplate事務模板類,通過它也可以實現事務的手動控制,并且操作更加簡單,示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 方式一:帶返回值的事務提交
* @param role
* @param menu
*/
public void insert1(Role role, Menu menu){
Integer result =transactionTemplate.execute(status -> {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
return1;
});
}
/**
* 方式二:忽略返回值的事務提交
* @param role
* @param menu
*/
public void insert2(Role role, Menu menu){
transactionTemplate.execute(status -> {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
returnnull;
});
}
/**
* 方式三:不帶返回值的事務提交
* @param role
* @param menu
*/
public void insert3(Role role, Menu menu){
transactionTemplate.execute(new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
}
});
}
}
以上三種方式,都可以實現實現事務的手動控制,效果等同于采用事務管理器來實現事務手動控制。
如果仔細翻查TransactionTemplate類的execute()方法,你會發現它底層的實現邏輯,與上文介紹的利用事務管理器來控制事務的提交和回滾操作類似。
execute()方法的部分核心源碼如下!
圖片
因此,在編程式事務管理方式下,推薦采用TransactionTemplate類來實現,編程上會更加靈活簡單。
2.2、聲明式事務管理
聲明式事務管理就更加簡單了,只需要在方法上增加注解@Transactional即可,無需任何配置。
2.2.1、Transactional 注解應用示例
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
public void insert(Role role, Menu menu){
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
}
}
聲明式事務管理方式,本質采用的是 AOP 動態代理的方式,對標注@Transactional注解的方法進行前后攔截,然后通過事務管理器來實現事務控制。
盡管@Transactional注解可以作用于接口、接口方法、類以及類方法上,但是 Spring 不推薦在接口或者接口方法上使用該注解,如果編程不當某些場景下可能會失效。當作用于類上,那么該類的所有public方法將都具有事務屬性。在實際使用過程中,推薦在類的方法上使用該注解,以便實現精準的事務控制。
2.2.2、Transactional 注解失效場景
在使用@Transactional注解時,有以下幾個場景,事務可能不會生效!
- 場景一:@Transactional注解如果應用在非public方法,事務不會生效,并且不會拋異常,該注解只會代理public修飾的方法
- 場景二:同一個類中的方法,調用標注@Transactional注解的方法,事務控制也不會生效
- 場景三:內部異常如果被catch吃了,事務不會回滾
- 場景四:@Transactional注解默認只對運行時異常或者 Error 才回滾事務,其它場景不會觸發事務回滾,如果異常不在范圍之內,事務不會回滾
- 場景五:@Transactional注解上的配置參數使用不當,可能導致事務失效
下面我們每個場景下,錯誤的用法。
事務失效:場景一
@Transactional注解應該只被應用到public方法上,如果應用在非public方法,事務不會生效,并且不會拋異常,錯誤示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
protected void insert(Role role, Menu menu){
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
}
}
此時,執行insert操作的時候會自動提交,Spring Boot 不會開啟事務控制。假如menuService.insert()方法執行異常,此時roleService.insert()提交的數據不會回滾。
原因在于:@Transactional注解只會代理public修飾的方法,由 Spring AOP 代理決定的。
事務失效:場景二
同一個類中的方法,如果調用標注@Transactional注解的方法,事務控制也不會生效,錯誤示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
public void save(Role role, Menu menu){
insert(role, menu);
}
@Transactional
public void insert(Role role, Menu menu){
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
}
}
當外部調用save()方法來保存數據的時候,此時 Spring Boot 不會開啟事務控制,會自動提交數據,如果執行過程中發生異常,之前執行過的數據操作不會回滾。
原因在于:被@Transactional標注的方法,只有被當前類以外的代碼調用時,才會由 Spring Aop 生成的代理對象來管理。
事務失效:場景三
被@Transactional標注的方法,內部異常如果被手動catch吃了,事務不會回滾,錯誤示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
public void insert(Role role, Menu menu){
try {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
} catch (Exception e){
e.printStackTrace();
// todo..
}
}
}
此時,被@Transactional標注的方法具備事務控制,如果執行過程中發生異常,數據不會回滾,因為異常被捕獲了。當 Spring AOP 事務代理類沒有感知到異常時,會自動提交事務。
事務失效:場景四
@Transactional注解默認只對運行時異常或者 Error 才回滾事務,其它場景不會觸發事務回滾,如果異常不在范圍之內,事務不會回滾,錯誤示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
public void insert(Role role, Menu menu) throws Exception {
try {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
} catch (Exception e){
thrownew Exception("保存錯誤");
}
}
}
此時中途如果插入數據失敗,會拋Exception異常,但是之前執行成功的數據不會回滾。
如果想要支持其它類型的異常,可以在@Transactional注解類上配置rollbackFor參數,比如如下示例:
@Transactional(rollbackFor = Exception.class)
這個參數配置僅限于 Throwable 異常類及其子類。
事務失效:場景五
在@Transactional注解類上,其實隱含了很多的事務屬性參數,如果參數配置不當,可能也會導致事務失效,錯誤示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Transactional(readOnly = true)
public void insert(Role role, Menu menu){
// 新增角色信息
roleService.insert(role);
// 新增菜單信息
menuService.insert(menu);
}
}
此時提交數據會報錯,因為readOnly = true參數表示只讀模式,不能對數據庫的數據進行更改操作。
三、事務注解詳解
在上文中,我們介紹了@Transactional事務注解的基本用法,正如上文所說,在注解類上,其實隱含了很多的事務屬性參數,Transactional注解類源碼如下。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public@interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
下面我們一起來看看每個屬性的作用。
屬性 | 類型 | 默認值 | 說明 |
transactionManager | String | DEFAULT | 事務管理器 |
propagation | Propagation枚舉 | REQUIRED | 事務傳播屬性 |
isolation | Isolation枚舉 | DEFAULT | 事務隔離級別 |
timeout | int | -1 | 超時(秒) |
readOnly | boolean | false | 是否只讀 |
rollbackFor | Class[] | {} | 需要支持回滾的異常類 |
rollbackForClassName | String[] | {} | 需要支持回滾的異常類名 |
noRollbackFor | Class[] | {} | 不需要支持回滾的異常類 |
noRollbackForClassName | String[] | {} | 不需要支持回滾的異常類名 |
我們重點看看transactionManager、propagation和isolation這三個參數屬性值配置,其它參數基本上見名之意,就不用介紹了。
3.1、事務管理器屬性
默認情況下,不需要我們手動配置事務管理器實例。如果 Spring 容器中有多個事務管理器實例,比如多數據源的情況下,某些場景下,就需要我們手動指定事務管理器實例。
具體應用示例如下:
@Configuration
publicclass TransactionManagerConfigBean {
@Autowired
private DataSource dataSource;
/**
* 自定義一個事務管理器1,同時作為默認事務管理器
* @return
*/
@Bean(name = "txManager1")
@Primary
public PlatformTransactionManager txManager1() {
returnnew DataSourceTransactionManager(dataSource);
}
/**
* 自定義一個事務管理器2
* @return
*/
@Bean(name = "txManager2")
public PlatformTransactionManager txManager2() {
returnnew DataSourceTransactionManager(dataSource);
}
}
如果需要使用指定的事務管理器,只需要在@Transactional注解中配置相應的參數即可。
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional(value = "txManager2")
public void insert(Role role, Menu menu) throws Exception {
// 新增角色信息
roleMapper.insert(role);
// 新增菜單信息
menuMapper.insert(menu);
}
}
3.2、事務傳播屬性
事務傳播屬性,指的是當一個方法內同時存在多個事務的時候,Spring 如何處理這些事務的行為。
Spring 支持 7 種事務傳播方式,Propagation枚舉類支持的屬性值如下:
- REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務
- SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行
- MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常
- REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起
- NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起
- NEVER:以非事務方式運行,如果當前存在事務,則拋出異常
- NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價于REQUIRED
如果想要指定事務傳播行為,可以通過propagation屬性設置,例如:
@Transactional(propagation = Propagation.REQUIRED)
Spring 默認采用的是REQUIRED屬性值,也就是說,如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
這樣設計的好處在于:當一個方法存在多個事務開啟的操作時,只會有一個有效的事務實例,可以實現數據的原子性操作。
比如如下示例:
@Service
publicclass ApiService {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(ApiService.class);
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Autowired
private PlatformTransactionManager transactionManager;
public void save(Role role, Menu menu){
//手動開啟事務
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 新增角色信息
roleService.insert(role);
// 新增菜單信息
menuService.insert(menu);
// 提交事務
transactionManager.commit(status);
} catch (Exception e) {
// 回滾事務
transactionManager.rollback(status);
LOGGER.error("提交數據異常",e);
}
}
}
@Service
public class RoleService {
@Autowired
private RoleMapper roleMapper;
@Transactional
public void insert(Role role){
roleMapper.insert(role);
}
}
@Service
publicclass MenuService {
@Autowired
private MenuMapper menuMapper;
@Autowired
private TransactionTemplate transactionTemplate;
public void insert(Menu menu){
transactionTemplate.execute(status -> {
menuMapper.insert(menu);
returnnull;
});
}
}
當調用ApiService.save()方法時,如果出現異常,所有的操作都會回滾;反之,提交事務。
3.3、事務隔離級別屬性
事務隔離級別,可以簡單的理解為數據庫的事務隔離級別。
從數據庫角度,為了解決多個事務操作同一條數據產生的并發問題,提出了事務隔離級別概念,由低到高依次為 Read uncommitted 、Read committed 、Repeatable read 、Serializable ,這四個級別可以逐個解決臟讀 、不可重復讀 、幻讀等這幾類問題,每個隔離級別作用如下:
- read uncommitted:俗稱讀未提交,指的是一個事務還沒提交時,它做的變更就能被別的事務看到。
- Read committed:俗稱讀提交,指的是一個事務提交之后,它做的變更才會被其他事務看到。
- Repeatable read:俗稱可重復讀,指的是一個事務執行過程中看到的數據,總是跟這個事務在啟動時看到的數據是一致的,同時當其他事務在未提交時,變更是不可見的。
- Serializable:俗稱串行化,顧名思義就是對于同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖沖突的時候,后訪問的事務必須等前一個事務執行完成,才能繼續執行。
在 Spring 中,事務隔離級別的設置可以通過Isolation枚舉類來指定,其支持的屬性值如下:
- DEFAULT:默認值,表示使用底層數據庫的默認隔離級別。大部分數據庫,默認隔離級別為可重復讀;Mysql 有些例外,采用可重復讀隔離級別
- READ_UNCOMMITTED:對應數據庫中讀未提交的隔離級別
- READ_COMMITTED :對應數據庫中讀提交的隔離級別
- REPEATABLE_READ :對應數據庫中可重復讀的隔離級別
- SERIALIZABLE:對應數據庫中串行化的隔離級別
如果想要指定事務隔離級別,可以通過isolation屬性設置,例如:
@Transactional(isolation = Isolation.DEFAULT)
四、小結
最后總結一下,編程式的事務管理比較靈活,如果當前操作非常耗時,可以采用編程式的事務管理來提交事務,避免長事務影響數據庫性能;其次如果數據操作比較簡單時間短,可以采用聲明式事務管理,如果使用不當,可能會導致事務失效,因此在實際使用中要多加小心。
本文主要圍繞 Spring Boot 事務管理的使用方式,做了一次知識內容的總結,如果有描述不對的地方,歡迎留言指出。
五、參考
1.https://www.cnblogs.com/sharpest/p/7995203.html
2.https://blog.csdn.net/MinggeQingchun/article/details/119579941