MySQL InnoDB存儲引擎中事務的隔離級別有哪些?對應隔離級別的實現機制是什么?
本文就將對上面這兩個問題進行解答,分析事務的隔離級別以及相關鎖機制。
隔離性簡介
隔離性主要是指數據庫系統提供一定的隔離機制,保證事務在不受外部並發操作影響的"獨立"環境執行,意思就是多個事務並發執行時,一個事務的執行不應影響其它事務的執行。
4種隔離級別介紹
在SQL標准中定義了4種隔離級別,分別是:
- Read uncommitted: 未提交讀,事務中的修改,即使沒有提交,對其他事務也是可見的。存在臟讀
- Read committed: 提交讀,大多數數據庫系統的默認隔離級別(MySQL不是), 一個事務從開始到提交之前,所做的修改對其他事務不可見。解決臟讀,存在幻讀和不可重復讀
- repeatable read: 可重復讀,該級別保證在同一事務中多次讀取同樣記錄的結果是一致的。解決臟讀和不可重復讀,理論上存在幻讀,但是在InnoDB引擎中解決了幻讀
- Serializable:可串行化,強制事務串行執行。
上面4種隔離級別是SQL標准定義的,但是在不同的存儲引擎中,實現的隔離級別不盡相同。本文主要介紹MySQL InnoDB 存儲引擎中的隔離級別,在InnoDB存儲引擎中,Repeatable Read 是默認的事務隔離級別,同時該引擎的實現基於多版本的並發控制協議——MVCC (Multi-Version Concurrency Control),解決了幻讀問題,當然 臟讀和不可重復讀也是不存在的。MVCC最大的好處就在於讀不加鎖,讀寫不沖突,這樣極大的增加了系統的並發性能
Read uncommitted
未提交讀,這種情況下,一個事務A可以看到另一個事務B未提交的數據,如果此時事務B發生回滾,那么事務A拿到的就是臟數據,這也就是臟讀的含義。此隔離級別在MySQL InnoDB一般不會使用,不做過多說明。
Read Committed
提交讀,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。解決了臟讀問題,但是存在幻讀現象。
所謂幻讀,指的是在同一事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能會返回之前不存在的行,也就是"幻行"。
比如下面這個例子:
- 1. 首先創建一張表,
CREATE TABLE `t` ( `a` int(11) NOT NULL, PRIMARY KEY (`a`) ) ENGINE=InnoDB insert into t(a) values(1); insert into t(a) values(2); insert into t(a) values(4); 復制代碼
- 1. 分別執行事務1和事務2:

