上篇文章總結了有關事務的原子性,持久性和一致性【mysql】Innodb存儲引擎是如何保證事務的ACID四個原則的,接下來是事務的隔離性。
事務隔離級別
- READ UNCOMMITTED : RU 稱爲瀏覽訪問,可以讀取事務未提交的數據。
- READ COMMITTED : RC 稱爲遊標穩定,只能讀取已經提交事務的數據。
- REPEATABLE READ: RR 可重複讀。
- SERIALIZABLE : SR 序列化。所有事務按照次序依次執行。當一個事務開始未提交時,其他事務均不能執行。
各個事務級別所造成的問題以及原因
-
RU:
問題:讀取髒數據,不可重複讀,幻像
原因:在RU隔離級別下,讀未提交的數據,所以,當事務還沒有提交時,數據並不是最終的數據,另一個事務就有可能讀取到髒數據。 -
RC:
問題:不可重複讀,幻讀
原因:
不可重複讀:在對數據進行修改時,會產生undo log,在undo log中,數據會有多個版本的快照數據這種技術成爲行多版本技術(請閱讀下面 undo log 存儲版本鏈)。在RC級別下,innodb存儲引擎每次讀取的歷史版本是最新的版本數據,因此,當有新的事務提交產生新的版本數據時,RC級別讀取到的數據會發生不一致這種現象稱爲不可重複讀(每次select會產生一個新的readView導致)。
幻讀:在事務執行select操作加鎖時,仍然可以進行insert操作對數據進行更改,再次執行select操作得到的結果集不一致的問題。這是因爲在RC級別下不支持間隙鎖,當select時加鎖只能對記錄本身的索引值加鎖,不能對範圍進行加鎖,以致於可以繼續插入範圍內的數據造成幻讀(下面解決方案中會舉例說明)。 -
RR
問題:在RR級別下,innodb存儲引擎解決了RC下的問題。可以達到隔離性的要求。
-
SR
此級別下由於是一個事務執行完成,另一個事務才能開始執行,事務級別最高,因此可以達到隔離性。
undo log 存儲的版本鏈
版本鏈類其實是採用了樂觀鎖的機制。
在InnoDB引擎表中,它的聚簇索引記錄中有兩個必要的隱藏列:
- trx_id
這個id用來存儲的每次對某條聚簇索引記錄進行修改的時候的事務id。 - roll_pointer
每次對哪條聚簇索引記錄有修改的時候,都會把老版本寫入undo日誌中。這個roll_pointer就是存了一個指針,它指向這條聚簇索引記錄的上一個版本的位置,通過它來獲得上一個版本的記錄信息。(注意插入操作的undo日誌沒有這個屬性,因爲它沒有老版本)
對於不同的操作,存儲的數據如下:
SELECT
innodb會根據以下兩個條件檢查每行記錄:
a.innodb只查找版本號早於當前事務版本的數據行,<=當前事務版本號,這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的
b.行的刪除版本要麼未定義,要麼大於當前的事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。
INSERT
INNODB爲新插入的每一行保存當前系統版本號作爲行版本號
DELETE
innodb爲刪除的每一行保存當前系統版本號作爲行刪除標識
UPDATE
innodb爲插入一行新紀錄,保存當前系統版本號爲行版本號,同時保存當前系統版本號到原來的行作爲行刪除標識
- RR :在innodb存儲引擎中,已基本解決以上的問題,可以達到和SR相同效果的事務隔離級別
- SR:有事務按照次序依次執行。當一個事務開始未提交時,其他事務均不能執行。不會出現以上問題。
問題描述
1、髒讀:讀到未提交事務數據時,稱爲髒讀。即讀到髒數據,並不是事務最終的數據。
2、不可重複讀:是幻像問題中的一種。在事務進行中,重複讀取統一條記錄,顯示的數據不同,稱爲不可重複讀。
3、幻讀:是幻像問題的一種。在事務進行中,重複讀取記錄,得到的記錄數與之前不一致。如:在同一個事務中第一次讀取有5條記錄,第二次讀取爲6條記錄,出現了幻讀現象。
不可重複讀和幻讀都是幻像問題的一種表現形式。
解決方案
1、髒讀:將數據庫隔離級別修改爲READ COMMITTED。只能讀取提交事務的數據即可。在mysql數據庫中讀取的是事務版本號比自己小的數據。
2、不可重複讀:在RC 級別由於上面所說的讀取的歷史數據版本總是爲最新的版本,這個跟ReadView相關,每次select會生成一個新的ReadView,所以造成多次查詢結果數據不一致。
解決方案:將隔離級別修改爲REPEATABLE READ,因爲在這個級別下,每次select時總是查詢事務開始時的數據版本,即每次事務開始時都會生成一份獨立的ReadView,直到事務結束都用這一份ReadView,也就讀取的都是事務開始時的數據。
3、幻讀
在innodb存儲引擎的RR級別,由於運用了間隙鎖,所以可以解決幻讀的問題。
間隙鎖鎖定的不是單個的值而是一個範圍。具體解釋看下面的例子。
舉例:
CREATE TABLE z( a INT, b INT ,PRIMARY KEY(a),KEY(b));
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;
===========語句1=================
SELECT * FROM z WHERE b=3 FOR UPDATE;
==========語句2==================
SELEFCT * FROM z WHERE a=5 LOCK IN SHARE MODE;
==========語句3==================
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,5;
==========語句4 ================
INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;
事實上在加鎖時,不僅會對主鍵唯一索引加鎖,同時也會對行記錄中的輔助索引加鎖。
當我們在執行語句1時,對輔助索引b=3這個值加X鎖,此時已經對a=5也加了X鎖,因此在此時不能執行語句2。
當此時數據庫隔離級別爲RR時,由於間隙鎖的原因,此時會對b值爲(1,3)和(3,6)這個範圍也加上鎖,因此在執行語句3時也會被阻塞。但是當執行語句4時不會被阻塞。因爲語句4中b值得範圍沒有被加間隙鎖。
那爲什麼主鍵索引沒有被間隙鎖限制呢?
這是因爲間隙鎖在查詢的列爲唯一索引時,會自動降級爲記錄鎖。提高數據庫的併發性。所以主鍵索引不會受到影響。你想想,當條件是唯一索引時,怎麼查數據量也不會再增多了吧,就那一條記錄。
下面來繼續介紹一下上面的知識中涉及到的一些基本的概念。ReadView和數據庫鎖。
ReadView
ReadView中主要就是有個列表來存儲我們系統中當前活躍着的讀寫事務,也就是begin了還未提交的事務。通過這個列表來判斷記錄的某個版本是否對當前事務可見。
假設當前列表裏的事務id爲[80,100]。
- 如果你要訪問的記錄版本的事務id爲50,比當前列表最小的id80小,那說明這個事務在之前就提交了,所以對當前活動的事務來說是可訪問的。
- 如果你要訪問的記錄版本的事務id爲90,發現此事務在列表id最大值和最小值之間,那就再判斷一下是否在列表內,如果在那就說明此事務還未提交,所以版本不能被訪問。如果不在那說明事務已經提交,所以版本可以被訪問。
- 如果你要訪問的記錄版本的事務id爲110,那比事務列表最大id100都大,那說明這個版本是在ReadView生成之後才發生的,所以不能被訪問。
那麼通過對ReadView的瞭解,我們就知道READ UNCOMITTED爲什麼能讀到髒數據了。而READ COMMITTED則可根據這個鏈表來讀取必須是已經提交了的數據。
舉個例子。此例子來源於其他博客:https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc 來幫助理解。
在RR級別下:
比如此時有一個事務id爲100的事務,修改了name,使得的name等於小明2,但是事務還沒提交。則此時的版本鏈是
那此時另一個事務發起了select 語句要查詢id爲1的記錄,那此時生成的ReadView 列表只有[100]。那就去版本鏈去找了,首先肯定找最近的一條,發現trx_id是100,也就是name爲小明2的那條記錄,發現在列表內,所以不能訪問。
這時候就通過指針繼續找下一條,name爲小明1的記錄,發現trx_id是60,小於列表中的最小id,所以可以訪問,直接訪問結果爲小明1。
那這時候我們把事務id爲100的事務提交了,並且新建了一個事務id爲110也修改id爲1的記錄,並且不提交事務。
這時候版本鏈就是
這時候之前那個select事務又執行了一次查詢,要查詢id爲1的記錄。
由於在RR級別下,此時讀取到的ReadView還是最開始生成的獨立的ReadView,因此讀取到的還是60版本的小明1.
但是如果此時在RC級別下,再次select會重新生成一個ReadView此時,鏈表中爲110,100已經不在鏈表中,說明該事務已經提交,所以讀取到的值爲事務id爲100的值小明2.
根據時間軸如下表更清晰:
數據庫中的鎖
鎖時數據庫區別於文件系統的一個重要特性。用於管理對共享資源的併發訪問。
鎖的類型:
- 共享鎖 S:允許事務讀一行數據。
- 排他鎖 X:允許事務刪除或更新一條數據。
兼容性:
一致性非鎖定讀:
指的是INNODB存儲引擎通過行多版本控制的方式來讀取當前執行的時間數據庫中行的數據。如果讀取的行正在執行DETELE或UPDATE操作,這是讀取操作不會因此等待行上的鎖的釋放。相反地,會去讀取行的一個快照數據。
正如上面小明的例子中,事務101第二次select時,其實事務110已經執行了update操作但是並沒有提交這時採用的就是一致性非鎖定讀。是爲了提高併發性。
一致性鎖定讀:
雖然數據庫爲了提高並發現支持一致性非鎖定度,但是某些情況下用戶需要顯式地對數據庫讀取操作進行加鎖以保證數據邏輯一致性。這時需要數據庫支持加鎖語句。即使是select語句。
- SELECT … FOR UPDATE -------X鎖
- SELECT … LOCK IN SHARE MODE; -------S鎖
鎖算法
- Record Lock: 單個行記錄上的鎖
- Gap Lock: 間隙鎖,鎖定一個範圍,但不包含記錄本身
- Next-Key Lock:Gap Lock+Record Lock,鎖定一個範圍,並且鎖定記錄本身。對應的還有previous-key locking
以上是關於mysql數據庫 innodb存儲引擎的事務隔離級別相關的知識。