本篇速覽
鎖是計算機協調多個程序或線程并發通路某一資源的機制。在資料庫中,除了傳統的計算資源(CPU、RAM、I/O)的争用以外,資料也是一種供許多使用者共享的資源,如何保證資料并發通路的一緻性、有效性是所有資料庫必須解決的一個問題,鎖沖突也是影響資料庫并發通路性能的一個重要因素。
在Java中我們就提到過鎖的概念,鎖存在的意義就是幫助我們解決并發問題,當多個線程并發通路的時候,鎖是幫助我們解決問題的一個很好的方法,但是類似于Java中的 synchronize關鍵字鎖的對象,可以是一個對象,也可以是一個方法,選擇合适的對象以及鎖能夠幫助我們提高資料庫的并發通路性能,而我們如何使用鎖,又如何使用合适的鎖呢?本文下面将從這三種鎖來深入了解MySQL的鎖機制:
- 1️⃣ 全局鎖:鎖定資料庫中的所有表
- 2️⃣ 表級鎖:每次操作鎖住整張表
- 3️⃣ 行級鎖:每次操作鎖住對應的行資料
1️⃣ 全局鎖
全局鎖就是對于整個資料庫執行個體加鎖,在加鎖之後,整個資料庫就處于隻讀的狀态,後續的DML的寫語句,DDL語句,已經更新的事務送出語句都會被阻塞。
看到全局鎖的功能,可能會覺得,鎖的粒度這麼大,有什麼用呢?
它最典型的使用場景時做全庫的邏輯備份,對所有的表進行鎖定,進而擷取一緻性視圖,保證資料的完整性。
上面的場景聽起來很有道理,但是我們不妨走入實際場景,想想為什麼備份要加全局鎖呢?又有哪些原因呢?
假設我有兩張表order,stock,也就是訂單表和庫存表,現在假設沒有全局鎖,我開始導出資料。在導出stock的過程中,order表新增的一條記錄,理論上講,stock應該減少一條資料,但是stock的資料已經導出了,是以最後導出的資料就會産生髒資料,stock的資料與order的資料對應不上。
如果還是這個場景,我們如何去解決呢,如何加全局鎖,加了全局鎖又會有什麼樣的效果呢?
首先談談如何去加全局鎖,也就是全局鎖的文法:FLUSH tables with read lock
接下來可以執行資料備份,借用MySQL中提供給我們的工具 mysqldump 進行資料備份:
mysqldump -uroot -p1234 databaseName > fileName.sql
資料備份結束後,就可以進行解鎖:unlock tables
誠然,全局鎖能幫助我們解決導出資料庫的時候的髒資料,但是使用全局鎖也存在一定的問題:
資料庫中加全局鎖,鎖的粒度很大,是一個比較重的操作:
- 在加鎖期間,其他用戶端在執行寫入操作的時候都會阻塞,業務基本上就處停擺狀态
- 如果資料庫不是單機,而是主從結構,甚至做了讀寫分離,我們在做寫入的時候不會阻塞,因為我們可以從從庫中做備份,而資料寫入主庫,但是存在的問題就是,在備份期間從庫不能執行主庫同步過來的二進制日志,就會産生主從延遲
經典“白學”: 在InnoDB存儲引擎中,我們為了解決這一個問題,可以在備份中加上一個參數 --single-transaction 參數來完成不加鎖的一緻性資料備份。為什麼可以這樣呢,是因為在InnoDB存儲引擎的底層,它實際使用的是快照讀實作。
2️⃣ 表級鎖
表級鎖,每次操作鎖住整張表,鎖的粒度也比較大,容易發生鎖沖突的機率最高,并發度最低。
對于表級鎖,可以分為這三類:表鎖、中繼資料鎖、意向鎖。接下來,我們分别來了解這三種表級鎖,了解它們的具體使用場景和使用方法:
⛏ 表鎖
對于表鎖,我們又可以分為兩類:表共享讀鎖(read lock)、表獨占寫鎖(write lock)。我們有時候簡稱他們為:讀鎖、寫鎖
在了解他們的差別、應用之前,我們首先來了解一下加鎖和解鎖的文法,友善我們後續的使用:
- 加鎖:lock tables 表名…… read/write
- 解鎖:unlock tables 或者 斷開MySQL連接配接,鎖會自動釋放
然後再談談讀鎖和寫鎖的鎖的範圍。其實在最開始看到讀鎖和寫鎖的時候,我的第一反應是JUC裡面的讀寫分離的鎖,但是在具體了解到MySQL的讀鎖和寫鎖後,發現他們的差别很大:
讀鎖
如果對表加了讀鎖,那麼其他的用戶端是能繼續讀的,但是不能寫,與JUC的讀寫分離鎖的區分的點就在于,對于加鎖的線程也不能寫,隻能讀:
如圖,右側的用戶端加了讀鎖,左側的用戶端能進行查詢,但是做不了插入,插入的語句會被阻塞,會在解鎖的時候執行
可以用這個圖來表示加了讀鎖時候的狀态:(其中綠色表示能夠執行,紅色表示會阻塞)
不論是加鎖的線程,還是其他的線程,都隻能查詢,不能修改,當然,在解鎖後,修改的語句會自動執行。
⚫ 寫鎖
如果對表加了寫鎖,其實我們可以了解為一個線程鎖,隻有這個加鎖的線程能操作這個表,包括查詢和修改,其他的線程既不能查詢也不能修改,都會被阻塞。
可以用這個圖來表示加了寫鎖時候的狀态:(其中綠色表示能夠執行,紅色表示會阻塞)
⚒ 中繼資料鎖(meta data lock)
中繼資料鎖的加鎖過程是系統自動控制的,不需要顯式使用,在通路一張表的時候就會自動加上。中繼資料鎖的主要作用就是維護表中繼資料的資料一緻性,在表上有活動事務的時候,不可以對中繼資料進行寫入操作。為的就是避免DML和DDL沖突,保證讀寫的正确性。
這裡提到了中繼資料,那麼什麼是中繼資料呢?
中繼資料:簡單說,中繼資料可以簡單了解為表結構。是以其實中繼資料鎖是MySQL底層維護的一把防止表的結構發生改變的鎖。
在MySQL5.5中引入了MDL(中繼資料鎖):
- 當對一張表進行增删改的時候,會給表加一個MDL讀鎖(共享)
- 當對表結構進行變更的時候,加MDL寫鎖(排他)
我們先來看看各種SQL語句對應的中繼資料鎖:
SQL語句 | 中繼資料鎖類型 | 說明 |
lock tables tableName read/write | SHARED_READ_ONLY / SHARED_NO_READ_WRITE | |
select, select…… lock in share mode(共享鎖,後續會提到) | SHARED_READ | 與SHARED_WRITE相容,與EXCLUSIVE互斥 |
insert, update, delete, select……for update | SHARED_WRITE | 與SHARED_READ相容,與EXCLUSIVE互斥 |
alter tables | EXCLUSIVE | 與其他的MDL都互斥 |
其實我們得到的資訊大概就是,MySQL給增删改查的SQL加了中繼資料鎖,其中,修改表結構的鎖與其他的增删改查的鎖互斥,也就是說:中繼資料鎖就是為了保證表的完整性和資料完整性,一緻性。防止在修改資料的過程中出現表結構被修改的情況。
聽完上面的描述,可能對于中繼資料鎖的作用有了一定了解,但是還是感覺有點模糊,我們不妨舉個例子,如圖:
在一個線程我們開啟一個事務,然後在事務中進行查詢,此時就加上了中繼資料鎖SHARED_READ鎖
此時在另外一個線程中去修改表結構,也就是要加上EXLUSIVE這個鎖,但是兩個鎖互斥,就導緻了,修改表結構阻塞。
如果想要更深一步了解中繼資料鎖,除了看到它的效果(上述的修改表結構的SQL語句被阻塞),還要去看看SQL語句對應的鎖,我們可以借用這條SQL來檢視中繼資料鎖:
SELECT object_type, object_schema, object_name, lock_type, lock_duration, from performance_schema.metadata_locks;
意向鎖
我們先不想什麼是意向鎖,我們先看一個場景:
對于下面這張表
線程A開啟事務,根據主鍵對資料進行更改,此時會給該行加上一個行鎖
而此時線程B要給表加上一個表鎖,由于表鎖可能會與行鎖進行沖突,是以線程B要全表掃描,是否存在行鎖,如果存在行鎖就阻塞。
很明顯,全表掃描行鎖的效率很低,是以就引入了我們的意向鎖。
那意向鎖是怎麼做到提高效率的呢?
DML語句和for update會給表加上意向鎖,同時會給索引添加行鎖,表的意向鎖與表鎖是互斥的(從大體上來講,特殊情況我們後續講),因為二者互斥,表鎖就無法加進去,這樣就不用全表掃描去找行鎖了。
剛剛有提到特殊情況,特殊情況是怎麼回事呢?
意向鎖分為兩種:
- 意向共享鎖:與表鎖讀鎖相容,與表鎖寫鎖互斥
- 意向排他鎖:與表鎖的寫鎖和讀鎖都互斥
也就是說,我們的說法可以了解為:
- 普通的DML語句和for update語句會給表加上意向排它鎖,與表鎖互斥,這樣在加表鎖的時候就會阻塞
- 而共享鎖SELECT …… lock in share mode會給表加上意向共享鎖,與表鎖的讀鎖相容,此時是可以給表加上讀鎖的
其實可以說,意向鎖有點像表的一種狀态,一共是三種狀态:不能加表鎖,隻能加讀鎖,能加表鎖,而SQL語句就是改變意向鎖狀态的關鍵
3️⃣ 行級鎖
行級鎖,每次操作鎖住對應的行資料,鎖定粒度最小,發生鎖沖突的機率最低,并發度最高。隻應用在InnoDB存儲引擎中,這一點我們在講存儲引擎中有提到。
InnoDB的資料是基于索引組織的,行鎖是通過對索引上的索引項加鎖來實作的,而不是對記錄加的鎖。對于行級鎖,主要分為以下三類:
- 行鎖:鎖定單個行記錄的鎖,防止其他事務對此進行update和delete。在RC、RR隔離級别下都支援。
- 間隙鎖:鎖定索引間隙,確定索引記錄的間隙不變,反之其他事務對這個間隙插入,産生幻讀。在RR隔離級别下支援。這裡提到了間隙,那間隙是什麼了,就是下圖中兩個資料之間的間隙,那個鎖的圖示就是間隙鎖。
- 臨鍵鎖:行鎖和間隙鎖的組合,同時鎖住資料和資料前面的間隙,也就是行鎖和間隙鎖的組合,在RR隔離級别下支援。
行鎖
InnoDB實作了兩種類型的行鎖:
- 共享鎖:允許一個事務去讀一行,阻止其他事務獲得相同資料集的排它鎖。簡單來說就是,共享鎖與共享鎖相容,但是共享鎖與排它鎖互斥。
- 排它鎖:允許擷取排它鎖的事務更新資料,也就是說事務如果拿到了這行的排它鎖,它就可以更新資料,阻止其他事務獲得相同資料集的共享鎖和排它鎖,也就是說其他事務不能拿到這行資料的共享鎖和排它鎖。
- 綜上:共享鎖和共享鎖相容,但是排它鎖與其他鎖都不相容
那麼在常見的增删查改的SQL語句中,它們加的是何種類型的行鎖呢?
SQL | 行鎖類型 | 說明 |
INSERT | 排它鎖 | 自動加鎖 |
UPDATE | 排它鎖 | 自動加鎖 |
DELETE | 排它鎖 | 自動加鎖 |
SELECT | 不加鎖 | |
SELECT…… lock in share mode | 共享鎖 | 需要手動在SELECT之後加上LOCK IN SHARE MODE |
SELECT…… FOR UPDATE | 排它鎖 | 需要手動在SELECT之後加上FOR UPDATE |
預設情況下,InnoDB存儲引擎在RR事務隔離級别下運作,InnoDB會使用next-key鎖進行搜尋和索引掃描,以防止幻讀。
- 針對唯一索引進行檢索的時候,對于已存在的記錄進行等值比對時,将自動優化為行鎖
- InnoDB的行鎖是針對索引加的鎖,不通過索引條件檢索資料的話,InnoDB會對表中所有資料加鎖,此時就會更新為表鎖
間隙鎖 & 臨鍵鎖
如果說行鎖的目的是防止兩個事務對同一條記錄做更改,那間隙鎖又有什麼作用呢?我們以下面這個場景來了解一下間隙鎖的使用:
如圖我們擁有一張表,ID分别為1,3,8,11,19,25。
現在,我們開啟一個事務,修改一條不存在的ID的資料,比如UPDATE STU SET age = 10 where id = 5
那麼此時,InnoDB就會在3和8之間加上間隙鎖,那麼此時在另外一個線程插入資料:
INSERT INTO stu values(7,'Ruby',1)
就會因為間隙鎖而阻塞
是以我們可以得到:索引上的等值查詢(唯一索引),給不存在的記錄加鎖的時候,優化為間隙鎖。
總結
本篇分别介紹了三種鎖:全局鎖、表級鎖、行級鎖,三種鎖的粒度不同,使用的場景也不同。
- 首先說說全局鎖,全局鎖的粒度最大,它的使用場景是做全庫的邏輯備份,防止産生髒資料,但是它也有它的弊端,因為全局鎖的粒度太大了,盡管做了主從和讀寫分離也會存在一定的問題,當然InnoDB也給我們提供了備份的方法。
- 再說說表級鎖,表級鎖有三種分别為表鎖、中繼資料鎖、意向鎖。表鎖要弄清楚讀鎖和寫鎖的差別,中繼資料鎖是為了維護表結構中繼資料的資料一緻性,防止表結構被篡改(在寫資料的時候),意向鎖可以了解為表的一種狀态,是否能夠使用表鎖的狀态,它的底層由InnoDB維護,會随着SQL語句的變化而變化。
- 然後就是行級鎖,行級鎖也有三種,分别為:行鎖、間隙鎖、臨鍵鎖。行鎖存在的目的是為了,保證一條資料的讀寫的正确性,有點類似于表級鎖的中繼資料鎖,而間隙鎖和臨鍵鎖存在的目的就是為了防止資料在更新和插入的時候間隙不被修改,而導緻幻讀的問題。
原文:https://juejin.cn/post/7170707711208718344