最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文帶你了解MySQL之事務隔離級別和MVCC

 更新時間:2023年06月06日 09:20:04   作者:multis  
這篇文章主要帶大家詳細了解一下MySQL之事務隔離級別和MVCC,文中有詳細的代碼示例,具有一定的參考價值,感興趣的同學可以借鑒月u的

一、數(shù)據(jù)準備

為了我們學習的順利進行,我們這邊創(chuàng)建一張 hero

CREATE TABLE hero(
	number INT PRIMARY KEY,
	name VARCHAR(4),
	country VARCHAR(2)
);

這里需要注意的是,我們的 hero表的主鍵是number,而不是id,主要是后邊要用到的事務id做一下區(qū)別,然后我們給這個表里插入一條數(shù)據(jù)

mysql> INSERT INTO hero VALUES(1,'張角','東漢');
Query OK, 1 row affected (0.01 sec

現(xiàn)在我們表里的數(shù)據(jù)就是這樣:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 張角   | 東漢    |
+--------+--------+---------+
1 row in set (0.00 sec)

二、事務隔離級別

我們知道MySQLCS架構的軟件,若干個客戶端與服務器連接上之后,就可以稱之為一個會話Session)。每個客戶端都可以在自己的會話中向服務器發(fā)出請求語句,一個請求語句可能是某個事務的一部分,也就是對于服務器來說可能同時處理多個事務。在事務簡介的章節(jié)中我們說過事務有一個稱之為隔離性的特性,理論上在某個事務對某個數(shù)據(jù)進行訪問時,其他事務應該進行排隊,當該事務提交之后,其他事務才可以繼續(xù)訪問這個數(shù)據(jù)。但是這樣子的話對性能影響太大,我們既想保持事務的隔離性,又想讓服務器在處理訪問同一數(shù)據(jù)的多個事務時性能盡量高些,魚和熊掌不可得兼,舍一部分隔離性而取性能者也。

2.1 事務并發(fā)執(zhí)行遇到的問題

怎么個舍棄法呢?我們先得看一下訪問相同數(shù)據(jù)的事務在不保證串行執(zhí)行(也就是執(zhí)行完一個再執(zhí)行另一個)的情況下可能會出現(xiàn)哪些問題:

臟寫(Dirty Write): 如果一個事務修改了另一個未提交事務修改過的數(shù)據(jù),那就意味著發(fā)生了臟寫,示意圖如下:

Session ASession B
1BEGIN;
2BEGIN;
3UPDATE hero SET name = ‘趙云’ WHERE number = 1;
4UPDATE hero SET name = ‘法正’ WHERE number = 1;
5COMMIT;
6ROLLBACK;

如上表,Session ASession B各開啟了一個事務,Session B中的事務先將number列為1的記錄的name列更新為'趙云',然后Session A中的事務接著又把這條number列為1的記錄的name列更新為法正。如果之后Session B中的事務進行了回滾,那么Session A中的更新也將不復存在,這種現(xiàn)象就稱之為臟寫。這時Session A中的事務就很懵逼,我明明把數(shù)據(jù)更新了,最后也提交事務了,怎么到最后說自己啥也沒干呢?

臟讀(Dirty Read) :如果一個事務讀到了另一個未提交事務修改過的數(shù)據(jù),那就意味著發(fā)生了臟讀,示意圖如下:

Session ASession B
1BEGIN;
2BEGIN;
3UPDATE hero SET name = ‘趙云’ WHERE number = 1;
4SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘趙云’,味的發(fā)生了臟讀)
5COMMIT;
6ROLLBACK;

如上表,Session ASession B各開啟了一個事務,Session B中的事務先將number列為1的記錄的name列更新為'趙云',然后Session A中的事務再去查詢這條number1的記錄,如果讀到列name的值為'趙云',而Session B中的事務稍后進行了回滾,那么Session A中的事務相當于讀到了一個不存在的數(shù)據(jù),這種現(xiàn)象就稱之為臟讀。

不可重復讀(Non-Repeatable Read): 如果一個事務只能讀到另一個已經提交的事務修改過的數(shù)據(jù),并且其他事務每對該數(shù)據(jù)進行一次修改并提交后,該事務都能查詢得到最新值,那就意味著發(fā)生了不可重復讀,示意圖如下:

Session ASession B
1BEGIN;
2SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘張角’)
3UPDATE hero SET name = ‘趙云’ WHERE number = 1;
4SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘趙云’,意味的發(fā)生了不可重復讀)
5UPDATE hero SET name = ‘法正’ WHERE number = 1;
6SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘法正’,意味的發(fā)生了不可重復讀)

