Mysql心路歷程:Mysql各類鎖機制(進階篇)

經過上一篇基本鎖的介紹,基本上Mysql的基礎加鎖步奏,有了一個大概的瞭解。接下來咱們進入最後一個鎖的議題:間隙鎖。間隙鎖,與行鎖、表鎖這些要複雜的多,是用來解決幻讀的一大手段。經過這種手段,咱們不必將事務隔離級別調整到序列化這個最嚴格的級別,而致使併發量大大降低。讀取這篇文章以前,我想,咱們要首先仍是讀一下我先前的兩篇文章,不然一些理念還真的透徹不了:java

1、基礎測試表與數據

爲了進行整個間隙鎖的深刻,咱們要構建一些基礎的數據,本章咱們都會用構建的基礎數據來進行,下面是數據表與索引的創建:sql

create table `t` (
  `id` int(11) not null,
  `c` int(11) DEFAULT null,
  `d` int(11) DEFAULT null,
  primary key (`id`),
  key `c` (`c`)
) ENGINE=InnoDB;

而後咱們插入一些基礎的數據:數據庫

insert into t values 
(0,0,0),
(5,5,5),
(10,10,10),
(15,15,15),
(20,20,20),
(25,25,25);

另外,咱們本次講解,都是使用默認的數據庫隔離級別:可重複讀session

2、什麼叫幻讀

好,這個問題,就很關鍵了!咱們來細說,幻讀的具體出現的經典場景。其實很簡單,先看下面的具體的復現sql語句:併發

sessionA sessionB sessionC
t1 begin;
select * from t where d = 5 for update;
t2 update t set d = 5 where id = 0;
t3 select * from t where d = 5 for update;
t4 insert into t values(1,1,5)
t5 select * from t where d = 5 for update;
t6 commit;

針對這一系列的操做,咱們來一個個分析:mvc

  • sessionA在t1時刻,可見讀的結果是:(5,5,5),d沒有索引,因此是全表掃描,對id爲5的那一行,加行鎖的寫鎖
  • 因爲sessionB再t2時刻,將id爲0的數據改了下,因此t3時刻,sessionA的可見讀的結果是:(0,0,5),(5,5,5)
  • 因爲sessionC再t4時刻,插入了條不存在的數據,因此t6時刻,sessionA的可見讀結果是:(0,0,0)(1,1,5)(5,5,5)
  • 若是,咱們不添加for update進行可見讀,普通的一致性讀的狀況下,因爲mvcc的建立快照機制的影響,sessionA一直都會只看到(5,5,5)這一條數據
  • update以後,可見讀查出來的多一條數據,並非幻讀,只有插入以後的可見讀,多讀出來的數據,才叫幻讀。就比如咱們原本有兩條原始數據,但是在事務的沒結束以前的先後去讀,分別讀出來2條和3條,多出一條,就好像我在以後讀出的3條數據,是幻影同樣,忽然出現了,因此叫幻讀。
  • 雖然咱們平時幾乎不會使用select for update進行查詢,可是,要記住,update語句以前就是要進行一次for update的select查詢的!

3、幻讀會有什麼影響

大概上,有兩個影響,以下。高併發

一、語義衝突

select * from t where d = 5 for update;

相似的,咱們這條語句,其實語義上面是想鎖住全部d等於5的行數據,讓其不能update和insert。然而,咱們接下來的sessionB和sessionC裏面,若是沒有相關的解決幻讀的機制存在,那麼都會有問題:測試

-- sessionB增長點操做
update t set d = 5 where id = 0;
update t set c = 5 where id = 0;

可見第二條sql已經操做了id等於0,d等於5這一行的數據,與以前的鎖全部等於5的行語義上面衝突。優化

二、數據一致性問題

這個很關鍵,涉及到binglog問題。下面是咱們具體操做的sql表格:.net

sessionA sessionB sessionC
t1 begin;
select * from t where d = 5 for update;
update t set d = 100 where d = 5;
t2 update t set d = 5 where id = 0;
update t set c = 5 where id = 0;
t3 select * from t where d = 5 for update;
t4 insert into t values(1,1,5);
update t set c = 5 where id = 1;
t5 select * from t where d = 5 for update;
t6 commit;

因爲,binglog是要等commit以後,纔會記錄的(後面文章會有細節的講解),因此,上面這一系列的sql操做,到了binglog裏面會變成下面的樣子:

update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/* 全部 d=5 的行,d 改爲 100*/

能夠看到,因爲咱們前面說,只對id等於5這一行,加了行鎖,因此sessionB的操做是能夠進行的,因此,最終會發現,咱們sessionA裏面的update操做,是最後執行的,若是拿着這個binglog同步從庫的話,必然會致使,(0,5,100)、(1,5,100) 和 (5,5,100)這種數據出現,和主庫徹底不一致!(主庫裏面,只有id爲5的數據,d才爲100)。