可以從上圖看出,Read Committed這種隔離級別存在幻讀現象。實際上,Read Committed還可能存在不可重復讀的問題,不可重復讀,指的是一個事務內根據同一條件對行記錄進行多次查詢,但是查詢出的數據結果不一致,原因就是查詢區間數據被其他事務修改了。
不可重復讀感覺和幻讀有點像,實際上,前者強調是同一行記錄數據結果不一樣,后者強調的時多次查詢返回的結果集不一樣,增加了或減少了。
Repeatable Read
可重復讀,該級別保證在同一事務中多次讀取同樣記錄的結果是一致的,在InnoDB存儲引擎中同時解決了幻讀和不可重復讀問題。至於InnoDB通過什么方式解決幻讀和不可重復讀問題,后續內容揭曉。
Serializable (可串行化)
Serializable 是最高的隔離級別,它通過 強制事務串行執行,避免了幻讀的問題,但是 Serializable 會在讀取的每一行數據上都加鎖,所以可能導致大量的超時和鎖爭用的問題,因此並發度急劇下降,在MySQL InnoDB不被建議使用
Read Committed隔離級別下的加鎖分析
隔離級別的實現與鎖機制密不可分,所以需要引入鎖的概念,首先我們看下InnoDB存儲引擎提供的兩種標准的行級鎖:
- 共享鎖(S Lock):又稱為讀鎖,可以允許多個事務並發的讀取同一資源,互不干擾。即如果一個事務T對數據A加上共享鎖后,其他事務只能對A再加共享鎖,不能再加排他鎖,只能讀數據,不能修改數據
- 排他鎖(X Lock): 又稱為寫鎖,如果事務T對數據A加上排他鎖后,其他事務不能再對A加上任何類型的鎖,獲取排他鎖的事務既能讀數據,也能修改數據。
注意: 共享鎖和排他鎖是不相容的。
MySQL InnoDB存儲引擎是使用多版本並發控制的,讀不加鎖,讀寫不沖突,除非特定場景下的顯示加讀鎖(這里不去探究)。本小節主要分析Read Committed隔離級別下的加鎖情況,在MVCC的作用下,一般也就是寫操作加X鎖了。
加鎖操作是和索引緊密相關的,對一個SQL語句進行加鎖分析時,也要仔細考究其屬性列上的索引類型。假設有數據表t1,有兩個列,name列和id列,插入了幾條數據,沒有明確索引情況:
insert into t1(name,id) values("a",10); insert into t1(name,id) values("b",11); insert into t1(name,id) values("c",13); insert into t1(name,id) values("d",20); 復制代碼
下面執行 delete from t1 where id = 10 這條SQL語句,這里的隔離級別設置為Read Committed,從這條SQL語句不能得知id列的索引情況,所以需要分情況討論:
- id列是主鍵
- id列是二級唯一索引
- id列是二級非唯一索引
- id列上沒有索引
id列是主鍵
id是主鍵時,上述SQL只需要在id=10這條記錄上加X鎖即可
id列是二級唯一索引
若id列是唯一索引,而主鍵是name列,那么SQL需要加上兩個X鎖,一個對應於id索引上的id=10的記錄,另一把鎖對應於主鍵索引上的[name="a",id=10]的記錄
id列是二級非唯一索引
若id列上有非唯一索引,那么對應的所有滿足SQL查詢條件的記錄,都會被加鎖,同時,這些記錄在主鍵索引上的記錄也會被加鎖。
id列上沒有索引
若id列上沒有索引,SQL會走聚簇索引的全掃描進行過濾,由於過濾是由MySQL Sever層面進行的,因此每條記錄,無論是否滿足條件,都會被加上X鎖。
Repeatable Read隔離級別下的加鎖分析
前面說過,在Repeatable Read隔離級別下,InnoDB存儲引擎解決了幻讀和不可重復讀問題,具體的原理是怎么樣的呢?
之前簡短的介紹了InnoDB中行鎖的知識,下面來看下行鎖的三種算法:
- Record Lock: 單個索引記錄上的鎖,即加X鎖
- Gap Lock: 間隙鎖,鎖定一個范圍,但不包含記錄自身
- Next-Key Lock: Gap Lock + Record Lock,鎖定一個范圍,並且鎖定本身。
Record Lock總是會去鎖住索引記錄,如果InnoDB存儲引擎在建表的時候沒有設置任何一個索引,那么這時InnoDB會使用隱式的主鍵來進行鎖定。(表沒有定義主鍵的情況,InnoDB會默認添加一個隱式的主鍵索引)
Next-Key Lock是結合了Gap Lock和Record Lock的一種鎖定算法,比如一個索引列有10,11,13和20這4個值,那么該索引可能被Next-Key Locking的區間為:
- (
$-\infty$
,10) - (10,11]
- (11,13]
- (13,20]
- (20,
$+\infty$
)
需要注意一點的是,當查詢的索引含有唯一屬性時,即是主鍵索引或者唯一索引時,InnoDB存儲引擎會對Next-Key Lock進行優化,將其降級為Record Lock,即僅鎖住索引本身,一般加上X鎖。
Next-Key Lock機制設計的目的就是為了解決幻讀問題,主要針對查詢列索引為非唯一索引的時候。以下面這個例子進行說明:
- 1. 首先創建測試表t1,name是主鍵索引,id為非唯一索引,即輔助索引
CREATE TABLE `t1` ( `id` int(11) NOT NULL, `name` varchar(200) DEFAULT NULL, PRIMARY KEY (`name`), KEY `id_indx` (`id`) ) ENGINE=InnoDB insert into t1(name,id) values("a",10); insert into t1(name,id) values("b",11); insert into t1(name,id) values("c",13); insert into t1(name,id) values("d",20); 復制代碼
- 1. 執行 delete from t1 where id = 11,其加鎖情況如下圖所示

這條SQL通過索引列id進行刪除操作,該索引為非唯一索引,所以其使用傳統的Next-Key Locking 技術加鎖,並且由於有主鍵索引和輔助索引兩個,需要分別進行鎖定。對於主鍵索引(即聚集索引),其僅對列name = "b"的索引加上 Record Lock,實際上就是X鎖。
而對於非唯一索引,其加上的時Next-Key Lock,鎖定范圍是(10,11),對其加上Gap Lock(間隙鎖),GAP鎖實際上就是加在兩條邊界記錄之間的位置。還需要注意的是,InnoDB還會對輔助索引下一個鍵值加上gap lock,即看到在(11,13)之間加了一個GAP鎖。對於11值本身加上Record Lock,即X鎖。
若此時開啟另外一個事務執行下面的語句,就會阻塞:
1. select * from t1 where name = "b"; 2. insert into t1(name,id) values("c",12); 復制代碼
比如第一條語句不能執行,因為在開始的事務中已經對聚集索引中的列name="b"的值加上了X鎖。因此執行會被阻塞。而第二個SQL,同樣不能執行,插入的值12在鎖定范圍(11,13)中,需要阻塞等待。
所以,從上例就可以看出,GAP Lock的作用就是為了阻止多個事務將記錄插入到同一范圍內,這樣就有效的解決了幻讀問題。
隔離級別總結
下面總結下InnoDB存儲引擎下的各種隔離級別: