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