前面幾篇博文介紹了聲明式事務@Transactional
的使用姿勢,只知道正確的使用姿勢可能還不夠,還得知道什么場景下不生效,避免采坑。本文將主要介紹讓事務不生效的幾種 case
I. 配置
本文的 case,將使用聲明式事務,首先我們創建一個 SpringBoot 項目,版本為2.2.1.RELEASE
,使用 mysql 作為目標數據庫,存儲引擎選擇Innodb
,事務隔離級別為 RR
1. 項目配置
在項目pom.xml
文件中,加上spring-boot-starter-jdbc
,會注入一個DataSourceTransactionManager
的 bean,提供了事務支持
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2. 數據庫配置
進入 spring 配置文件application.properties
,設置一下 db 相關的信息
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
3. 數據庫
新建一個簡單的表結構,用於測試
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '錢',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;
II. 不生效 case
在聲明式事務的使用教程200119-SpringBoot 系列教程之聲明式事務 Transactional 中,也提到了一些事務不生效的方式,比如聲明式事務注解@Transactional
主要是結合代理實現,結合 AOP 的知識點,至少可以得出放在私有方法上,類內部調用都不會生效,下面進入詳細說明
1. 數據庫
事務生效的前提是你的數據源得支持事務,比如 mysql 的 MyISAM 引擎就不支持事務,而 Innodb 支持事務
下面的 case 都是基於 mysql + Innodb 引擎
為后續的演示 case,我們准備一些數據如下
@Service
public class NotEffectDemo {
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
String sql = "replace into money (id, name, money) values" + " (520, '初始化', 200)," + "(530, '初始化', 200)," +
"(540, '初始化', 200)," + "(550, '初始化', 200)";
jdbcTemplate.execute(sql);
}
}
2. 類內部訪問
簡單來講就是指非直接訪問帶注解標記的方法 B,而是通過類普通方法 A,然后由 A 訪問 B
下面是一個簡單的 case
/**
* 非直接調用,不生效
*
* @param id
* @return
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
public boolean testCompileException2(int id) throws Exception {
if (this.updateName(id)) {
this.query("after update name", id);
if (this.update(id)) {
return true;
}
}
throw new Exception("參數異常");
}
public boolean testCall(int id) throws Exception {
return testCompileException2(id);
}
上面兩個方法,直接調用testCompleException
方法,事務正常操作;通過調用testCall
間接訪問,在不生效
測試 case 如下:
@Component
public class NotEffectSample {
@Autowired
private NotEffectDemo notEffectDemo;
public void testNotEffect() {
testCall(530, (id) -> notEffectDemo.testCall(530));
}
private void testCall(int id, CallFunc<Integer, Boolean> func) {
System.out.println("============ 事務不生效case start ========== ");
notEffectDemo.query("transaction before", id);
try {
// 事務可以正常工作
func.apply(id);
} catch (Exception e) {
}
notEffectDemo.query("transaction end", id);
System.out.println("============ 事務不生效case end ========== \n");
}
@FunctionalInterface
public interface CallFunc<T, R> {
R apply(T t) throws Exception;
}
}
輸出結果如下:
============ 事務不生效case start ==========
transaction before >>>> {id=530, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=530, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
transaction end >>>> {id=530, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ 事務不生效case end ==========
從上面的輸出可以看到,事務並沒有回滾,主要是因為類內部調用,不會通過代理方式訪問
3. 私有方法
在私有方法上,添加@Transactional
注解也不會生效,私有方法外部不能訪問,所以只能內部訪問,上面的 case 不生效,這個當然也不生效了
/**
* 私有方法上的注解,不生效
*
* @param id
* @return
* @throws Exception
*/
@Transactional
private boolean testSpecialException(int id) throws Exception {
if (this.updateName(id)) {
this.query("after update name", id);
if (this.update(id)) {
return true;
}
}
throw new Exception("參數異常");
}
直接使用時,下面這種場景不太容易出現,因為 IDEA 會有提醒,文案為: Methods annotated with '@Transactional' must be overridable
4. 異常不匹配
@Transactional
注解默認處理運行時異常,即只有拋出運行時異常時,才會觸發事務回滾,否則並不會如
/**
* 非運行異常,且沒有通過 rollbackFor 指定拋出的異常,不生效
*
* @param id
* @return
* @throws Exception
*/
@Transactional
public boolean testCompleException(int id) throws Exception {
if (this.updateName(id)) {
this.query("after update name", id);
if (this.update(id)) {
return true;
}
}
throw new Exception("參數異常");
}
測試 case 如下
public void testNotEffect() {
testCall(520, (id) -> notEffectDemo.testCompleException(520));
}
輸出結果如下,事務並未回滾(如果需要解決這個問題,通過設置@Transactional
的 rollbackFor 屬性即可)
============ 事務不生效case start ==========
transaction before >>>> {id=520, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=520, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
transaction end >>>> {id=520, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ 事務不生效case end ==========
5. 多線程
這個場景可能並不多見,在標記事務的方法內部,另起子線程執行 db 操作,此時事務同樣不會生效
下面給出兩個不同的姿勢,一個是子線程拋異常,主線程 ok;一個是子線程 ok,主線程拋異常
a. case1
/**
* 子線程拋異常,主線程無法捕獲,導致事務不生效
*
* @param id
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean testMultThread(int id) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
updateName(id);
query("after update name", id);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
boolean ans = update(id);
query("after update id", id);
if (!ans) {
throw new RuntimeException("failed to update ans");
}
}
}).start();
Thread.sleep(1000);
System.out.println("------- 子線程 --------");
return true;
}
上面這種場景不生效很好理解,子線程的異常不會被外部的線程捕獲,testMultThread
這個方法的調用不拋異常,因此不會觸發事務回滾
public void testNotEffect() {
testCall(540, (id) -> notEffectDemo.testMultThread(540));
}
輸出結果如下
============ 事務不生效case start ==========
transaction before >>>> {id=540, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=540, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
Exception in thread "Thread-3" java.lang.RuntimeException: failed to update ans
at com.git.hui.boot.jdbc.demo.NotEffectDemo$2.run(NotEffectDemo.java:112)
at java.lang.Thread.run(Thread.java:748)
after update id >>>> {id=540, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
------- 子線程 --------
transaction end >>>> {id=540, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ 事務不生效case end ==========
b. case2
/**
* 子線程拋異常,主線程無法捕獲,導致事務不生效
*
* @param id
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean testMultThread2(int id) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
updateName(id);
query("after update name", id);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
boolean ans = update(id);
query("after update id", id);
}
}).start();
Thread.sleep(1000);
System.out.println("------- 子線程 --------");
update(id);
query("after outer update id", id);
throw new RuntimeException("failed to update ans");
}
上面這個看着好像沒有毛病,拋出線程,事務回滾,可惜兩個子線程的修改並不會被回滾
測試代碼
public void testNotEffect() {
testCall(550, (id) -> notEffectDemo.testMultThread2(550));
}
從下面的輸出也可以知道,子線程的修改並不在同一個事務內,不會被回滾
============ 事務不生效case start ==========
transaction before >>>> {id=550, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:38.0}
after update name >>>> {id=550, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
after update id >>>> {id=550, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
------- 子線程 --------
after outer update id >>>> {id=550, name=更新, money=220, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:41.0}
transaction end >>>> {id=550, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
============ 事務不生效case end ==========
6. 傳播屬性
上一篇關於傳播屬性的博文中,介紹了其中有幾種是不走事務執行的,所以也需要額外注意下,詳情可以參考博文 200202-SpringBoot 系列教程之事務傳遞屬性
7. 小結
下面小結幾種@Transactional
注解事務不生效的 case
- 數據庫不支持事務
- 注解放在了私有方法上
- 類內部調用
- 未捕獲異常
- 多線程場景
- 傳播屬性設置問題
III. 其他
0. 系列博文&源碼
系列博文
- 180926-SpringBoot 高級篇 DB 之基本使用
- 190407-SpringBoot 高級篇 JdbcTemplate 之數據插入使用姿勢詳解
- 190412-SpringBoot 高級篇 JdbcTemplate 之數據查詢上篇
- 190417-SpringBoot 高級篇 JdbcTemplate 之數據查詢下篇
- 190418-SpringBoot 高級篇 JdbcTemplate 之數據更新與刪除
- 200119-SpringBoot 系列教程之聲明式事務 Transactional
- 200120-SpringBoot 系列教程之事務隔離級別知識點小結
- 200202-SpringBoot 系列教程之事務傳遞屬性
源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 實例源碼: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction
1. 一灰灰 Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top