如上表,我們在Session B中提交了幾個隱式事務(意味著語句結束事務就提交了),這些事務都修改了number列為1的記錄的列name的值,每次事務提交之后,如果Session A中的事務都可以查看到最新的值,這種現(xiàn)象也被稱之為不可重復讀。

幻讀(Phantom): 如果一個事務先根據(jù)某些條件查詢出一些記錄,之后另一個事務又向表中插入了符合這些條件的記錄,原先的事務再次按照該條件查詢時,能把另一個事務插入的記錄也讀出來,那就意味著發(fā)生了幻讀,示意圖如下:

Session ASession B
1BEGIN;
2SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘張角’)
3INSERT INTO hero VALUES(1,‘許諸’,‘魏國’);
4SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘張角’、‘許諸’,意味的發(fā)生了幻讀)

如上表,Session A中的事務先根據(jù)條件number > 0這個條件查詢表hero,得到了name列值為'張角'的記錄;之后Session B中提交了一個隱式事務,該事務向表hero中插入了一條新記錄;之后Session A中的事務再根據(jù)相同的條件number > 0查詢表hero,得到的結果集中包含Session B中的事務新插入的那條記錄,這種現(xiàn)象也被稱之為幻讀。

Q1: 如果Session B中是刪除了一些符合number > 0的記錄而不是插入新記錄,那Session A中之后再根據(jù)number > 0的條件讀取的記錄變少了,這種現(xiàn)象算不算幻讀呢?

W1: 明確說一下,這種現(xiàn)象不屬于幻讀,幻讀強調的是一個事務按照某個相同條件多次讀取記錄時,后讀取時讀到了之前沒有讀到的記錄。

小提士:
那對于先前已經讀到的記錄,之后又讀取不到這種情況,算啥呢?其實這相當于對每一條記錄都發(fā)生了不可重復讀的現(xiàn)象?;米x只是重點強調了讀取到了之前讀取沒有獲取到的記錄。

2.2 SQL標準中的四種隔離級別

我們上邊介紹了幾種并發(fā)事務執(zhí)行過程中可能遇到的一些問題,這些問題也有輕重緩急之分,我們給這些問題按照嚴重性來排一下序:

臟寫 > 臟讀 > 不可重復讀 > 幻讀

我們上邊所說的舍棄一部分隔離性來換取一部分性能在這里就體現(xiàn)在:設立一些隔離級別,隔離級別越低,越嚴重的問題就越可能發(fā)生。于是制定了一個所謂的SQL標準,在標準中設立了4個隔離級別:

  • READ UNCOMMITTED:未提交讀
  • READ COMMITTED:已提交讀
  • REPEATABLE READ:可重復讀
  • SERIALIZABLE:可串行化

SQL標準中規(guī)定,針對不同的隔離級別,并發(fā)事務可以發(fā)生不同嚴重程度的問題,具體情況如下:

隔離級別臟讀不可重復讀幻讀
READ UNCOMMITTEDPossiblePossiblePossible
READ COMMITTEDNot PossiblePossiblePossible
REPEATABLE READNot PossibleNot PossiblePossible
SERIALIZABLENot PossibleNot PossibleNot Possible

也就是說:

  • READ UNCOMMITTED隔離級別下,可能發(fā)生臟讀、不可重復讀和幻讀問題。
  • READ COMMITTED隔離級別下,可能發(fā)生不可重復讀和幻讀問題,但是不可以發(fā)生臟讀問題。
  • REPEATABLE READ隔離級別下,可能發(fā)生幻讀問題,但是不可以發(fā)生臟讀和不可重復讀的問題。
  • SERIALIZABLE隔離級別下,各種問題都不可以發(fā)生。

Q2: 臟寫是怎么回事兒?怎么里邊都沒寫呢?
W2: 這是因為臟寫這個問題太嚴重了,不論是哪種隔離級別,都不允許臟寫的情況發(fā)生。

2.3 MySQL中支持的四種隔離級別

不同的數(shù)據(jù)庫廠商對SQL標準中規(guī)定的四種隔離級別支持不一樣,比方說Oracle就只支持READ COMMITTED和SERIALIZABLE隔離級別。MySQL雖然支持4種隔離級別,但與SQL標準中所規(guī)定的各級隔離級別允許發(fā)生的問題卻有些出入,MySQL在REPEATABLE READ隔離級別下,是可以禁止幻讀問題的發(fā)生的。

MySQL的默認隔離級別為REPEATABLE READ,我們可以手動修改一下事務的隔離級別。

如何設置事務的隔離級別

我們可以通過下邊的語句修改事務的隔離級別:

SET [GLOBAL|SESSION|PERSIST] TRANSACTION ISOLATION LEVEL level;

其中的level可選值有4個:

level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}

設置事務的隔離級別的語句中,在SET關鍵字后可以放置GLOBAL關鍵字、SESSION關鍵字、PERSIST關鍵字或者什么都不放,這樣會對不同范圍的事務產生不同的影響,具體如下:

  • 使用GLOBAL關鍵字(在全局范圍影響):

比如:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

則:

  • 則:

    • 只對執(zhí)行完該語句之后產生的會話起作用。
    • 當前已經存在的會話無效
    • 重啟之后會恢復到默認值
  • 使用SESSION關鍵字(在會話范圍影響):

    比如:
    SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    則:

    • 對當前會話的所有后續(xù)的事務有效
    • 該語句可以在已經開啟的事務中間執(zhí)行,但不會影響當前正在執(zhí)行的事務
    • 如果在事務之間執(zhí)行,則對后續(xù)的事務有效
    • 重啟之后會恢復到默認值
  • 使用PERSIST關鍵字(使全局系統(tǒng)變量變?yōu)橛谰眯裕?/p>

比如:
SET PERSIST TRANSACTION ISOLATION LEVEL SERIALIZABLE;則:

  • 只對執(zhí)行完該語句之后產生的會話起作用
  • 當前已經存在的會話無效
  • 重啟之后不會恢復到默認值

 小提示:在5.7版本以前在修改全局變量時候,并不會影響到配置文件,重啟之后就會恢復到默認值,MySQL8可以使用PERSIST使全局系統(tǒng)變量變?yōu)橛谰眯?/p>

上述三個關鍵字都不用(只對執(zhí)行語句后的下一個事務產生影響):

比如:SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

則:

  • 只對當前會話中下一個即將開啟的事務有效。
  • 下一個事務執(zhí)行完后,后續(xù)事務將恢復到之前的隔離級別。
  • 該語句不能在已經開啟的事務中間執(zhí)行,會報錯的

想要查看當前會話默認的隔離級別可以通過查看系統(tǒng)變量transaction_isolation的值來確定:

mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+--------------+
| Variable_name         | Value        |
+-----------------------+--------------+
| transaction_isolation | SERIALIZABLE |
+-----------------------+--------------+
1 row in set (0.01 sec)

或者使用更簡便的寫法:

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE            |
+-------------------------+
1 row in set (0.00 sec)

三、MVCC

3.1 版本鏈

我們前邊說過,對于使用InnoDB存儲引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列(row_id并不是必要的,我們創(chuàng)建的表中有主鍵或者非NULL的UNIQUE鍵時都不會包含row_id列):

  • trx_id:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。
  • roll_pointer:每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo日志中,然后這個隱藏列就相當于一個指針,可以通過它來找到該記錄修改前的信息

比如我們的表hero現(xiàn)在只包含一條記錄:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 張角   | 東漢    |
+--------+--------+---------+
1 row in set (0.00 sec)

假設插入該記錄的事務id為80,那么此刻該條記錄的示意圖如下所示:

小提示:
實際上insert undo只在事務回滾時起作用,當事務提交后,該類型的undo日志就沒用了,它占用的Undo Log Segment也會被系統(tǒng)回收(也就是該undo日志占用的Undo頁面鏈表要么被重用,要么被釋放)。雖然真正的insert undo日志占用的存儲空間被釋放了,但是roll_pointer的值并不會被清除,roll_pointer屬性占用7個字節(jié),第一個比特位就標記著它指向的undo日志的類型,如果該比特位的值為1時,就代表著它指向的undo日志類型為insert undo。所以我們之后在畫圖時都會把insert undo給去掉,大家留意一下就好了。

假設之后兩個事務id分別為100、200的事務對這條記錄進行UPDATE操作,操作流程如下:

trx 100trx 200
1BEGIN;
2BEGIN;
3UPDATE hero SET name = ‘趙云’ WHERE number = 1;
4UPDATE hero SET name = ‘法正’ WHERE number = 1;
5COMMIT;
6UPDATE hero SET name = ‘孫尚香’ WHERE number = 1;
7UPDATE hero SET name = ‘妲己’ WHERE number = 1;
8COMMIT;

小提示
能不能在兩個事務中交叉更新同一條記錄呢?哈哈,這不就是一個事務修改了另一個未提交事務修改過的數(shù)據(jù),淪為了臟寫了么?InnoDB使用鎖來保證不會有臟寫情況的發(fā)生,也就是在第一個事務更新了某條記錄后,就會給這條記錄加鎖,另一個事務再次更新時就需要等待第一個事務提交了,把鎖釋放之后才可以繼續(xù)更新。關于鎖的更多細節(jié)我們后續(xù)的文章中再進行學習~

每次對記錄進行更新,都會記錄一條undo日志,每條undo日志也都有一個roll_pointer屬性(INSERT操作對應的undo日志沒有該屬性,因為該記錄并沒有更早的版本),可以將這些undo日志都連起來,串成一個鏈表,所以現(xiàn)在的情況就像下圖一樣:

對該記錄每次更新后,都會將舊值放到一條undo日志中,就算是該記錄的一個舊版本,隨著更新次數(shù)的增多,所有的版本都會被roll_pointer屬性連接成一個鏈表,我們把這個鏈表稱之為版本鏈,版本鏈的頭節(jié)點就是當前記錄最新的值。另外,每個版本中還包含生成該版本時對應的事務id,這個信息很重要,我們稍后就會用到。

3.2 ReadView

對于使用READ UNCOMMITTED隔離級別的事務來說,由于可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好了;對于使用SERIALIZABLE隔離級別的事務來說,InnoDB的規(guī)定使用加鎖的方式來訪問記錄(加鎖是啥我們后續(xù)文章中說哈);對于使用READ COMMITTEDREPEATABLE READ隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:需要判斷一下版本鏈中的哪個版本是當前事務可見的。為此,InnoDB提出了一個ReadView的概念,這個ReadView中主要包含4個比較重要的內容:

  • m_ids:表示在生成ReadView時當前系統(tǒng)中活躍的讀寫事務的事務id列表。

  • min_trx_id:表示在生成ReadView時當前系統(tǒng)中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。

  • max_trx_id:表示生成ReadView時系統(tǒng)中應該分配給下一個事務的id值。

  • 小提示:
    注意max_trx_id并不是m_ids中的最大值,事務id是遞增分配的。比方說現(xiàn)在有id為1,2,3這三個事務,之后id為3的事務提交了。那么一個新的讀事務在生成ReadView時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。

  • creator_trx_id:表示生成該ReadView的事務的事務id。

    小提示:
    我們前邊說過,只有在對表中的記錄做改動時(執(zhí)行INSERT、DELETE、UPDATE這些語句時)才會為事務分配事務id,否則在一個只讀事務中的事務id值都默認為0。

有了這個ReadView,這樣在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:

  • 如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問。

  • 如果被訪問版本的trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務訪問。

  • 如果被訪問版本的trx_id屬性值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView后才開啟,所以該版本不可以被當前事務訪問。

  • 如果被訪問版本的trx_id屬性值在ReadViewmin_trx_idmax_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創(chuàng)建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時生成該版本的事務已經被提交,該版本可以被訪問。

如果某個版本的數(shù)據(jù)對當前事務不可見的話,那就順著版本鏈找到下一個版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本。如果最后一個版本也不可見的話,那么就意味著該條記錄對該事務完全不可見,查詢結果就不包含該記錄。

MySQL中,READ COMMITTEDREPEATABLE READ隔離級別的的一個非常大的區(qū)別就是它們生成ReadView的時機不同。我們還是以表hero為例來,假設現(xiàn)在表hero中只有一條由事務id80的事務插入的一條記錄:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 張角   | 東漢    |
+--------+--------+---------+
1 row in set (0.00 sec)

下來看一下READ COMMITTEDREPEATABLE READ所謂的生成ReadView的時機不同到底不同在哪里

3.2.1 READ COMMITTED

對于使用REPEATABLE COMMITTED隔離級別的事務來說,每次讀取數(shù)據(jù)前都生成一個ReadView。比方說現(xiàn)在系統(tǒng)里有兩個事務id分別為100、200的事務在執(zhí)行:

# Transaction 100
BEGIN;
UPDATE hero SET name = '趙云' WHERE number = 1;
UPDATE hero SET name = '法正' WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
...

小提示:
再次強調一遍,事務執(zhí)行過程中,只有在第一次真正修改記錄時(比如使用INSERT、DELETE、UPDATE語句),才會被分配一個單獨的事務id,這個事務id是遞增的。所以我們才在Transaction 200中更新一些別的表的記錄,目的是讓它分配事務id。

此刻,表heronumber1的記錄得到的版本鏈表如下所示:

假設現(xiàn)在有一個使用READ COMMITTED隔離級別的事務開始執(zhí)行:

# 使用READ COMMITTED隔離級別的事務
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張角'

這個SELECT1的執(zhí)行過程如下:

  • 在執(zhí)行SELECT語句時會先生成一個ReadViewReadViewm_ids列表的內容就是[100, 200],min_trx_id100,max_trx_id201,creator_trx_id0。

  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'法正',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據(jù)roll_pointer跳到下一個版本。

  • 下一個版本的列name的內容是'趙云',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續(xù)跳到下一個版本。

    下一個版本的列name的內容是'張角',該版本的trx_id值為80,小于ReadView中的min_trx_id100,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name'張角'的記錄。

# Transaction 100
BEGIN;
UPDATE hero SET name = '趙云' WHERE number = 1;
UPDATE hero SET name = '法正' WHERE number = 1;
COMMIT;

然后再到事務id200的事務中更新一下表heronumber為1的記錄:

# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
UPDATE hero SET name = '孫尚香' WHERE number = 1;
UPDATE hero SET name = '妲己' WHERE number = 1;

此刻,表heronumber1的記錄的版本鏈就長這樣:

然后再到剛才使用READ COMMITTED隔離級別的事務中繼續(xù)查找這個number1的記錄,如下:

# 使用READ COMMITTED隔離級別的事務
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張角'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'法正'

這個SELECT2的執(zhí)行過程如下:

  • 在執(zhí)行SELECT語句時會又會單獨生成一個ReadView,該ReadViewm_ids列表的內容就是[200]事務id100的那個事務已經提交了,所以再次生成快照時就沒有它了),min_trx_id200max_trx_id201,creator_trx_id0。

  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'妲己',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據(jù)roll_pointer跳到下一個版本。

  • 下一個版本的列name的內容是'孫尚香',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續(xù)跳到下一個版本。

  • 下一個版本的列name的內容是'法正',該版本的trx_id值為100,小于ReadView中的min_trx_id200,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name'法正'的記錄。

以此類推,如果之后事務id200的記錄也提交了,再次在使用READ COMMITTED隔離級別的事務中查詢表heronumber值為1的記錄時,得到的結果就是'妲己'了,具體流程我們就不分析了。總結一下就是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的ReadView。

3.2.2 REPEATABLE READ

對于使用REPEATABLE READ隔離級別的事務來說,只會在第一次執(zhí)行查詢語句時生成一個ReadView,之后的查詢就不會重復生成了。比方說現(xiàn)在系統(tǒng)里有兩個事務id分別為100、200的事務在執(zhí)行:

# Transaction 100
BEGIN;
UPDATE hero SET name = '趙云' WHERE number = 1;
UPDATE hero SET name = '法正' WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
...

此刻,表heronumber1的記錄得到的版本鏈表如下所示:

假設現(xiàn)在有一個使用REPEATABLE READ隔離級別的事務開始執(zhí)行:

# 使用REPEATABLE READ隔離級別的事務
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張角'

這個SELECT1的執(zhí)行過程如下:

  • 在執(zhí)行SELECT語句時會先生成一個ReadView,ReadViewm_ids列表的內容就是[100, 200],min_trx_id100,max_trx_id201,creator_trx_id0

  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'法正',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據(jù)roll_pointer跳到下一個版本。

  • 下一個版本的列name的內容是'趙云',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續(xù)跳到下一個版本。

  • 下一個版本的列name的內容是'張角',該版本的trx_id值為80,小于ReadView中的min_trx_id100,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name'張角'的記錄。

之后,我們把事務id100的事務提交一下:

# Transaction 100
BEGIN;
UPDATE hero SET name = '關羽' WHERE number = 1;
UPDATE hero SET name = '張飛' WHERE number = 1;
COMMIT;

然后再到事務id200的事務中更新一下表heronumber1的記錄:

# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
...
UPDATE hero SET name = '孫尚香' WHERE number = 1;
UPDATE hero SET name = '妲己' WHERE number = 1;

此刻,表heronumber1的記錄的版本鏈就長這樣:

然后再到剛才使用REPEATABLE READ隔離級別的事務中繼續(xù)查找這個number1的記錄,如下:

# 使用REPEATABLE READ隔離級別的事務
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張角'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍為'張角'

這個SELECT2的執(zhí)行過程如下:

  • 因為當前事務的隔離級別為REPEATABLE READ,而之前在執(zhí)行SELECT1時已經生成過ReadView了,所以此時直接復用之前的ReadView,之前的ReadViewm_ids列表的內容就是[100, 200]min_trx_id100,max_trx_id201creator_trx_id0

  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'妲己',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據(jù)roll_pointer跳到下一個版本。

  • 下一個版本的列name的內容是'孫尚香',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續(xù)跳到下一個版本。

    下一個版本的列name的內容是'法正',該版本的trx_id值為100,而m_ids列表中是包含值為100事務id的,所以該版本也不符合要求,同理下一個列name的內容是'趙云'的版本也不符合要求。繼續(xù)跳到下一個版本。

  • 下一個版本的列name的內容是'張角',該版本的trx_id值為80,小于ReadView中的min_trx_id100,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列‘name’'張角'的記錄。

也就是說兩次SELECT查詢得到的結果是重復的,記錄的列name值都是'張角',這就是可重復讀的含義。如果我們之后再把事務id200的記錄提交了,然后再到剛才使用REPEATABLE READ隔離級別的事務中繼續(xù)查找這個number1的記錄,得到的結果還是'張角',具體執(zhí)行過程大家可以自己分析一下。

3.3 MVCC小結

從上邊的描述中我們可以看出來,所謂的MVCCMulti-Version Concurrency Control ,多版本并發(fā)控制)指的就是在使用READ COMMITTD、REPEATABLE READ這兩種隔離級別的事務在執(zhí)行普通的SELECT操作時訪問記錄的版本鏈的過程,這樣子可以使不同事務的讀-寫、寫-讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。READ COMMITTDREPEATABLE READ這兩個隔離級別的一個很大不同就是:生成ReadView的時機不同,READ COMMITTD在每一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView,之后的查詢操作都重復使用這個ReadView就好了。

小提示:
我們之前說執(zhí)行DELETE語句或者更新主鍵的UPDATE語句并不會立即把對應的記錄完全從頁面中刪除,而是執(zhí)行一個所謂的delete mark操作,相當于只是對記錄打上了一個刪除標志位,這主要就是為MVCC服務的,大家可以對比上邊舉的例子自己試想一下怎么使用。
另外,所謂的MVCC只是在我們進行普通的SEELCT查詢時才生效,截止到目前我們所見的所有SELECT語句都算是普通的查詢,至于啥是個不普通的查詢,我們稍后再說哈~

四、關于purge

大家有沒有發(fā)現(xiàn)兩件事兒:

  • 我們說insert undo在事務提交之后就可以被釋放掉了,而update undo由于還需要支持MVCC,不能立即刪除掉。

    我們在學習undo日志的時候,一個事務寫的一組undo日志中都有一個Undo Log Header部分,這個Undo Log Header中有一個名為TRX_UNDO_HISTORY_NODE的屬性,表示名為History鏈表的節(jié)點。當一個事務提交之后,就會把這個事務的執(zhí)行過程產生的這一組Update undo日志插入到History鏈表的頭部

    我們還說過,每個回滾段都對應一個名為Rollbcak Segment Header的頁面,這個頁面中有兩個屬性:

    • TRX_RSEG_HISTORY:表示Histroy鏈表的基節(jié)點

    • TRX_RSEG_HISTORY_SIZE:表示Histroy鏈表占用的頁面數(shù)量

    也就是說每個回滾段都有一個History鏈表,一個事務在某個回滾段中寫入的一組update undo日志在該事物提交之后就會加入這個回滾段的History鏈表。系統(tǒng)中存在很多回滾段,這也意味著可能存在很多個History鏈表。不過加入到History鏈表的update undo日志所占用的存儲空間頁沒有釋放掉,他們總不能一直存在吧,那得用多大的存儲空間存放這些undo日志。

  • 為了支持MVCC,對于delete mark操作來說,僅僅是在記錄上打一個刪除標記,并沒有真正將它刪除掉。

    大家應該還記得,在一組undo日志中的Undo Log Header部分有個名為TRX_UNDO_DEL_MARKS的屬性,用來標記本組的undo日志中是否包含因delete mark操作二產生的日志,這些打了刪除標記的記錄也不能一直存在吧?

為了節(jié)約空間,我們應該在合適的時候把update undo日志以及僅僅被標記為刪除的記錄徹底刪除,這個刪除操作就是purge不過于問題的關健在于這個合適的時候到底是什么時候?

update undo日志和被標記刪除的記錄都是為了支持MVCC而存在的,只要系統(tǒng)中最早產生的ReadView不在訪問它們,它們的使命就此結束了,就可以丟進歷史的垃圾堆里了。一個ReadView在什么時候才肯定不會訪問某個事物執(zhí)行過程中產生的undo日志呢?其實,只要我們能保證生成ReadView時,某個事物已經提交,那么該ReadView肯定就不需要訪問事物運行過程中產生的undo日志了(因為該事物所改動的最新版均對該ReadView可見)。

InnoDB為此做了兩件事:

  • 在一個事務提交時,會為這個事務生成一個名為事務no的值,該值用來表示事務提交的順序。先提交的事務的事務no值小,后提交的事務的事務no值大。

    別忘看在一組undo日志中對應的Undo Log Header部分有一個名為TRX_UNDO_TRX_NO的屬性。當事務提交時,就把該事物對應的事務no值填到這個屬性中,因為事務no代表著各個事務提交的順序,而History鏈表又是按照事務提交的順序來排列各種undo日志,所以History鏈表中各組的undo日志也是按照對應的事務no來排序的。

  • 一個ReadView結構除了包含前面學習的幾個屬性之外,還會包含一個事務no的屬性。在生成一個ReadView時,會把比當前系統(tǒng)中最大的事務no的值還大1的值賦予這個屬性。

InnoDB還把當前系統(tǒng)中所有的ReadView按照創(chuàng)建時間連成了一個鏈表。當執(zhí)行purge操作時(這個purge操作是在專門的后線程中執(zhí)行的),就把系統(tǒng)中最早生成的ReadView給取出來。如果當前系統(tǒng)中不存在ReadView,那就現(xiàn)場創(chuàng)建一個(新創(chuàng)建的這個ReadView的事務no值肯定比當前已經提交的事務的事務no值大)。然后從各個回滾段的History鏈表中取出事務no值較小的各組undo日志。如果第一組undo日志的事務no小于當前系統(tǒng)最早生成的ReadView事務no,就意味著該組undo日志沒有用了,就會從History鏈表中移除,并且釋放掉它們占用的存儲空間。如果該組undo日志包含因delete mark操作而產生的undo日志(TRX_UNDO_DEL_MARKS的屬性值為1),那么也需要將對應的標記為刪除的記錄給徹底刪除。

這里我們需要注意的一點,當前系統(tǒng)中系統(tǒng)最早生成的ReadView決定了purge操作中可以清理哪些update undo日志以及打了刪除標記的記錄。如果某個事物使用的REPEATABLE READ隔離級別,那么該事物一直復用最初產生的ReadView。假如這個事務運行了很久,一直沒有提交,那么最早產生的ReadView會一直不釋放。系統(tǒng)中updtae undo日志和打了刪除標記的記錄就會越來越多,表空間對應的文件也會越來越大,一條記錄的版本鏈將越來越長,從而影響系統(tǒng)的性能。

五、總結

  • 并發(fā)的事務在運行過程中會出現(xiàn)一些可能引發(fā)一致性問題的現(xiàn)象,具體如下:

    • 臟寫:一個事務修改了另一個未提交事務修改過的數(shù)據(jù)
    • 臟讀:一個事務讀到了另一個未提交事務修改過的數(shù)據(jù)
    • 不可重復讀:一個事務修改了另一個未提交事務讀取的數(shù)據(jù)
    • 幻讀:一個事務先根據(jù)某些搜索條件查詢出一些記錄,在該事務未提交時,另一個事務寫入了一些符合那些搜索條件的記錄
  • SOL標準中的4種隔離級別:

    • READ UNCOMMITTED:可能發(fā)生臟讀、不可重復讀和幻讀現(xiàn)象

    • READCOMMITTED:可能發(fā)生不可重復讀和幻讀現(xiàn)象,但是不可能發(fā)生臟讀現(xiàn)象。

    • REPEATABLE READ:可能發(fā)生幻讀現(xiàn)象,但是不可能發(fā)生臟讀和不可重復讀的現(xiàn)象

    • SERIALIZABLE:各種現(xiàn)象都不可以發(fā)生。

    實際上,MySOLREPEATABLE READ隔離級別下是可以在很大程度上禁止出現(xiàn)幻讀現(xiàn)象的。

  • 下面的語句用來設置事務的隔離級別:
    SET [GLOBALI|SESSION|PERSIST] TRANSACTION ISOLATION LEVEL Level;

  • 聚簇索引記錄和undo日志中的roll pointer屬性可以串連成一個記錄的版本鏈

  • 通過生成ReadView來判斷記錄的某個版本的可見性,其中READ COMMITTD在每一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView,之后的查詢操作都重復使用這個ReadView

  • 當前系統(tǒng)中,如果最早生成的 ReadView 不再訪問undo日志以及打了刪除標記的記錄則可以通過purge操作將它們清除。

