spring事務詳解(四)測試驗證

系列目錄

spring事務詳解(一)初探事務html

spring事務詳解(二)簡單樣例java

spring事務詳解(三)源碼詳解mysql

spring事務詳解(四)測試驗證spring

spring事務詳解(五)總結提升sql

1、引子

在第一節中咱們知道spring爲了支持數據庫事務的ACID四大特性,在底層源碼中對事務定義了6個屬性:事務名稱隔離級別超時時間是否只讀傳播機制回滾機制。其中隔離級別傳播機制光看第一節的描述仍是不夠的,須要實際測試一下方能放心且記憶深入。數據庫

2、環境

2.1 業務模擬

模擬用戶去銀行轉帳,用戶A轉帳給用戶B,mybatis

須要保證用戶A扣款,用戶B加款同時成功或失敗回滾。app

2.2 環境準備

測試環境dom

mysql8+mac,測試時使用的mysql8(和mysql5.6的設置事務變量的語句不一樣,不用太在乎)ide

測試準備

建立一個數據庫test,建立一張表user_balance用戶餘額表。id主鍵,name姓名,balance帳戶餘額。

 1 mysql> create database test;  2 Query OK, 1 row affected (0.05 sec)  3  4 mysql> use test;  5 Database changed  6 mysql> CREATE TABLE `user_balance` (  7 -> `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID主鍵',  8 -> `name` varchar(20) DEFAULT NULL COMMENT '姓名',  9 -> `balance` decimal(10,0) DEFAULT NULL COMMENT '帳戶餘額', 10 -> PRIMARY KEY (`id`) 11 -> ) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8; 12 Query OK, 0 rows affected, 1 warning (0.15 sec)

初始化數據,2個帳戶都是1000元:

mysql> INSERT INTO `user_balance` VALUES ('1', '張三', '1000'), ('2', '李四', '1000'); Query OK, 2 rows affected (0.06 sec) Records: 2 Duplicates: 0 Warnings: 0 mysql> select * from user_balance; 
+----+--------+---------+ | id | name | balance | +----+--------+---------+ | 1 | 張三 | 1000 | | 2 | 李四 | 1000 | +----+--------+---------+ 2 rows in set (0.00 sec)

3、隔離級別實測

3.2 隔離級別實測

通用語句

1.開啓/提交事務:開啓:begin/start transaction都行,提交:commit;

2.查詢事務級別:select @@transaction_isolation;

3.修改事務級別:set global transaction_isolation='read-uncommitted';

注意:修改完了後要exit退出再從新鏈接mysql(mysql -uroot)才能生效(這裏是模擬MySQL5.6,MySQL8有直接生效的語句)。

如下4種測試都是先設置好事務隔離級別,再作的測試,下面的測試就再也不展現出來了。

3.2.1 Read Uncommitted(讀未提交)

測試步驟:

1.開啓2個會話鏈接mysql,會話1開始事務A,會話2開始事務B。

2.事務A中執行update把張三的餘額1000-100=900,事務A查詢結果爲900。

3.此時事務A並無提交,事務B查詢結果也是900,即:讀取了未提交的內容(MVCC快照讀的最新版本號數據)。

以下圖(左邊的是會話1-事務A,右邊的是會話2-事務B):

總結明顯不行,由於事務A內部的處理數據不必定是最後的數據,極可能事務A後續再加上1000,那麼事務B讀取的數據明顯就錯了,即髒讀!

3.2.2 Read Committed(讀提交)

測試步驟:

1.開啓2個會話鏈接mysql,會話1開始事務A,會話2開始事務B。

2.事務A中執行update把張三的餘額1000-100=900,事務A查詢結果爲900。只要事務A未提交,事務B查詢數據都沒有變化仍是1000.

3.事務A提交,事務B查詢當即變成900了,即:讀已提交。

以下圖(左邊的是會話1-事務A,右邊的是會話2-事務B)

總結解決了髒讀問題,但此時事務B還沒提交,即出現了在一個事務中屢次查詢同一sql數據不一致的狀況,即不可重複讀!

3.2.3 Repeatable Read(可重讀)

測試步驟:

1.開啓2個會話鏈接mysql,會話1開始事務A,會話2開始事務B。

2.事務A中執行update把張三的餘額1000-100=900,事務A查詢結果爲900。事務A提交,事務B查詢數據仍是1000不變.

3.會話1再開始一個事務C插入一條「王五」數據,並提交,事務B查詢仍是2條數據,且數據和第一次查詢一致,即:讀已提交+可重複讀。

4.會話2中的事務B也插入一條相同ID的數據,報錯:已經存在相同ID=3的數據插入失敗!,即出現了幻讀。

以下圖:

mysql支持的解決方案

要防止幻讀,能夠事務A中for update加上範圍,最終會生成間隙鎖,阻塞其它事務插入數據,而且當事務A提交後,事務B當即能夠插入成功。

3.2.4 Serializable(可串行化)

測試步驟:

1.開啓2個會話鏈接mysql,會話1開始事務A,會話2開始事務B。

2.事務A,查詢id=2的記錄,事務B更新id=2的記錄,update操做被阻塞一直到超時(事務A提交後,事務B update能夠當即執行)。

以下圖左邊的是會話1-事務A,右邊的是會話2-事務B)

結論:Serializable級別下,讀也加鎖!若是是行鎖(查詢一行),那麼後續對這一行的修改操做會直接阻塞等待第一個事務完畢。若是是表鎖(查詢整張表),那麼後續對這張表的全部修改操做都阻塞等待。可見僅僅一個查詢就鎖住了相應的查詢數據,性能實在是不敢恭維。

4、傳播機制實測

3.3.1 測試準備

環境:

spring4+mybatis+mysql+slf4j+logback,注意:日誌logback要配置:日誌打印爲debug級別,這樣才能看見事務過程。以下:

1 <root level="DEBUG"> 2 <appender-ref ref="STDOUT"/> 3 </root>

 

測試代碼:

測試基類:BaseTest
 1 import lombok.extern.slf4j.Slf4j;  2 import org.junit.runner.RunWith;  3 import org.springframework.boot.test.context.SpringBootTest;  4 import org.springframework.test.context.junit4.SpringRunner;  5 import study.StudyDemoApplication;  6  7 @Slf4j  8 @RunWith(SpringRunner.class)  9 @SpringBootTest(classes = StudyDemoApplication.class) 10 public class BaseTest { 11 12 13 }

測試子類:UserBalanceTest

 1 import org.junit.Test;  2 import study.service.UserBalanceService;  3  4 import javax.annotation.Resource;  5 import java.math.BigDecimal;  6  7 /**  8  * @Description 用戶餘額測試類(事務)  9  * @author denny 10  * @date 2018/9/4 上午11:38 11 */ 12 public class UserBalanceTest extends BaseTest{ 13 14  @Resource 15 private UserBalanceService userBalanceService; 16 17  @Test 18 public void testAddUserBalanceAndUser(){ 19 userBalanceService.addUserBalanceAndUser("趙六",new BigDecimal(1000)); 20  } 21 22 public static void main(String[] args) { 23 24  } 25 26 }
UserBalanceImpl:
 1 package study.service.impl;  2  3 import lombok.extern.slf4j.Slf4j;  4 import org.springframework.stereotype.Service;  5 import org.springframework.transaction.annotation.Propagation;  6 import org.springframework.transaction.annotation.Transactional;  7 import study.domain.UserBalance;  8 import study.repository.UserBalanceRepository;  9 import study.service.UserBalanceService; 10 import study.service.UserService; 11 12 import javax.annotation.Resource; 13 import java.math.BigDecimal; 14 15 /** 16  * @Description 17  * @author denny 18  * @date 2018/8/31 下午6:30 19 */ 20 @Slf4j 21 @Service 22 public class UserBalanceImpl implements UserBalanceService { 23 24  @Resource 25 private UserService userService; 26  @Resource 27 private UserBalanceRepository userBalanceRepository; 28 29 /** 30  * 建立用戶 31  * 32  * @param userBalance 33  * @return 34 */ 35  @Override 36 public void addUserBalance(UserBalance userBalance) { 37 this.userBalanceRepository.insert(userBalance); 38  } 39 40 /** 41  * 建立用戶並建立帳戶餘額 42  * 43  * @param name 44  * @param balance 45  * @return 46 */ 47 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class) 48  @Override 49 public void addUserBalanceAndUser(String name, BigDecimal balance) { 50 log.info("[addUserBalanceAndUser] begin!!!"); 51 //1.新增用戶 52  userService.addUser(name); 53 //2.新增用戶餘額 54 UserBalance userBalance = new UserBalance(); 55  userBalance.setName(name); 56 userBalance.setBalance(new BigDecimal(1000)); 57 this.addUserBalance(userBalance); 58 log.info("[addUserBalanceAndUser] end!!!"); 59  } 60 }
如上圖所示:

addUserBalanceAndUser(){

  addUser(name);//添加用戶

  addUserBalance(userBalance);//添加用戶餘額
}

addUserBalanceAndUser開啓一個事務,內部方法addUser也申明事務,以下:

UserServiceImpl:

 1 package study.service.impl;  2  3 import lombok.extern.slf4j.Slf4j;  4 import org.springframework.stereotype.Service;  5 import org.springframework.transaction.annotation.Propagation;  6 import org.springframework.transaction.annotation.Transactional;  7 import study.domain.User;  8 import study.repository.UserRepository;  9 import study.service.UserService; 10 11 import javax.annotation.Resource; 12 13 /** 14  * @Description 15  * @author denny 16  * @date 2018/8/27 下午5:31 17 */ 18 @Slf4j 19 @Service 20 public class UserServiceImpl implements UserService{ 21  @Resource 22 private UserRepository userRepository; 23 24 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class) 25  @Override 26 public void addUser(String name) { 27 log.info("[addUser] begin!!!"); 28 User user = new User(); 29  user.setName(name); 30  userRepository.insert(user); 31 log.info("[addUser] end!!!"); 32  } 33 }

 

3.3.2 實測

1.REQUIRED:若是當前沒有事務,就建立一個新事務,若是當前存在事務,就加入該事務,該設置是最經常使用的設置。

外部方法,內部方法都是REQUIRED:

 如上圖所示:外部方法開啓事務,因爲不存在事務,Registering註冊一個新事務;內部方法Fetched獲取已經存在的事務並使用,符合預期。

 

2.SUPPORTS:支持當前事務,若是當前存在事務,就加入該事務,若是當前不存在事務,就以非事務執行。

外部方法required,內部SUPPORTS。

如上圖,外部方法建立一個事務,傳播機制是required,內部方法Participating in existing transaction即加入已存在的外部事務,並最終一塊兒提交事務,符合預期。

3.MANDATORY:支持當前事務,若是當前存在事務,就加入該事務,若是當前不存在事務,就拋出異常

外部沒有事務,內部MANDATORY:

如上圖,外部沒有事務,內部MANDATORY,報錯,符合預期。

 

4.REQUIRES_NEW:建立新事務,若是存在當前事務,則掛起當前事務。新事務執行完畢後,再繼續執行老事務。

外部方法REQUIRED,內部方法REQUIRES_NEW:

如上圖,外部方法REQUIRED建立新事務,內部方法REQUIRES_NEW掛起老事務,建立新事務,新事務完畢後,喚醒老事務繼續執行。符合預期。

 

5.NOT_SUPPORTED:以非事務方式執行操做,若是當前存在事務,就把當前事務掛起。

外部方法REQUIRED,內部方法NOT_SUPPORTED

如上圖,外部方法建立事務A,內部方法不支持事務,掛起事務A,內部方法執行完畢,喚醒事務A繼續執行。符合預期。

 

6.NEVER:以非事務方式執行,若是當前存在事務,則拋出異常。

外部方法REQUIRED,內部方法NEVER:

如上圖,外部方法REQUIRED建立事務,內部方法NEVER若是當前存在事務報錯,符合預期。

 

7.NESTED:若是當前存在事務,則在嵌套事務內執行。若是當前沒有事務,則執行與REQUIRED相似的操做。

外部方法REQUIRED,內部方法NEVER:

如上圖,外部方法REQUIRED建立事務,內部方法NESTED構造一個內嵌事務並建立保存點,內部事務運行完畢釋放保存點,繼續執行外部事務。最終和外部事務一塊兒commit.上圖只有一個sqlSession對象,commit時也是一個。符合預期。

注意:NESTED和REQUIRES_NEW區別?

1.回滾:NESTED在建立內層事務以前建立一個保存點,內層事務回滾只回滾到保存點,不會影響外層事務(真的能夠自動實現嗎?❎具體見下面「強烈注意」!)。外層事務回滾則會連着內層事務一塊兒回滾;REQUIRES_NEW構造一個新事務,和外層事務是兩個獨立的事務,互不影響。

2.提交:NESTED是嵌套事務,是外層事務的子事務。外層事務commit則內部事務一塊兒提交,只有一次commit;REQUIRES_NEW是新事務,徹底獨立的事務,獨立進行2次commit。

強烈注意:

NESTED嵌套事務可以本身回滾到保存點,可是嵌套事務方法中的上拋的異常,外部方法也能捕獲,那麼外部事務也就回滾了,因此若是指望實現內部嵌套異常回滾不影響外部事務,那麼須要捕獲嵌套事務的異常。以下:

 

 1 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
 2     @Override
 3     public void addUserBalanceAndUser(String name, BigDecimal balance) {
 4         log.info("[addUserBalanceAndUser] begin!!!");
 5         //1.新增用戶餘額--》最終會插入成功,不受嵌套回滾異常影響
 6         UserBalance userBalance = new UserBalance();
 7         userBalance.setName(name);
 8         userBalance.setBalance(new BigDecimal(1000));
 9         this.addUserBalance(userBalance);
10         //2.新增用戶,這裏捕獲嵌套事務的異常,不讓外部事務獲取到,否則外部事務確定會回滾!
11         try{
12             // 嵌套事務@Transactional(propagation= Propagation.NESTED, rollbackFor = Exception.class)--》異常會回滾到保存點
13             userService.addUser(name);
14         }catch (Exception e){
15             // 這裏可根據實際狀況添加本身的業務!
16             log.error("嵌套事務【addUser】異常!",e);
17         }
18 
19         log.info("[addUserBalanceAndUser] end!!!");
20     }
相關文章
相關標籤/搜索