最近學習了mysql的各種鎖,有點暈,打算通過文章的方式捋一捋。
在學習了mvcc后,我就想,他已經很好的解決了並發讀寫了,但我也知道innodb提供了多種類型的鎖,所以很好奇這些鎖有什么用,為什么這些鎖的功能是mvcc做不到的?(本文討論的都是rr級別下的鎖)
我先創建一個表,並插入幾行數據,如下圖:
插入內容如下:
c字段加了普通索引,d字段無索引。
此時,開啟session a,開始一個事務,搜索d=5的記錄,同時開始session b執行更新操作,session c執行插入操作。
現象:session b 的update和session c的 insert都被阻塞了,注意,並沒有直接返回錯誤。
為什么會被阻塞呢?
因為session a用了select for update這個鎖,是排他鎖。
在研究這個排他鎖之前,我先從語義上來理解select * from t where d=5 for update的含義。 這句sql的含義是鎖住d=5的行嗎?也許是的。其實也是的。但是這里有個問題,如果不存在的行,怎么控制呢?session a一次事務中多次執行elect * from t where d=5 for update我們是不是希望得到同樣的記錄(除非本事務內自己修改)?如果只是鎖住了d=5這行,那么session B的update和session C的insert就會在session A的事務進行時插入、更新,這樣session a會后面select for update會發現多出來了d=5的行,和第一次不一樣,這就破壞了session A鎖的聲明。這就是幻讀。
什么是幻讀?
幻讀指的是一個事務在前后兩次查詢同一個范圍的時候,后一次查詢看到了前一次查詢沒有看到的行。
- 在可重復讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的數據的。因此,幻讀在“當前讀”下才會出現。
- 上面 session B 的修改結果,被 session A 之后的 select 語句用“當前讀”看到,不能稱為幻讀。幻讀僅專指“新插入的行”。
怎么解決呢?
通過上面的討論我們得知通過給d=5這行加鎖是不行的,那么如果我們給所有掃描的記錄都加鎖能解決這個問題嗎?如果這樣,update語句會被阻塞,但是insert語句依然可以執行成功,因為insert這行是新的行,我們並沒有辦法給他加鎖。也就是說,即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄
到底如何解決?
接下來,我們再看看 InnoDB 怎么解決幻讀的問題。
我們知道行鎖只能鎖住現有的行,阻止update,但是不能阻止新的行插入,所以innodb引入了間隙鎖gap lock,這樣在其他session 執行select * for update時,插入操作就會被block。
下圖就是六條記錄,並且形成了7個間隙,因此有7個間隙鎖。
由於d沒有索引,所以所有掃描到的行和間隙都會加上鎖,此時其他session就不能做任何插入和更新操作了。為了減小鎖的粒度,一般需要在索引上做select for update,比如select * from t where c=5 for update。
我們知道,行鎖分為讀和寫兩類鎖,那么同一個記錄寫行鎖和寫行鎖是沖突的,這符合行鎖的語義。間隙鎖呢?不同session的對同一個間隙加鎖,會沖突嗎?先來分析一下間隙鎖的語義,比如(5,10)這個間隙鎖,語義是阻止5-10內的插入。所以不同的session對同一個間隙加鎖是服務語義的,不沖突。當然也會導致死鎖。比如如下順序操作:
上圖的執行順序會造成死鎖:
1、session A先鎖住了(5,10),然后session B 也鎖住了(5,10)。
2、session B試圖插入(9, 9, 9),但是他和session A的間隙鎖沖突了,所以會被block
3、接下來session 試圖插入(9, 9, 9),和session B的間隙鎖沖突,也會被block,就形成了死鎖。
間隙鎖的引入解決了rr級別下的幻讀,但是也導致鎖的范圍變大了,不同時刻,同一個select 語句可能鎖住不同的間隙,的確影響了並發度,但這也是無奈之舉。行鎖加上間隙鎖就是next-key-lock,當select * from t where c=x for update,這個x在表中時就會加next-key-lock。
值得注意的是,只有rr才有間隙鎖,讀提交的情況下,是不會有間隙鎖的,因為提交后必須被讀到,隔離性沒那么好。