1. 程式人生 > >spring事務詳解(四)測試驗證

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

系列目錄

spring事務詳解(五)總結提高

一、引子

在第一節中我們知道spring為了支援資料庫事務的ACID四大特性,在底層原始碼中對事務定義了6個屬性:事務名稱隔離級別超時時間是否只讀傳播機制回滾機制。其中隔離級別傳播機制光看第一節的描述還是不夠的,需要實際測試一下方能放心且記憶深刻。

二、環境

2.1 業務模擬

模擬使用者去銀行轉賬,使用者A轉賬給使用者B,

需要保證使用者A扣款,使用者B加款同時成功或失敗回滾。

2.2 環境準備

測試環境

mysql8+mac,測試時使用的mysql8(和mysql5.6的設定事務變數的語句不同,不用太在意)

測試準備

建立一個數據庫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.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級別下,讀也加鎖!如果是行鎖(查詢一行),那麼後續對這一行的修改操作會直接阻塞等待第一個事務完畢。如果是表鎖(查詢整張表),那麼後續對這張表的所有修改操作都阻塞等待。可見僅僅一個查詢就鎖住了相應的查詢資料,效能實在是不敢恭維。

四、七種傳播機制實測

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     }