經過上一篇基本鎖的介紹,基本上Mysql的基礎加鎖步奏,有了一個大概的瞭解。接下來咱們進入最後一個鎖的議題:間隙鎖。間隙鎖,與行鎖、表鎖這些要複雜的多,是用來解決幻讀的一大手段。經過這種手段,咱們不必將事務隔離級別調整到序列化這個最嚴格的級別,而致使併發量大大降低。讀取這篇文章以前,我想,咱們要首先仍是讀一下我先前的兩篇文章,不然一些理念還真的透徹不了:java
爲了進行整個間隙鎖的深刻,咱們要構建一些基礎的數據,本章咱們都會用構建的基礎數據來進行,下面是數據表與索引的創建: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
好,這個問題,就很關鍵了!咱們來細說,幻讀的具體出現的經典場景。其實很簡單,先看下面的具體的復現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
大概上,有兩個影響,以下。高併發
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,又會有不一致的狀況出現了!
針對幻讀問題,咱們平常理論中常常"背誦"的,是:第三事務隔離級別會出現幻讀狀況,只有經過提升隔離級別,到最高級別的串行化,能解決幻讀這樣的問題。可是這樣,每個時刻只能有一個線程操做同一個表,併發性大大的下降,根本沒法知足,高併發的需求,要知道,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回滾,報錯!
有關於間隙鎖,是最後一層級的細節所在,因此在判斷是否加、怎麼加、是否會阻塞方面,有很是多的考量。接下來咱們來分別來講一下4個細節,分別對應4個例子,來說講,首先咱們列出五條規則:
加鎖的基本單位是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的這兩行的時候,加鎖步驟以下:
索引上進行等值查詢時,給惟一索引加鎖的時候,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;(成功) |
咱們來分析:
因此根據這個規則,(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);(阻塞) |
分析:
這麼一看,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);(阻塞) |
鎖,在個人能力範圍能,能說的就這麼多,具體仍是要用於實踐。接下來,打算寫很重要的兩個日誌文件的介紹:binglog和redolog