前幾天有粉絲和我聊到他找工做面試大廠時被問的問題,由於如今疫情期間,找工做也特別難找。他說面試的題目也比較難,都偏向於一兩年的工做經驗的面試題。程序員
他說在一面的時候被問到Mysql的面試題,索引那塊本身都回答比較滿意,可是問到Mysql的鎖機制就比較懵了。web
由於平時沒有關注Mysql的鎖機制,當被問到高併發場景下鎖機制是怎麼保證數據的一致性的和事務隔離性的。面試
他把他面試的過程分享給了我,Mysql高併發鎖機制的問題,幾乎面大廠都有被問到,Mysql怎麼在高併發下控制併發訪問的?sql
我細想了一下,Mysql的鎖機制確實很是重要,因此在這裏作一個全面的總結整理,便於之後的查閱,也分享給各位讀者大大們。數據庫
Mysql的鎖機制仍是有點難理解的,因此這篇文章採用圖文結合的方式講解難點,幫助你們理解,講解的主要內容以下圖的腦圖所示,基本涵蓋了Mysql鎖機制的全部知識點。session
Mysql中鎖的分類按照不一樣類型的劃分能夠分紅不一樣的鎖,按照「鎖的粒度」劃分能夠分紅:「表鎖、頁鎖、行鎖」;按照「使用的方式」劃分能夠分爲:「共享鎖」和「排它鎖」;按照思想的劃分:「樂觀鎖」和「悲觀鎖」。併發
下面咱們對着這幾種劃分的鎖進行詳細的解說和介紹,在瞭解設計者設計鎖的概念的同時,也能深刻的理解設計者的設計思想。框架
「表鎖」是粒度最大的鎖,開銷小,加鎖快,不會出現死鎖,可是因爲粒度太大,所以形成鎖的衝突概率大,併發性能低。編輯器
Mysql中「MyISAM儲存引擎就支持表鎖」,MyISAM的表鎖模式有兩種:「表共享讀鎖」和「表獨佔寫鎖」。高併發
當一個線程獲取到MyISAM表的讀鎖的時候,會阻塞其餘用戶對該表的寫操做,可是不會阻塞其它用戶對該用戶的讀操做。
相反的,當一個線程獲取到MyISAM表的寫鎖的時候,就會阻塞其它用戶的讀寫操做對其它的線程具備排它性。
「頁鎖」的粒度是介於行鎖和表鎖之間的一種鎖,由於頁鎖是在BDB中支持的一種鎖機制,也不多沒人說起和使用,因此這裏製做概述,不作詳解。
「行鎖」是粒度最小的鎖機制,行鎖的加鎖開銷性能大,加鎖慢,而且會出現死鎖,可是行鎖的鎖衝突的概率低,併發性能高。
行鎖是InnoDB默認的支持的鎖機制,MyISAM不支持行鎖,這個也是InnoDB和MyISAM的區別之一。
行鎖在使用的方式上能夠劃分爲:「共享讀鎖(S鎖)「和」排它寫鎖(X鎖)」。
當一個事務對Mysql中的一條數據行加上了S鎖,當前事務不能修改該行數據只能執行度操做,其餘事務只能對該行數據加S鎖不能加X鎖。
如果一個事務對一行數據加了X鎖,該事物可以對該行數據執行讀和寫操做,其它事務不能對該行數據加任何的鎖,既不能讀也不能寫。
「悲觀鎖和樂觀鎖是在不少框架都存在的一種思想,不要狹義地認爲它們是某一種框架的鎖機制」。
數據庫管理系統中爲了控制併發,保證在多個事務執行時的數據一致性以及事務的隔離性,使用悲觀鎖和樂觀鎖來解決併發場景下的問題。
Mysql中「悲觀鎖的實現是基於Mysql自身的鎖機制實現,而樂觀鎖須要程序員本身去實現的鎖機制」,最多見的樂觀鎖實現就鎖機制是「使用版本號實現」。
樂觀鎖設計思想的在CAS
的運用也是比較經典,以前我寫過一篇關於CAS的文章,你們感興趣的能夠參考這一篇[]。
從上面的介紹中說了每一種鎖的概念,可是很難說哪種鎖就是最好的,鎖沒有最好的,只有哪一種業務場景最適合哪一種鎖,具體業務具體分析。
下面咱們就具體基於Mysql的存儲引擎詳細的分析每一種鎖在存儲引擎中的運用和實現。
MyISAM中默認支持的表級鎖有兩種:「共享讀鎖」和「獨佔寫鎖」。表級鎖在MyISAM和InnoDB的存儲引擎中都支持,可是InnoDB默認支持的是行鎖。
Mysql中平時讀寫操做都是隱式的進行加鎖和解鎖操做,Mysql已經自動幫咱們實現加鎖和解鎖操做了,如果想要測試鎖機制,咱們就要顯示的本身控制鎖機制。
Mysql中能夠經過如下sql來顯示的在事務中顯式的進行加鎖和解鎖操做:
// 顯式的添加表級讀鎖 LOCK TABLE 表名 READ // 顯示的添加表級寫鎖 LOCK TABLE 表名 WRITE // 顯式的解鎖(當一個事務commit的時候也會自動解鎖) unlock tables; 複製代碼
下面咱們就來測試一下MyISAM中的表級鎖機制,首先建立一個測試表employee
,這裏要指定存儲引擎爲MyISAM,並插入兩條測試數據:
CREATE TABLE IF NOT EXISTS employee (
id INT PRIMARY KEY auto_increment, name VARCHAR(40), money INT )ENGINE MyISAM INSERT INTO employee(name, money) VALUES('黎杜', 1000); INSERT INTO employee(name, money) VALUES('非科班的科班', 2000); 複製代碼
查看一下,表結果以下圖所示:
(1)與此同時再開啓一個session窗口,而後在第一個窗口執行下面的sql,在session1中給表添加寫鎖:
LOCK TABLE employee WRITE
複製代碼
(2)能夠在session2中進行查詢或者插入、更新該表數據,能夠發現都會處於等待狀態,也就是session1鎖住了整個表,致使session2只能等待:
(3)在session1中進行查詢、插入、更新數據,均可以執行成功:
「總結:」 從上面的測試結果顯示「當一個線程獲取到表級寫鎖後,只能由該線程對錶進行讀寫操做,別的線程必須等待該線程釋放鎖之後才能操做」。
(1)接下來測試一下表級共享讀鎖,一樣仍是利用上面的測試數據,第一步仍是在session1給表加讀鎖。
(2)而後在session1中嘗試進行插入、更新數據,發現都會報錯,只能查詢數據。
(3)最後在session2中嘗試進行插入、更新數據,程序都會進入等待狀態,只能查詢數據,直到session1解鎖表session2才能插入、更新數據。
「總結:」 從上面的測試結果顯示「當一個線程獲取到表級讀鎖後,該線程只能讀取數據不能修改數據,其它線程也只能加讀鎖,不能加寫鎖」。
MyISAM存儲引擎中,能夠經過查詢變量來查看併發場景鎖的爭奪狀況,具體執行下面的sql語句:
show status like 'table%';
複製代碼
主要是查看table_locks_waited
和table_locks_immediate
的值的大小分析鎖的競爭狀況。
Table_locks_immediate
:表示可以當即得到表級鎖的鎖請求次數;Table_locks_waited
表示不能當即獲取表級鎖而須要等待的鎖請求次數分析,「值越大競爭就越嚴重」。
經過上面的操做演示,詳細的說明了表級共享鎖和表級寫鎖的特色。可是在平時的執行sql的時候,這些「解鎖和釋放鎖都是Mysql底層隱式的執行的」。
上面的演示只是爲了證實顯式的執行事務的過程共享鎖和表級寫鎖的加鎖和解鎖的特色,實際並不會這麼作的。
在咱們平時執行select語句的時候就會隱式的加讀鎖,執行增、刪、改的操做時就會隱式的執行加寫鎖。
MyISAM存儲引擎中,雖然讀寫操做是串行化的,可是它也支持併發插入,這個須要設置內部變量concurrent_insert
的值。
它的值有三個值0、一、2
。能夠經過如下的sql查看concurrent_insert
的默認值爲「AUTO(或者1)」。
concurrent_insert的值爲NEVER (or 0)
表示不支持比並發插入;值爲AUTO(或者1)
表示在MyISAM表中沒有被刪除的行,運行另外一個線程從表尾插入數據;值爲ALWAYS (or 2)
表示無論是否有刪除的行,都容許在表尾插入數據。
MyISAM存儲引擎中,「假如同時一個讀請求,一個寫請求過來的話,它會優先處理寫請求」,由於MyISAM存儲引擎中認爲寫請求比都請求重要。
這樣就會致使,「假如大量的讀寫請求過來,就會致使讀請求長時間的等待,或者"線程餓死",所以MyISAM不適合運用於大量讀寫操做的場景」,這樣會致使長時間讀取不到用戶數據,用戶體驗感極差。
固然能夠經過設置low-priority-updates
參數,設置請求連接的優先級,使得Mysql優先處理讀請求。
InnoDB和MyISAM不一樣的是,InnoDB支持「行鎖」和「事務」,行級鎖的概念前面以及說了,這裏就再也不贅述,事務的四大特性的概述以及實現的原理能夠參考這一篇[]。
InnoDB中除了有「表鎖」和「行級鎖」的概念,還有Gap Lock(間隙鎖)、Next-key Lock鎖,「間隙鎖主要用於範圍查詢的時候,鎖住查詢的範圍,而且間隙鎖也是解決幻讀的方案」。
InnoDB中的行級鎖是「對索引加的鎖,在不經過索引查詢數據的時候,InnoDB就會使用表鎖」。
「可是經過索引查詢的時候是否使用索引,還要看Mysql的執行計劃」,Mysql的優化器會判斷是一條sql執行的最佳策略。
如果Mysql以爲執行索引查詢還不如全表掃描速度快,那麼Mysql就會使用全表掃描來查詢,這是即便sql語句中使用了索引,最後仍是執行爲全表掃描,加的是表鎖。
如果對於Mysql的sql執行原理不熟悉的能夠參考這一篇文章[]。最後是否執行了索引查詢能夠經過explain
來查看,我相信這個你們都是耳熟能詳的命令了。
InnoDB的行鎖也是分爲行級「共享讀鎖(S鎖)「和」排它寫鎖(X鎖)」,原理特色和MyISAM的表級鎖兩種模式是同樣的。
若想顯式的給表加行級讀鎖和寫鎖,能夠執行下面的sql語句:
// 給查詢sql顯示添加讀鎖
select ... lock in share mode; // 給查詢sql顯示添加寫鎖 select ... for update; 複製代碼
(1)下面咱們直接進入鎖機制的測試階段,仍是建立一個測試表,並插入兩條數據:
// 先把原來的MyISAM表給刪除了
DROP TABLE IF EXISTS employee; CREATE TABLE IF NOT EXISTS employee ( id INT PRIMARY KEY auto_increment, name VARCHAR(40), money INT )ENGINE INNODB; // 插入測試數據 INSERT INTO employee(name, money) VALUES('黎杜', 1000); INSERT INTO employee(name, money) VALUES('非科班的科班', 2000); 複製代碼
(2)建立的表中能夠看出對錶中的字段只有id添加了主鍵索引,接着就是在session1窗口執行begin
開啓事務,並執行下面的sql語句:
// 使用非索引字段查詢,並顯式的添加寫鎖
select * from employee where name='黎杜' for update; 複製代碼
(3)而後在session2中執行update語句,上面查詢的式id=1的數據行,下面update的是id=2的數據行,會發現程序也會進入等待狀態:
update employee set name='ldc' where id =2;
複製代碼
可見如果「使用非索引查詢,直接就是使用的表級鎖」,鎖住了整個表。
(4)如果session1使用的是id來查詢,以下圖所示:
(5)那麼session2是能夠成功update其它數據行的,可是這裏我建議使用數據量大的表進行測試,由於前面我說過了「是否執行索引還得看Mysql的執行計劃,對於一些小表的操做,可能就直接使用全表掃描」。
(6)還有一種狀況就是:假如咱們給name字段也加上了普通索引,那麼經過普通索引來查詢數據,而且查詢到多行數據,拿它是鎖這多行數據仍是鎖整個表呢?
下面咱們來測試一下,首先給「name字段添加普通索引」,以下圖所示:
(6)並插入一條新的數據name值與id=2的值相同,並顯式的加鎖,以下如果:
(7)當update其它數據行name值不是ldc的也會進入等待狀態,而且經過explain來查看是否name='ldc'有執行索引,能夠看到sql語句是有執行索引條件的。
結論:從上面的測試鎖機制的演示能夠得出如下幾個結論:
當咱們使用範圍條件查詢而不是等值條件查詢的時候,InnoDB就會給符合條件的範圍索引加鎖,在條件範圍內並不存的記錄就叫作"間隙(GAP)"
你們大概都知道在事務的四大隔離級別中,不可重複讀會產生幻讀的現象,只能經過提升隔離級別到串行化來解決幻讀現象。
可是Mysql中的不可重複是已經解決了幻讀問題,它經過引入間隙鎖的實現來解決幻讀,經過給符合條件的間隙加鎖,防止再次查詢的時候出現新數據產生幻讀的問題。
例如咱們執行下面的sql語句,就會對id大於100的記錄加鎖,在id>100的記錄中確定是有不存在的間隙:
Select * from employee where id> 100 for update;
複製代碼
(1)接着來測試間隙鎖,新增一個字段num,並將num添加爲普通索引、修改以前的數據使得num之間的值存在間隙,操做以下sql所示:
alter table employee add num int not null default 0;
update employee set num = 1 where id = 1; update employee set num = 1 where id = 2; update employee set num = 3 where id = 3; insert into employee values(4,'kris',4000,5); 複製代碼
(2)接着在session1的窗口開啓事務,並執行下面操做:
(3)同時打開窗口session2,並執行新增語句:
insert into employee values(5,'ceshi',5000,2); // 程序出現等待
insert into employee values(5,'ceshi',5000,4); // 程序出現等待 insert into employee values(5,'ceshi',5000,6); // 新增成功 insert into employee values(6,'ceshi',5000,0); // 新增成功 複製代碼
「從上面的測試結果顯示在區間(1,3]U[3,5)之間加了鎖,是不可以新增數據行,這就是新增num=2和num=4失敗的緣由,可是在這個區間之外的數據行是沒有加鎖的,能夠新增數據行」。
根據索引的有序性,而普通索引是能夠出現重複值,那麼當咱們第一個sesson查詢的時候只出現一條數據num=3,爲了解決第二次查詢的時候出現幻讀,也就是出現兩條或者更多num=3這樣查詢條件的數據。
Mysql在知足where條件的狀況下,給(1,3]U[3,5)
區間加上了鎖不容許插入num=3的數據行,這樣就解決了幻讀。
這裏拋出幾種狀況接着來測試間隙鎖。主鍵索引(惟一索引)是否會加上間隙所呢?範圍查詢是否會加上間隙鎖?使用不存在的檢索條件是否會加上間隙鎖?
先來講說:「主鍵索引(惟一索引)是否會加上間隙所呢?」
由於主鍵索引具備惟一性,不容許出現重複,那麼當進行等值查詢的時候id=3,只能有且只有一條數據,是不可能再出現id=3的第二條數據。
所以它只要鎖定這條數據(鎖定索引),在下次查詢當前讀的時候不會被刪除、或者更新id=3的數據行,也就保證了數據的一致性,因此主鍵索引因爲他的惟一性的緣由,是不須要加間隙鎖的。
再來講說第二個問題:「範圍查詢是否會加上間隙鎖?」
直接在session1中執行下面的sql語句,並在session2中在這個num>=3的查詢條件內和外新增數據:
select * from employee where num>=3 for update;
insert into employee values(6,'ceshi',5000,2); // 程序出現等待 insert into employee values(7,'ceshi',5000,4); // 程序出現等待 insert into employee values(8,'ceshi',5000,1); // 新增數據成功 複製代碼
咱們來分析如下原理:單查詢num>=3的時候,在現有的employee表中知足條件的數據行,以下所示:
id | num |
---|---|
3 | 3 |
4 | 5 |
5 | 6 |
那麼在設計者的角度出發,我爲了解決幻讀的現象:在num>=3的條件下是必須加上間隙鎖的。
而在小於num=3中,下一條數據行就是num=1了,爲了防止在(1,3]的範圍中加入了num=3的數據行,因此也給這個間隙加上了鎖,這就是添加num=2數據行出現等待的緣由。
最後來講一說:「使用不存在的檢索條件是否會加上間隙鎖?」
假如是查詢num>=8的數據行呢?由於employee表並不存在中num=8的數據行,num最大num=6,因此爲了解決幻讀(6,8]與num>=8也會加上鎖。
說到這裏我相信不少人已經對間隙鎖有了清晰和深刻的認識,能夠說是精通了,又能夠和麪試官互扯了。
假如你是第一次接觸Mysql的鎖機制,第一次確定是懵的,建議多認真的看幾遍,跟着案例敲一下本身深入的去體會,慢慢的就懂了。
死鎖在InnoDB中才會出現死鎖,MyISAM是不會出現死鎖,由於MyISAM支持的是表鎖,一次性獲取了全部得鎖,其它的線程只能排隊等候。
而InnoDB默認支持行鎖,獲取鎖是分步的,並非一次性獲取全部得鎖,所以在鎖競爭的時候就會出現死鎖的狀況。
雖然InnoDB會出現死鎖,可是並不影響InnoDB最受歡成爲迎的存儲引擎,MyISAM能夠理解爲串行化操做,讀寫有序,所以支持的併發性能低下。
舉一個例子,如今數據庫表employee中六條數據,以下所示:
其中name=ldc的有兩條數據,而且name字段爲普通索引,分別是id=2和id=3的數據行,如今假設有兩個事務分別執行下面的兩條sql語句:
// session1執行
update employee set num = 2 where name ='ldc'; // session2執行 select * from employee where id = 2 or id =3; 複製代碼
其中session1執行的sql獲取的數據行是兩條數據,假設先獲取到第一個id=2的數據行,而後cpu的時間分配給了另外一個事務,另外一個事務執行查詢操做獲取了第二行數據也就是id=3的數據行。
當事務2繼續執行的時候獲取到id=3的數據行,鎖定了id=3的數據行,此時cpu又將時間分配給了第一個事務,第一個事務執行準備獲取第二行數據的鎖,發現已經被其餘事務獲取了,它就處於等待的狀態。
當cpu把時間有分配給了第二個事務,第二個事務準備獲取第一行數據的鎖發現已經被第一個事務獲取了鎖,這樣就好了死鎖,兩個事務彼此之間相互等待。
第二種死鎖狀況就是當一個事務開始而且update一條id=1的數據行時,成功獲取到寫鎖,此時另外一個事務執行也update另外一條id=2的數據行時,也成功獲取到寫鎖(id爲主鍵)。
此時cpu將時間分配給了事務一,事務一接着也是update id=2的數據行,由於事務二已經獲取到id=2數據行的鎖,因此事務已處於等待狀態。
事務二有獲取到了時間,像執行update id=1的數據行,可是此時id=1的鎖被事務一獲取到了,事務二也處於等待的狀態,所以造成了死鎖。
session1 | session2 |
---|---|
begin;update t set name='測試' where id=1; | begin |
update t set name='測試' where id=2; | |
update t set name='測試' where id=2; | |
等待..... | update t set name='測試' where id=1; |
等待..... | 等待...... |
首先要解決死鎖問題,在程序的設計上,當發現程序有高併發的訪問某一個表時,儘可能對該表的執行操做串行化,或者鎖升級,一次性獲取全部的鎖資源。
而後也能夠設置參數innodb_lock_wait_timeout
,超時時間,而且將參數innodb_deadlock_detect
打開,當發現死鎖的時候,自動回滾其中的某一個事務。
上面詳細的介紹了MyISAM和InnoDB兩種存儲引擎的鎖機制的實現,並進行了測試。
MyISAM的表鎖分爲兩種模式:「共享讀鎖」和「排它寫鎖」。獲取的讀鎖的線程對該數據行只能讀,不能修改,其它線程也只能對該數據行加讀鎖。
獲取到寫鎖的線程對該數據行既能讀也能寫,對其餘線程對該數據行的讀寫具備排它性。
MyISAM中默認寫優先於去操做,所以MyISAM通常不適合運用於大量讀寫操做的程序中。
InnoDB的行鎖雖然會出現死鎖的可能,可是InnoDB的支持的併發性能比MyISAM好,行鎖的粒度最小,必定的方法和措施能夠解決死鎖的發生,極大的發揮InnoDB的性能。
InnoDB中引入了間隙鎖的概念來決解出現幻讀的問題,也引入事務的特性,經過事務的四種隔離級別,來下降鎖衝突,提升併發性能。