那麼咱們將全部掃秒到的數據行都加了鎖,會如何呢?那麼,sessionB裏面的第一條update語句將被阻塞,binglog裏面的數據以下:

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/* 全部 d=5 的行,d 改爲 100*/

update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/

這樣的結果,id爲0的這一行的數據,的確能保證數據的一直性,可是,會發現,剛剛插進去的id爲1的這同樣,在主庫裏面,d的值爲5,可是在從庫裏面執行了binglog以後,會變成100,又會有不一致的狀況出現了!

4、初入"間隙鎖"

針對幻讀問題,咱們平常理論中常常"背誦"的,是:第三事務隔離級別會出現幻讀狀況,只有經過提升隔離級別,到最高級別的串行化,能解決幻讀這樣的問題。可是這樣,每個時刻只能有一個線程操做同一個表,併發性大大的下降,根本沒法知足,高併發的需求,要知道,Mysql這東西,但是各大頂級互聯網公司趨之若鶩的基礎數據庫,怎麼能效率這麼差呢?在這裏,Mysql就引入了間隙鎖的概念。下面咱們來看看,間隙鎖如何加鎖。

一、間隙鎖的產生

首先,若是咱們使用下面語句進行查詢:

select * from t where d = 5 for update;

這樣,因爲d是沒有索引的,那麼會走全表查詢,默認走的是id的主鍵索引,按照id的主鍵值,會產生以下的區間:

二、如何加間隙鎖

例如上面的select語句中,d是沒有索引的,因此經過id索引進行全表掃面,又由於是for update,那麼,會將表中僅有的六條數據,都加上行鎖,而後,針對上面的六個區間,也會加上間隙鎖。行鎖+間隙鎖就是咱們喜聞樂見的:next-key lock了!因此,總體上看也就是7個next-key lock:

這個+∞是能夠進行配置的,給每一個索引分配一個不存在的值

三、間隙鎖的特性

前面的文章,咱們彷佛聊過行鎖之間的互斥形式:

讀鎖 寫鎖
讀鎖 兼容 衝突
寫鎖 衝突 衝突

可是間隙鎖不是。和間隙鎖衝突的,是往這個間隙裏面插入一條數據!這一點也是很好的保持併發性的一個挽回。下面看一個操做:

sessionA sessionB
begin;
select * from t where c = 7 lock in share model;
begin;
select * from t where c = 7 for update;

雖然,兩個事務,都是真對同一條數據,進行可見讀的查詢,可是並不會阻塞!由於c沒有7的這個值,那結果就是,只會在數據庫裏面加上了(5,10)這個間隙鎖,兩個可見讀並不會由於間隙鎖和互斥衝突!

若是這樣,加上間隙鎖的特性,和行鎖的特性,針對上面章節的sql操做:

sessionA sessionB sessionC
t1 begin;
select * from t where d = 5 for update;
update t set d = 100 where d = 5;
t2 update t set d = 5 where id = 0;(阻塞)
update t set c = 5 where id = 0;
t3 select * from t where d = 5 for update;
t4 insert into t values(1,1,5);(阻塞)
update t set c = 5 where id = 1;
t5 select * from t where d = 5 for update;
t6 commit;

最終生成的binglog就會是:

update t set d=100 where d=5;/* d 改爲 100*/

insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/



update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/

這樣,就解決了數據一致性的問題了,主從庫裏面都能保持一致。

四、間隙鎖帶來的問題

雖然,間隙鎖能比較好的解決上訴咱們探討的問題,可是同時也會帶來些麻煩,要咱們特別的注意。例以下面的操做,是一段業務上面的僞代碼:

tx.beginTransaction();
var t  = select * from t where id = 9 for update;
if(t){
  update t set d = 45 where id = 9;
}else{
  insert into t values(9,5,45);
}
tx.commit();

(假設id等於9這一行不存在)這段業務邏輯代碼,普通狀況下,我也常常看到,問題不太會出現,一旦併發量上去了,就會出問題,會形成死鎖,下面咱們看看形成死鎖的sql執行序列:

sessionA sessionB
t1 begin;
select * from t where id = 9 for update;
begin;
t2 select * from t where id = 9 for update;
insert into t values(9,5,45);(阻塞,等待sessionA的(5,10)的間隙鎖釋放)
t3 insert into t values(9,5,45); (阻塞,等待sessionB的(5,10)的間隙鎖釋放,死鎖!)

固然,InnoDB的自動死鎖檢測,會發現這一點,主動將sessionA回滾,報錯!

5、晉級"間隙鎖"

有關於間隙鎖,是最後一層級的細節所在,因此在判斷是否加、怎麼加、是否會阻塞方面,有很是多的考量。接下來咱們來分別來講一下4個細節,分別對應4個例子,來說講,首先咱們列出五條規則:

  • 加鎖的基本單位是next-key lock,就是針對掃描過的數據進行加間隙鎖
  • 索引上進行等值查詢時,給惟一索引加鎖的時候,next-key lock退化爲行鎖
  • 索引上進行等值查詢時,向右遍歷,最後一個數值不知足等值的條件的時候,next-key lock退化爲間隙鎖,就是先後都是開區間
  • 惟一索引的範圍查詢,會訪問到第一個不知足的條件爲止

