1.4.事務(wù)和事務(wù)的隔離級別
1.4.1.為什么需要事務(wù)
事務(wù)是數(shù)據(jù)庫管理系統(tǒng)(DBMS)執(zhí)行過程中的一個邏輯單位(不可再進行分割),由一個有限的數(shù)據(jù)庫操作序列構(gòu)成(多個DML語句,select語句不包含事務(wù)),要不全部成功,要不全部不成功。
A 給B 要劃錢,A 的賬戶-1000元, B 的賬戶就要+1000元,這兩個update 語句必須作為一個整體來執(zhí)行,不然A 扣錢了,B 沒有加錢這種情況就是錯誤的。那么事務(wù)就可以保證A 、B 賬戶的變動要么全部一起發(fā)生,要么全部一起不發(fā)生。
1.4.2.事務(wù)特性
事務(wù)應(yīng)該具有4個屬性:原子性、一致性、隔離性、持久性。這四個屬性通常稱為ACID特性。
l 原子性(atomicity)
l 一致性(consistency)
l 隔離性(isolation)
l 持久性(durability)
1.4.2.1.原子性(atomicity)
一個事務(wù)必須被視為一個不可分割的最小單元,整個事務(wù)中的所有操作要么全部提交成功,要么全部失敗,對于一個事務(wù)來說,不能只執(zhí)行其中的一部分操作。比如:
連老師借給李老師1000元:
1.連老師工資卡扣除1000元
2.李老師工資卡增加1000元
整個事務(wù)的操作要么全部成功,要么全部失敗,不能出現(xiàn)連老師工資卡扣除,但是李老師工資卡不增加的情況。如果原子性不能保證,就會很自然的出現(xiàn)一致性問題。
1.4.2.2.一致性(consistency)
一致性是指事務(wù)將數(shù)據(jù)庫從一種一致性轉(zhuǎn)換到另外一種一致性狀態(tài),在事務(wù)開始之前和事務(wù)結(jié)束之后數(shù)據(jù)庫中數(shù)據(jù)的完整性沒有被破壞。
連老師借給李老師1000元:
1.連老師工資卡扣除1000元
2.李老師工資卡增加1000元
扣除的錢(-500) 與增加的錢(500) 相加應(yīng)該為0,或者說連老師和李老師的賬戶的錢加起來,前后應(yīng)該不變。
1.4.2.3.持久性(durability)
一旦事務(wù)提交,則其所做的修改就會永久保存到數(shù)據(jù)庫中。此時即使系統(tǒng)崩潰,已經(jīng)提交的修改數(shù)據(jù)也不會丟失。
1.4.2.4.隔離性(isolation)
一個事務(wù)的執(zhí)行不能被其他事務(wù)干擾。即一個事務(wù)內(nèi)部的操作及使用的數(shù)據(jù)對并發(fā)的其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個事務(wù)之間不能互相干擾。
如果隔離性不能保證,會導(dǎo)致什么問題?
連老師借給李老師生活費,借了兩次,每次都是1000,連老師的卡里開始有10000,李老師的卡里開始有500,從理論上,借完后,連老師的卡里有8000,李老師的卡里應(yīng)該有2500。
我們將連老師向李老師同時進行的兩次轉(zhuǎn)賬操作分別稱為T1和T2,在現(xiàn)實世界中T1和T2是應(yīng)該沒有關(guān)系的,可以先執(zhí)行完T1,再執(zhí)行T2,或者先執(zhí)行完T2,再執(zhí)行T1,結(jié)果都是一樣的。但是很不幸,真實的數(shù)據(jù)庫中T1和T2的操作可能交替執(zhí)行的,執(zhí)行順序就有可能是:
如果按照上圖中的執(zhí)行順序來進行兩次轉(zhuǎn)賬的話,最終我們看到,連老師的賬戶里還剩9000元錢,相當(dāng)于只扣了1000元錢,但是李老師的賬戶里卻成了2500元錢,多了10000元,這銀行豈不是要虧死了?
所以對于現(xiàn)實世界中狀態(tài)轉(zhuǎn)換對應(yīng)的某些數(shù)據(jù)庫操作來說,不僅要保證這些操作以原子性的方式執(zhí)行完成,而且要保證其它的狀態(tài)轉(zhuǎn)換不會影響到本次狀態(tài)轉(zhuǎn)換,這個規(guī)則被稱之為隔離性。
1.4.3.事務(wù)并發(fā)引發(fā)的問題
我們知道MySQL是一個客戶端/服務(wù)器架構(gòu)的軟件,對于同一個服務(wù)器來說,可以有若干個客戶端與之連接,每個客戶端與服務(wù)器連接上之后,就可以稱之為一個會話(Session)。每個客戶端都可以在自己的會話中向服務(wù)器發(fā)出請求語句,一個請求語句可能是某個事務(wù)的一部分,也就是對于服務(wù)器來說可能同時處理多個事務(wù)。
在上面我們說過事務(wù)有一個稱之為隔離性的特性,理論上在某個事務(wù)對某個數(shù)據(jù)進行訪問時,其他事務(wù)應(yīng)該進行排隊,當(dāng)該事務(wù)提交之后,其他事務(wù)才可以繼續(xù)訪問這個數(shù)據(jù),這樣的話并發(fā)事務(wù)的執(zhí)行就變成了串行化執(zhí)行。
但是對串行化執(zhí)行性能影響太大,我們既想保持事務(wù)的一定的隔離性,又想讓服務(wù)器在處理訪問同一數(shù)據(jù)的多個事務(wù)時性能盡量高些,當(dāng)我們舍棄隔離性的時候,可能會帶來什么樣的數(shù)據(jù)問題呢?
1.4.3.1.臟讀
當(dāng)一個事務(wù)讀取到了另外一個事務(wù)修改但未提交的數(shù)據(jù),被稱為臟讀。
1、在事務(wù)A執(zhí)?過程中,事務(wù)A對數(shù)據(jù)資源進?了修改,事務(wù)B讀取了事務(wù)A修改后的數(shù)據(jù)。
2、由于某些原因,事務(wù)A并沒有完成提交,發(fā)?了RollBack操作,則事務(wù)B讀取的數(shù)據(jù)就是臟數(shù)據(jù)。
這種讀取到另?個事務(wù)未提交的數(shù)據(jù)的現(xiàn)象就是臟讀(Dirty Read)。
1.4.3.2.不可重復(fù)讀
當(dāng)事務(wù)內(nèi)相同的記錄被檢索兩次,且兩次得到的結(jié)果不同時,此現(xiàn)象稱為不可重復(fù)讀。
事務(wù)B讀取了兩次數(shù)據(jù)資源,在這兩次讀取的過程中事務(wù)A修改了數(shù)據(jù),導(dǎo)致事務(wù)B在這兩次讀取出來的
數(shù)據(jù)不?致。
1.4.3.3.幻讀
在事務(wù)執(zhí)行過程中,另一個事務(wù)將新記錄添加到正在讀取的事務(wù)中時,會發(fā)生幻讀。
事務(wù)B前后兩次讀取同?個范圍的數(shù)據(jù),在事務(wù)B兩次讀取的過程中事務(wù)A新增了數(shù)據(jù),導(dǎo)致事務(wù)B后?
次讀取到前?次查詢沒有看到的?。
幻讀和不可重復(fù)讀有些類似,但是幻讀重點強調(diào)了讀取到了之前讀取沒有獲取到的記錄。
1.1.4.SQL標(biāo)準(zhǔn)中的四種隔離級別
我們上邊介紹了幾種并發(fā)事務(wù)執(zhí)行過程中可能遇到的一些問題,這些問題也有輕重緩急之分,我們給這些問題按照嚴(yán)重性來排一下序:
臟讀 > 不可重復(fù)讀 > 幻讀
我們上邊所說的舍棄一部分隔離性來換取一部分性能在這里就體現(xiàn)在:設(shè)立一些隔離級別,隔離級別越低,越嚴(yán)重的問題就越可能發(fā)生。有一幫人(并不是設(shè)計MySQL的大叔們)制定了一個所謂的SQL標(biāo)準(zhǔn),在標(biāo)準(zhǔn)中設(shè)立了4個隔離級別:
READ UNCOMMITTED:未提交讀。
READ COMMITTED:已提交讀。
REPEATABLE READ:可重復(fù)讀。
SERIALIZABLE:可串行化。
SQL標(biāo)準(zhǔn)中規(guī)定,針對不同的隔離級別,并發(fā)事務(wù)可以發(fā)生不同嚴(yán)重程度的問題,具體情況如下:
也就是說:
READ UNCOMMITTED隔離級別下,可能發(fā)生臟讀、不可重復(fù)讀和幻讀問題。
READ COMMITTED隔離級別下,可能發(fā)生不可重復(fù)讀和幻讀問題,但是不可以發(fā)生臟讀問題。
REPEATABLE READ隔離級別下,可能發(fā)生幻讀問題,但是不可以發(fā)生臟讀和不可重復(fù)讀的問題。
SERIALIZABLE隔離級別下,各種問題都不可以發(fā)生。
1.4.5.MySQL中的隔離級別
不同的數(shù)據(jù)庫廠商對SQL標(biāo)準(zhǔn)中規(guī)定的四種隔離級別支持不一樣,比方說Oracle就只支持READ COMMITTED和SERIALIZABLE隔離級別。本書中所討論的MySQL雖然支持4種隔離級別,但與SQL標(biāo)準(zhǔn)中所規(guī)定的各級隔離級別允許發(fā)生的問題卻有些出入,MySQL在REPEATABLE READ隔離級別下,是可以禁止幻讀問題的發(fā)生的。
MySQL的默認(rèn)隔離級別為REPEATABLE READ,我們可以手動修改事務(wù)的隔離級別。
1.4.5.1.如何設(shè)置事務(wù)的隔離級別
我們可以通過下邊的語句修改事務(wù)的隔離級別:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
其中的level可選值有4個:
level: {
REPEATABLE READ
| READ COMMITTED
| READ UNCOMMITTED
| SERIALIZABLE
}
設(shè)置事務(wù)的隔離級別的語句中,在SET關(guān)鍵字后可以放置GLOBAL關(guān)鍵字、SESSION關(guān)鍵字或者什么都不放,這樣會對不同范圍的事務(wù)產(chǎn)生不同的影響,具體如下:
使用GLOBAL關(guān)鍵字(在全局范圍影響):
比方說這樣:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則: 只對執(zhí)行完該語句之后產(chǎn)生的會話起作用。當(dāng)前已經(jīng)存在的會話無效。
使用SESSION關(guān)鍵字(在會話范圍影響):
比方說這樣:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:對當(dāng)前會話的所有后續(xù)的事務(wù)有效
該語句可以在已經(jīng)開啟的事務(wù)中間執(zhí)行,但不會影響當(dāng)前正在執(zhí)行的事務(wù)。
如果在事務(wù)之間執(zhí)行,則對后續(xù)的事務(wù)有效。
上述兩個關(guān)鍵字都不用(只對執(zhí)行語句后的下一個事務(wù)產(chǎn)生影響):
比方說這樣:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:只對當(dāng)前會話中下一個即將開啟的事務(wù)有效。下一個事務(wù)執(zhí)行完后,后續(xù)事務(wù)將恢復(fù)到之前的隔離級別。該語句不能在已經(jīng)開啟的事務(wù)中間執(zhí)行,會報錯的。
如果我們在服務(wù)器啟動時想改變事務(wù)的默認(rèn)隔離級別,可以修改啟動參數(shù)transaction-isolation的值,比方說我們在啟動服務(wù)器時指定了–transaction-isolation=SERIALIZABLE,那么事務(wù)的默認(rèn)隔離級別就從原來的REPEATABLE READ變成了SERIALIZABLE。
想要查看當(dāng)前會話默認(rèn)的隔離級別可以通過查看系統(tǒng)變量transaction_isolation的值來確定:
SHOW VARIABLES LIKE 'transaction_isolation';
或者使用更簡便的寫法:
SELECT @@transaction_isolation;
注意:transaction_isolation是在MySQL 5.7.20的版本中引入來替換tx_isolation的,如果你使用的是之前版本的MySQL,請將上述用到系統(tǒng)變量transaction_isolation的地方替換為tx_isolation。
1.4.6.MySQL事務(wù)
1.4.6.1.事務(wù)基本語法
事務(wù)開始
1、begin
2、START TRANSACTION(推薦)
3、begin work
事務(wù)回滾
rollback
事務(wù)提交
commit
使用事務(wù)插入兩行數(shù)據(jù),commit后數(shù)據(jù)還在
使用事務(wù)插入兩行數(shù)據(jù),rollback后數(shù)據(jù)沒有了
1.4.6.2.保存點
如果你開啟了一個事務(wù),執(zhí)行了很多語句,忽然發(fā)現(xiàn)某條語句有點問題,你只好使用ROLLBACK語句來讓數(shù)據(jù)庫狀態(tài)恢復(fù)到事務(wù)執(zhí)行之前的樣子,然后一切從頭再來,但是可能根據(jù)業(yè)務(wù)和數(shù)據(jù)的變化,不需要全部回滾。所以MySQL里提出了一個保存點(英文:savepoint)的概念,就是在事務(wù)對應(yīng)的數(shù)據(jù)庫語句中打幾個點,我們在調(diào)用ROLLBACK語句時可以指定會滾到哪個點,而不是回到最初的原點。定義保存點的語法如下:
SAVEPOINT 保存點名稱;
當(dāng)我們想回滾到某個保存點時,可以使用下邊這個語句(下邊語句中的單詞WORK和SAVEPOINT是可有可無的):
ROLLBACK TO [SAVEPOINT] 保存點名稱;
不過如果ROLLBACK語句后邊不跟隨保存點名稱的話,會直接回滾到事務(wù)執(zhí)行之前的狀態(tài)。
如果我們想刪除某個保存點,可以使用這個語句:
RELEASE SAVEPOINT 保存點名稱;
1.4.6.3.隱式提交
當(dāng)我們使用START TRANSACTION或者BEGIN語句開啟了一個事務(wù),或者把系統(tǒng)變量autocommit的值設(shè)置為OFF時,事務(wù)就不會進行自動提交,但是如果我們輸入了某些語句之后就會悄悄的提交掉,就像我們輸入了COMMIT語句了一樣,這種因為某些特殊的語句而導(dǎo)致事務(wù)提交的情況稱為隱式提交,這些會導(dǎo)致事務(wù)隱式提交的語句包括:
1.4.6.3.1.執(zhí)行DDL
定義或修改數(shù)據(jù)庫對象的數(shù)據(jù)定義語言(Datadefinition language,縮寫為:DDL)。
所謂的數(shù)據(jù)庫對象,指的就是數(shù)據(jù)庫、表、視圖、存儲過程等等這些東西。當(dāng)我們使用CREATE、ALTER、DROP等語句去修改這些所謂的數(shù)據(jù)庫對象時,就會隱式的提交前邊語句所屬于的事務(wù),就像這樣:
BEGIN;
SELECT ... # 事務(wù)中的一條語句
UPDATE ... # 事務(wù)中的一條語句
... # 事務(wù)中的其它語句
CREATE TABLE ...
此語句會隱式的提交前邊語句所屬于的事務(wù)
1.4.6.3.2.隱式使用或修改mysql數(shù)據(jù)庫中的表
當(dāng)我們使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等語句時也會隱式的提交前邊語句所屬于的事務(wù)。
1.4.6.3.3.事務(wù)控制或關(guān)于鎖定的語句
當(dāng)我們在一個會話里,一個事務(wù)還沒提交或者回滾時就又使用START TRANSACTION或者BEGIN語句開啟了另一個事務(wù)時,會隱式的提交上一個事務(wù),比如這樣:
BEGIN;
SELECT ... # 事務(wù)中的一條語句
UPDATE ... # 事務(wù)中的一條語句
... # 事務(wù)中的其它語句
BEGIN; # 此語句會隱式的提交前邊語句所屬于的事務(wù)
或者當(dāng)前的autocommit系統(tǒng)變量的值為OFF,我們手動把它調(diào)為ON時,也會隱式的提交前邊語句所屬的事務(wù)。
或者使用LOCK TABLES、UNLOCK TABLES等關(guān)于鎖定的語句也會隱式的提交前邊語句所屬的事務(wù)。
1.4.6.3.4.加載數(shù)據(jù)的語句
比如我們使用LOAD DATA語句來批量往數(shù)據(jù)庫中導(dǎo)入數(shù)據(jù)時,也會隱式的提交前邊語句所屬的事務(wù)。
1.4.6.3.5.關(guān)于MySQL復(fù)制的一些語句
使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等語句時也會隱式的提交前邊語句所屬的事務(wù)。
1.4.6.3.6.其它的一些語句
使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等語句也會隱式的提交前邊語句所屬的事務(wù)。
1.5. MVCC
全稱Multi-Version Concurrency Control,即多版本并發(fā)控制,主要是為了提高數(shù)據(jù)庫的并發(fā)性能。
同一行數(shù)據(jù)平時發(fā)生讀寫請求時,會上鎖阻塞住。但MVCC用更好的方式去處理讀—寫請求,做到在發(fā)生讀—寫請求沖突時不用加鎖。
這個讀是指的快照讀,而不是當(dāng)前讀,當(dāng)前讀是一種加鎖操作,是悲觀鎖。
那它到底是怎么做到讀—寫不用加鎖的,快照讀和當(dāng)前讀是指什么?我們后面都會學(xué)到。
1.5.1.MVCC原理
1.5.1.1.復(fù)習(xí)事務(wù)隔離級別
MySQL在REPEATABLE READ隔離級別下,是可以很大程度避免幻讀問題的發(fā)生的(好像解決了,但是又沒完全解決),MySQL是怎么做到的?
1.5.1.2.版本鏈
必須要知道的概念(每個版本鏈針對的一條數(shù)據(jù)):
我們知道,對于使用InnoDB存儲引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列(row_id并不是必要的,我們創(chuàng)建的表中有主鍵或者非NULL的UNIQUE鍵時都不會包含row_id列):
trx_id:每次一個事務(wù)對某條聚簇索引記錄進行改動時,都會把該事務(wù)的事務(wù)id賦值給trx_id隱藏列。
roll_pointer:每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo日志中,然后這個隱藏列就相當(dāng)于一個指針,可以通過它來找到該記錄修改前的信息。
(補充點:undo日志:為了實現(xiàn)事務(wù)的原子性,InnoDB存儲引擎在實際進行增、刪、改一條記錄時,都需要先把對應(yīng)的undo日志記下來。一般每對一條記錄做一次改動,就對應(yīng)著一條undo日志,但在某些更新記錄的操作中,也可能會對應(yīng)著2條undo日志。一個事務(wù)在執(zhí)行過程中可能新增、刪除、更新若干條記錄,也就是說需要記錄很多條對應(yīng)的undo日志,這些undo日志會被從0開始編號,也就是說根據(jù)生成的順序分別被稱為第0號undo日志、第1號undo日志、…、第n號undo日志等,這個編號也被稱之為undo no。)
為了說明這個問題,我們創(chuàng)建一個演示表
CREATE TABLE teacher (
number INT,
name VARCHAR(100),
domain varchar(100),
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;
然后向這個表里插入一條數(shù)據(jù):
INSERT INTO teacher VALUES(1, '李瑾', 'JVM系列');
現(xiàn)在表里的數(shù)據(jù)就是這樣的:
假設(shè)插入該記錄的事務(wù)id為60,那么此刻該條記錄的示意圖如下所示:
假設(shè)之后兩個事務(wù)id分別為80、120的事務(wù)對這條記錄進行UPDATE操作,操作流程如下:
每次對記錄進行改動,都會記錄一條undo日志,每條undo日志也都有一個roll_pointer屬性(INSERT操作對應(yīng)的undo日志沒有該屬性,因為該記錄并沒有更早的版本),可以將這些undo日志都連起來,串成一個鏈表,所以現(xiàn)在的情況就像下圖一樣:
對該記錄每次更新后,都會將舊值放到一條undo日志中,就算是該記錄的一個舊版本,隨著更新次數(shù)的增多,所有的版本都會被roll_pointer屬性連接成一個鏈表,我們把這個鏈表稱之為版本鏈,版本鏈的頭節(jié)點就是當(dāng)前記錄最新的值。另外,每個版本中還包含生成該版本時對應(yīng)的事務(wù)id。于是可以利用這個記錄的版本鏈來控制并發(fā)事務(wù)訪問相同記錄的行為,那么這種機制就被稱之為多版本并發(fā)控制(Mulit-Version Concurrency Control MVCC)。
1.5.1.3.ReadView
必須要知道的概念(作用于SQL查詢語句)
對于使用READ UNCOMMITTED隔離級別的事務(wù)來說,由于可以讀到未提交事務(wù)修改過的記錄,所以直接讀取記錄的最新版本就好了(所以就會出現(xiàn)臟讀、不可重復(fù)讀、幻讀)。
對于使用SERIALIZABLE隔離級別的事務(wù)來說,InnoDB使用加鎖的方式來訪問記錄(也就是所有的事務(wù)都是串行的,當(dāng)然不會出現(xiàn)臟讀、不可重復(fù)讀、幻讀)。
對于使用READ COMMITTED和REPEATABLE READ隔離級別的事務(wù)來說,都必須保證讀到已經(jīng)提交了的事務(wù)修改過的記錄,也就是說假如另一個事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:READ COMMITTED和REPEATABLE READ隔離級別在不可重復(fù)讀和幻讀上的區(qū)別是從哪里來的,其實結(jié)合前面的知識,這兩種隔離級別關(guān)鍵是需要判斷一下版本鏈中的哪個版本是當(dāng)前事務(wù)可見的。
為此,InnoDB提出了一個ReadView的概念(作用于SQL查詢語句),
這個ReadView中主要包含4個比較重要的內(nèi)容:
**m_ids:**表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表。
**min_trx_id:**表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,也就是m_ids中的最小值。
**max_trx_id:**表示生成ReadView時系統(tǒng)中應(yīng)該分配給下一個事務(wù)的id值。注意max_trx_id并不是m_ids中的最大值,事務(wù)id是遞增分配的。比方說現(xiàn)在有id為1,2,3這三個事務(wù),之后id為3的事務(wù)提交了。那么一個新的讀事務(wù)在生成ReadView時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
**creator_trx_id:**表示生成該ReadView的事務(wù)的事務(wù)id。
1.5.1.4.READ COMMITTED
臟讀問題的解決
READ COMMITTED隔離級別的事務(wù)在每次查詢開始時都會生成一個獨立的ReadView。
在MySQL中,READ COMMITTED和REPEATABLE READ隔離級別的的一個非常大的區(qū)別就是它們生成ReadView的時機不同。
我們還是以表teacher 為例,假設(shè)現(xiàn)在表teacher 中只有一條由事務(wù)id為60的事務(wù)插入的一條記錄,接下來看一下READ COMMITTED和REPEATABLE READ所謂的生成ReadView的時機不同到底不同在哪里。
READ COMMITTED —— 每次讀取數(shù)據(jù)前都生成一個ReadView
比方說現(xiàn)在系統(tǒng)里有兩個事務(wù)id分別為80、120的事務(wù)在執(zhí)行:Transaction 80
UPDATE teacher SET name = '馬' WHERE number = 1;
UPDATE teacher SET name = '連' WHERE number = 1;
...
此刻,表teacher 中number為1的記錄得到的版本鏈表如下所示:
假設(shè)現(xiàn)在有一個使用READ COMMITTED隔離級別的事務(wù)開始執(zhí)行:
使用READ COMMITTED隔離級別的事務(wù)
BEGIN;
SELECE1:Transaction 80、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'李瑾'
第1次select的時間點 如下圖:
這個SELECE1的執(zhí)行過程如下:
在執(zhí)行SELECT語句時會先生成一個ReadView:
ReadView的m_ids列表的內(nèi)容就是[80, 120],min_trx_id為80,max_trx_id為121,creator_trx_id為0。
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是’連’,該版本的trx_id值為80,在m_ids列表內(nèi),所以不符合可見性要求(trx_id屬性值在ReadView的min_trx_id和max_trx_id之間說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問),根據(jù)roll_pointer跳到下一個版本。
下一個版本的列name的內(nèi)容是’馬’,該版本的trx_id值也為80,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。
下一個版本的列name的內(nèi)容是’李瑾’,該版本的trx_id值為60,小于ReadView中的min_trx_id值,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name為’李瑾’的記錄。
所以有了這種機制,就不會發(fā)生臟讀問題!因為會去判斷活躍版本,必須是不在活躍版本的才能用,不可能讀到?jīng)]有 commit的記錄。
不可重復(fù)讀問題
然后,我們把事務(wù)id為80的事務(wù)提交一下,然后再到事務(wù)id為120的事務(wù)中更新一下表teacher 中number為1的記錄:
Transaction120
BEGIN;
更新了一些別的表的記錄
UPDATE teacher SET name = '嚴(yán)' WHERE number = 1;
UPDATE teacher SET name = '晁' WHERE number = 1;
此刻,表teacher 中number為1的記錄的版本鏈就長這樣:
然后再到剛才使用READ COMMITTED隔離級別的事務(wù)中繼續(xù)查找這個number為1的記錄,如下:
使用READ COMMITTED隔離級別的事務(wù)
BEGIN;
SELECE1:Transaction 80、120均未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'李瑾'
SELECE2:Transaction 80提交,Transaction 120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'連'
第2次select的時間點 如下圖:
這個SELECE2的執(zhí)行過程如下:
SELECT * FROM teacher WHERE number = 1;
在執(zhí)行SELECT語句時會又會單獨生成一個ReadView,該ReadView信息如下:
m_ids列表的內(nèi)容就是[120](事務(wù)id為80的那個事務(wù)已經(jīng)提交了,所以再次生成快照時就沒有它了),min_trx_id為120,max_trx_id為121,creator_trx_id為0。
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是’晁’,該版本的trx_id值為120,在m_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個版本。
下一個版本的列name的內(nèi)容是’嚴(yán)’,該版本的trx_id值為120,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。
下一個版本的列name的內(nèi)容是’連’,該版本的trx_id值為80,小于ReadView中的min_trx_id值120,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name為’連’的記錄。
以此類推,如果之后事務(wù)id為120的記錄也提交了,再次在使用READ COMMITTED隔離級別的事務(wù)中查詢表teacher 中number值為1的記錄時,得到的結(jié)果就是’晁’了,具體流程我們就不分析了。
但會出現(xiàn)不可重復(fù)讀問題。
明顯上面一個事務(wù)中兩次
1.5.1.5.REPEATABLE READ
REPEATABLE READ解決不可重復(fù)讀問題
REPEATABLE READ —— 在第一次讀取數(shù)據(jù)時生成一個ReadView
對于使用REPEATABLE READ隔離級別的事務(wù)來說,只會在第一次執(zhí)行查詢語句時生成一個ReadView,之后的查詢就不會重復(fù)生成了。我們還是用例子看一下是什么效果。
比方說現(xiàn)在系統(tǒng)里有兩個事務(wù)id分別為80、120的事務(wù)在執(zhí)行:Transaction 80
UPDATE teacher SET name = '馬' WHERE number = 1;
UPDATE teacher SET name = '連' WHERE number = 1;
...
此刻,表teacher 中number為1的記錄得到的版本鏈表如下所示:
假設(shè)現(xiàn)在有一個使用REPEATABLE READ隔離級別的事務(wù)開始執(zhí)行:
使用READ COMMITTED隔離級別的事務(wù)
BEGIN;
SELECE1:Transaction 80、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'李瑾'
這個SELECE1的執(zhí)行過程如下:
在執(zhí)行SELECT語句時會先生成一個ReadView:
ReadView的m_ids列表的內(nèi)容就是[80, 120],min_trx_id為80,max_trx_id為121,creator_trx_id為0。
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是’連’,該版本的trx_id值為80,在m_ids列表內(nèi),所以不符合可見性要求(trx_id屬性值在ReadView的min_trx_id和max_trx_id之間說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問),根據(jù)roll_pointer跳到下一個版本。
下一個版本的列name的內(nèi)容是’馬’,該版本的trx_id值也為80,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。
下一個版本的列name的內(nèi)容是’李瑾’,該版本的trx_id值為60,小于ReadView中的min_trx_id值,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name為’李瑾’的記錄。
之后,我們把事務(wù)id為80的事務(wù)提交一下,然后再到事務(wù)id為120的事務(wù)中更新一下表teacher 中number為1的記錄:
Transaction120
BEGIN;
更新了一些別的表的記錄
UPDATE teacher SET name = '嚴(yán)' WHERE number = 1;
UPDATE teacher SET name = '晁' WHERE number = 1;
此刻,表teacher 中number為1的記錄的版本鏈就長這樣:
然后再到剛才使用REPEATABLE READ隔離級別的事務(wù)中繼續(xù)查找這個number為1的記錄,如下:
使用READ COMMITTED隔離級別的事務(wù)
BEGIN;
SELECE1:Transaction 80、120均未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'李瑾'
SELECE2:Transaction 80提交,Transaction 120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'李瑾'
這個SELECE2的執(zhí)行過程如下:
因為當(dāng)前事務(wù)的隔離級別為REPEATABLE READ,而之前在執(zhí)行SELECE1時已經(jīng)生成過ReadView了,所以此時直接復(fù)用之前的ReadView,之前的ReadView的m_ids列表的內(nèi)容就是[80, 120],min_trx_id為80,max_trx_id為121,creator_trx_id為0。
根據(jù)前面的分析,返回的值還是’李瑾’。
也就是說兩次SELECT查詢得到的結(jié)果是重復(fù)的,記錄的列name值都是’李瑾’,這就是可重復(fù)讀的含義。
總結(jié)一下就是:
ReadView中的比較規(guī)則(前兩條)
1、如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問。
2、如果被訪問版本的trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。
1.5.1.6.MVCC下的幻讀解決和幻讀現(xiàn)象
前面我們已經(jīng)知道了,REPEATABLE READ隔離級別下MVCC可以解決不可重復(fù)讀問題,那么幻讀呢?MVCC是怎么解決的?幻讀是一個事務(wù)按照某個相同條件多次讀取記錄時,后讀取時讀到了之前沒有讀到的記錄,而這個記錄來自另一個事務(wù)添加的新記錄。
我們可以想想,在REPEATABLE READ隔離級別下的事務(wù)T1先根據(jù)某個搜索條件讀取到多條記錄,然后事務(wù)T2插入一條符合相應(yīng)搜索條件的記錄并提交,然后事務(wù)T1再根據(jù)相同搜索條件執(zhí)行查詢。結(jié)果會是什么?按照ReadView中的比較規(guī)則(后兩條):
3、如果被訪問版本的trx_id屬性值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
4、如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間(min_trx_id < trx_id < max_trx_id),那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。
不管事務(wù)T2比事務(wù)T1是否先開啟,事務(wù)T1都是看不到T2的提交的。請自行按照上面介紹的版本鏈、ReadView以及判斷可見性的規(guī)則來分析一下。
但是,在REPEATABLE READ隔離級別下InnoDB中的MVCC 可以很大程度地避免幻讀現(xiàn)象,而不是完全禁止幻讀。怎么回事呢?我們來看下面的情況:
我們首先在事務(wù)T1中:
select * from teacher where number = 30;
很明顯,這個時候是找不到number = 30的記錄的。
我們在事務(wù)T2中,執(zhí)行:
insert into teacher values(30,'豹','數(shù)據(jù)湖');
通過執(zhí)行insert into teacher values(30,‘豹’,‘?dāng)?shù)據(jù)湖’);,我們往表中插入了一條number = 30的記錄。
此時回到事務(wù)T1,執(zhí)行:
update teacher set domain='RocketMQ' where number=30;
select * from teacher where number = 30;
嗯,怎么回事?事務(wù)T1很明顯出現(xiàn)了幻讀現(xiàn)象。
在REPEATABLE READ隔離級別下,T1第一次執(zhí)行普通的SELECT 語句時生成了一個ReadView(但是版本鏈沒有),之后T2向teacher 表中新插入一條記錄并提交,然后T1也進行了一個update語句。
ReadView并不能阻止T1執(zhí)行UPDATE 或者DELETE 語句來改動這個新插入的記錄,但是這樣一來,這條新記錄的trx_id隱藏列的值就變成了T1的事務(wù)id。
之后T1再使用普通的SELECT 語句去查詢這條記錄時就可以看到這條記錄了,也就可以把這條記錄返回給客戶端。因為這個特殊現(xiàn)象的存在,我們也可以認(rèn)為MVCC 并不能完全禁止幻讀(就是第一次讀如果是空的情況,且在自己事務(wù)中進行了該條數(shù)據(jù)的修改)。
1.5.1.7.MVCC小結(jié)
從上邊的描述中我們可以看出來,所謂的MVCC(Multi-Version Concurrency Control ,多版本并發(fā)控制)指的就是在使用READ COMMITTD、REPEATABLE READ這兩種隔離級別的事務(wù)在執(zhí)行普通的SELECT操作時訪問記錄的版本鏈的過程,這樣子可以使不同事務(wù)的讀-寫、寫-讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。
READ COMMITTD、REPEATABLE READ這兩個隔離級別的一個很大不同就是:生成ReadView的時機不同,READ COMMITTD在每一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView,之后的查詢操作都重復(fù)使用這個ReadView就好了,從而基本上可以避免幻讀現(xiàn)象(就是第一次讀如果ReadView是空的情況中的某些情況則避免不了)。文章來源:http://www.zghlxwxcb.cn/news/detail-640913.html
另外,所謂的MVCC只是在我們進行普通的SEELCT查詢時才生效,截止到目前我們所見的所有SELECT語句都算是普通的查詢,至于什么是個不普通的查詢,后面馬上就會講到(鎖定讀)。文章來源地址http://www.zghlxwxcb.cn/news/detail-640913.html
到了這里,關(guān)于事務(wù)和事務(wù)的隔離級別的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!