以上就是一文帶你了解MySQL之事務隔離級別和MVCC的詳細內容,更多關于MySQL 事務隔離級別和MVCC的資料請關注腳本之家其它相關文章!

相關文章

  • MySQL數(shù)據(jù)庫 Load Data 多種用法

    MySQL數(shù)據(jù)庫 Load Data 多種用法

    這篇文章主要介紹MySQL數(shù)據(jù)庫中 Load Data 數(shù)據(jù)的幾種種用法,需要的朋友可以參考下面文章的解說
    2021-08-08
  • mysql中CONCAT()函數(shù)結果為null的問題解決

    mysql中CONCAT()函數(shù)結果為null的問題解決

    本文主要介紹了因SQL中的CONCAT()函數(shù)導致的數(shù)據(jù)漏查問題,下面就拉具體介紹一下問題的原因及解決方法,感興趣的可以了解一下
    2025-09-09
  • MySQL復制架構的搭建及配置過程

    MySQL復制架構的搭建及配置過程

    這篇文章主要介紹了MySQL復制架構的相關知識,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-02-02
  • mysql 8.0.16 winx64安裝配置方法圖文教程

    mysql 8.0.16 winx64安裝配置方法圖文教程

    這篇文章主要為大家分享了mysql 8.0.16 安裝配置方法圖文教程,具有一定的參考價值,感興趣的朋友可以參考一下
    2019-05-05
  • MySQL日期時間函數(shù)知識匯總

    MySQL日期時間函數(shù)知識匯總

    這篇文章主要介紹了MySQL日期時間函數(shù)知識匯總,這不同數(shù)據(jù)庫之間基本相同,只會有個別函數(shù)的差異。下文詳細介紹,需要的小伙伴可以參考一下
    2022-03-03
  • 一看就懂的MySQL的聚簇索引及聚簇索引是如何長高的

    一看就懂的MySQL的聚簇索引及聚簇索引是如何長高的

    聚簇索引不是一種單獨的索引類型,而是一種數(shù)據(jù)存儲方式。innodb的聚簇索引實際上在同一個結構中保存了B-tree索引和數(shù)據(jù)行。通過本文學習MySQL的聚簇索引及聚簇索引是如何長高的,感興趣的朋友一起學習下吧
    2021-05-05
  • MySQL如何使用時間作為判斷條件

    MySQL如何使用時間作為判斷條件

    這篇文章主要介紹了MySQL如何使用時間作為判斷條件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-11-11
  • MySQL實現(xiàn)每天定時12點彈出黑窗口

    MySQL實現(xiàn)每天定時12點彈出黑窗口

    這篇文章主要介紹了MySQL實現(xiàn)每天定時12點彈出黑窗口問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-05-05
  • Navicat連接MySQL時報10060、1045錯誤及my.ini位置問題

    Navicat連接MySQL時報10060、1045錯誤及my.ini位置問題

    這篇文章主要介紹了Navicat連接MySQL的10060及1045報錯,my.ini位置,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-03-03
  • MySQL 整表加密解決方案 keyring_file詳解

    MySQL 整表加密解決方案 keyring_file詳解

    這篇文章主要介紹了MySQL 整表加密解決方案 keyring_file詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-07-07

最新評論

横峰县| 龙胜| 长宁区| 仁怀市| 文山县| 桑植县| 萍乡市| 宁强县| 黄山市| 曲靖市| 浙江省| 响水县| 紫金县| 宁南县| 翼城县| 富蕴县| 龙岩市| 韶山市| 岳普湖县| 平昌县| 循化| 托克托县| 英超| 新营市| 和政县| 鄂托克旗| 凤山县| 三亚市| 文水县| 武宣县| 芮城县| 渭源县| 昭通市| 金堂县| 宁德市| 仁布县| 丰顺县| 福建省| 和硕县| 扶绥县| 扎囊县|