一、第一條規則

加鎖的基本單位是next-key lock,就是針對掃描過的數據進行加間隙鎖

先來看看幾個sql語句:

select * from t where id = 5 for update;
select * from t where id = 10 lock in share model;

兩個分別對5和10這兩行加了寫鎖與讀鎖,可是最開始,再索引樹上面,首先加載id爲5和10的這兩行的時候,加鎖步驟以下:

  • 加(0,5)和(5,10)這兩個間隙鎖
  • 加5的這一行的行鎖(寫鎖),加10這一行的行鎖(讀鎖)
  • 因此目前爲止,基礎加鎖的單位爲next-key lock

二、第二條規則

索引上進行等值查詢時,給惟一索引加鎖的時候,next-key lock退化爲行鎖

仍是第一條規則的兩天語句,發現,id是主鍵索引(惟一索引),因此去掉了(0,5)(5,10)的這兩個間隙鎖,因此整個next-key lock變成了單純的行鎖

三、第三條規則

索引上進行等值查詢時,向右遍歷,最後一個數值不知足等值的條件的時候,next-key lock退化爲間隙鎖,就是先後都是開區間

先來看看下面的操做過程:

sessionA sessionB sessionC
begin;
update t set d = d+1 where id = 7;
insert into t values (8,8,8);(阻塞!)
update t set d = d+1 where id = 10;(成功)

咱們來分析:

  • update以前會進行select for update操做,因此就是對id爲7的這一行進行可見讀
  • 因爲7這行記錄不存在,可是7落在了(5,10)這個區間,而根據第一條原則,加鎖基本單位是next-key lock,因此加鎖會加上(5,10)的間隙鎖,和10這一行的行鎖(寫鎖),就是(5,10]
  • 因爲最後一條記錄10和等值查詢中的7並不相等,因此退化成了間隙鎖,10這個的行鎖解除。

因此根據這個規則,(5,10)這個區間是被鎖住額,因此insert會被阻塞,另外10這一行的行鎖解除,因此sessionC中的update會成功。

四、第四條規則

惟一索引的範圍查詢,會訪問到第一個不知足的條件爲止

看看下面的操做序列:

sessionA sessionB sessionC
begin;
select * from t where id > 10 and id <=15 for update;
update t set d = d+1 where id = 20;(阻塞)
insert into t values(16,16,16);(阻塞)

分析:

  • 因爲10不是等值,15是等值,因此10這一條不會加next-key lock,15會,因此首先加上了(10,15]
  • 雖然是惟一索引,可是是區間查詢,並不會中止加鎖的腳步,會繼續向右
  • 找到20這條記錄,加上了next-key lock的(15,20]
  • 因爲不是等值查詢,是範圍查詢,因此應用不了規則三,因此最終造成的鎖是:(10,15],(15,20]

這麼一看,20這一行被行鎖鎖住,並且15,20的區間還有間隙鎖,因此sessionB和sessionC的操做纔會阻塞。

五、其餘方面的細節

每次加鎖,其實都是鎖索引樹。衆所周知,InnoDB的非主鍵索引的最終葉子節點,都只存儲id主鍵值,而後還要遍歷id主鍵索引,才能搜索出整條的數據,咱們一般將這個過程稱之爲:回表。固然,若是select的是一個字段,這個字段恰好是id,那麼Mysql就不用進行回表查詢,由於直接在索引樹上就能讀取到值,MySQL會進行這種優化,一般咱們稱之爲:索引下推。根據這個特性,咱們來看看下面的操做序列:

sessionA sessionB sessionC
begin;
select id from t where c = 5 lock in share model;
update t set d = d+1 where id = 5;(成功)
insert into t values(3,3,3);(阻塞)
  • 只在c這個非惟一索引上,加了行讀鎖,基礎的加鎖單位是(0,5],因爲是非惟一索引的查詢,並不能退化爲行鎖
  • 因爲非惟一索引,要繼續往下,加上了(5,10]這一個的next-key lock,因爲最右邊的最後一個值,和等值查詢並不相等,因此退化成間隙鎖(5,10),因此sessionC會被阻塞
  • 因爲sessionA中的可見讀是讀鎖,而且只查詢id的值,因此啓動了索引下推優化,只會加c這個索引上面的行鎖。若是換成for update,那就會順便將主鍵索引上面也加上鎖。因此這裏要分清兩種行鎖的粒度。
  • 因此,最後,sessionB能成功的願意是:主鍵索引上並無加鎖

6、結束

鎖,在個人能力範圍能,能說的就這麼多,具體仍是要用於實踐。接下來,打算寫很重要的兩個日誌文件的介紹:binglog和redolog

相關文章
相關標籤/搜索