文章目錄
- lock與latch
- 鎖的類型
- MVCC
- 一致性非鎖定讀(快照讀)
- 一致性鎖定讀(當前讀)
- 鎖算法
- 死鎖
- 鎖升級
lock與latch
在了解數據庫鎖之前,首先就要區分開 lock
和 latch
。在數據庫中,lock
和 latch
雖然都是鎖,卻有著截然不同的含義。
-
latch
通常被我們稱為閂鎖(輕量級鎖),因為其要求鎖定的時間必須非常短。在InnoDB
中,latch
可以分為mutex(互斥鎖)
和rwlock(讀寫鎖)
,它的作用是用來保證并發線程操作臨界資源的正確性,并且通常沒有死鎖檢測機制。 -
lock
的操作對象則是事務,用來鎖定數據庫中的對象,如表、頁、行等,一般lock
的對象僅在事務提交或者回滾后釋放,lock 有死鎖檢測機制。
關于數據庫頁結構的詳細內容可以看這篇博客
鎖的類型
在 InnoDB存儲引擎
中實現了下面兩種標準的行級鎖:
- 共享鎖(S Lock) : 允許事務讀一行數據
- 排他鎖(X Lock) : 允許事務刪除或者更新一行數據。
由于共享鎖并不涉及到數據的修改,所以即使一個事務已經獲得了某行的共享鎖,另外的事務也可以立即獲得該行的共享鎖,這種情況又被稱為鎖兼容。
對于排他鎖又是另一種情況,由于排他鎖涉及到了數據的修改,為了保證安全,其他的事務想要獲得同一行的排他鎖時,必須要等到前一個事務釋放鎖才行,這種情況又被稱為鎖不兼容。
下面是排他鎖和共享鎖的兼容性:
由于 InnoDB
支持多粒度鎖定,所以允許事務可以同時存在行鎖和表鎖,為了支持在不同粒度上進行加鎖操作,InnoDB
支持一種額外的鎖方式,即 意向鎖(Intention Lock)。意向鎖即將鎖定的對象分為多個層次,意味著希望事務在更細粒度上進行加鎖。
如果我們想對下層的對象(如記錄【一行就是一個記錄】)上一個 X鎖
,就需要先對粒度更粗的上層對象上鎖,需要分別先對數據庫、表、頁上 意向排他鎖(IX鎖)
,再對記錄上 X鎖
。(如果沒有意向鎖,我們對一行上了讀鎖之后,假如某個事務申請了一個表級的寫鎖,此時這個事務就會對我們上鎖的數據進行修改。)
PS:讀/寫鎖
是 共享/排他鎖
的一種。
InnoDB
支持意向鎖設計比較簡練,其意向鎖即為表級別的鎖,意向鎖主要是為了在一個事務中揭示下一行將被請求的鎖的類型,兩種意向鎖分別如下:
- 意向共享鎖(
IS Lock
),事務要想獲得某一張表中某幾行的共享鎖。 - 意向排他鎖(
IX Lock
),事務要想獲得某一張表中某幾行的排他鎖。
由于 InnoDB
支持的是行級別的鎖,因此意向鎖不會阻塞 除全表掃描外 的任何請求,故兼容性如下:
MVCC
MVCC(Multi-Version Concurrency Control)即多版本并發控制,是數據庫并發控制的一種方法
一致性非鎖定讀(快照讀)
如果我們讀取的行正在執行 DELETE
或者 UPDATE
操作,這時就不會去等待鎖釋放后再讀取,而是直接去讀取行的一個快照數據,如下圖所示:
快照數據指的是該行之前版本的數據,這些數據存儲在 undo段
中,由于 undo
用來在事務中回滾數據,因此這些快照數據本身并沒有額外的開銷。并且我們只是讀操作,并不涉及修改,而且也沒有事務會去對歷史數據進行修改,所以在讀取快照數據的時候不需要進行加鎖。
快照讀機制讓讀取操作不再占用和等待表上的鎖,極大的提高了數據庫的性能。但由于每個快照都相當于是一個歷史版本,行的多版本需要并發控制—— MVCC
。
需要注意的是,MVCC
只在 READ COMMITTED(讀已提交)
和 REPEATABLE READ(可重復讀)
兩個隔離級別下工作。由于 READ UNCOMMITTED(讀未提交)
總會讀取最新的數據,而 SERIALIZABLE(可串行化)
會對所有讀取的行加鎖,所以這兩種都不兼容 MVCC
。
- 在
RC
隔離級別下,是每個快照讀都會生成并獲取最新的Read View
,也就是同一個事務中每次快照讀的結果都不一樣; - 在
RR
隔離級別下,則是同一個事務中的第一個快照讀才會創建Read View
, 之后的快照讀獲取的都是同一個Read View
,也就是同一個事務中每次快照讀的結果都跟第一次快照讀一樣。
當前讀,快照讀和MVCC的關系
MVCC
多版本并發控制是 「維持一個數據的多個版本,使得讀寫操作沒有沖突」 的概念,只是一個抽象概念,并非實現。MVCC
只是一個抽象概念,MySQL 需要提供具體的功能去實現它,「快照讀就是 MySQL 實現了 MVCC 理想模型的其中一個功能——非阻塞讀」。而相對而言,當前讀就是悲觀鎖的具體功能實現。- 要說的再細致一些,快照讀本身也是一個抽象概念,再深入研究。MVCC 模型在 MySQL 中的具體實現則是由
3 個隱式字段
、undo 日志
、Read View
等去完成的。
更多MVCC知識可以閱讀這篇博客
一致性鎖定讀(當前讀)
一致性鎖定讀又稱為當前讀,讀取的是行的最新版本,并且讀取的時候為了防止其他事務修改當前行,還會對當前行進行加鎖。
在 InnoDB
中,對于 SELECT語句
支持以下兩種一致性鎖定讀操作:
- SELECT…FOR UPDATE(排他鎖) :會對讀取的行加一個排他鎖,此時其他事務不能對該行上任何鎖。
- SELECT…LOCK IN SHARE MODE(共享鎖) :會位讀取的行加上一個共享鎖,此時其余的事務可以對該行加上共享鎖,但是如果想加排他鎖,則會被阻塞。
鎖算法
在 InnoDB
中有三種行鎖的算法:
- Record Lock(記錄鎖): 單個行記錄上的鎖。
- Gap Lock(間隙鎖) : 鎖定一個范圍,但不包含記錄本身。
- Next-Key Lock(下一鍵鎖) : 前兩種鎖的結合,既鎖定一個范圍,也鎖定記錄本身。
舉例:
有一組數據,其索引分別為 10、30、60
,此時使用 SQL
語句 SELECT * FROM t WHERE id = 10 FOR UPDATE
,三種鎖的范圍如下
- Record: 對10單行進行加鎖
- Gap Lock : (-∞, 10)、(10, 30)、(30, 60)、(60, +∞)
- Next-Key Lock: (-∞,10]、(10,30]、(30,60]、(60,+∞)
對于 Record Lockl
來說,其總是會去鎖住索引記錄,即使沒有設置任何一個索引,它也會使用隱式的索引進行鎖定。
Next-Key Lock 是
結合了前面所說的兩種鎖算法,既鎖住范圍,也鎖住記錄本身,在 InnoDB
中對于行的查詢都會采用這種算法,而設計它的目的正是為了解決幻讀問題。
幻讀指 在同一事務中,用同樣的操作讀取兩次,得到的記錄數卻不一樣(針對同一個范圍的數據)。 主要原因就是當第一個事務對表中的所有數據行進行修改;同時,第二個事務向表中插入了一行。這樣也就導致了操作第一個事務的用戶發現表中還有沒修改的數據行,像發生了幻覺一樣。
明明在 會話A
的第一次查詢中,大于 2
的數只有行只有一行,而由于 會話B
插入了新行后,對于 會話A
而言就憑空多出來了一行,像出現了幻覺一樣。
對于以上數據,Next-Key Locking算法
在 SELECT * FROM t WHERE a > 2 FOR UPDATE
這條語句中,鎖住的不僅僅是 5
這個數值,而是對直接對[2,+∞)
這個范圍加了排他鎖,所以任何對于這個范圍的插入都不能進行,也就避免了幻讀現象的發生。
同理,間隙鎖 Gap Lock
也是鎖定某個范圍,所以它也能防止幻讀的出現。
InnoDB
正是借助 鎖(Gap Lock、Next-Key Lock)
以及 MVCC(快照讀)
這兩個機制實現了事務的隔離性。
死鎖
死鎖指的是兩個或者兩個以上的事務在執行過程中,因為爭搶所資源而導致的一種互相等待的現象。在死鎖的情況下,如果沒有外力作用,事務將永遠無法推進下去。
在數據庫中通常都會使用超時機制來解決死鎖:為事務設置超時時間,當其中一方超時后立刻進行回滾,另一個事務就能夠繼續進行了。
雖然超時機制可以解決這個問題,但是我們并不能掌握回滾的事務的量級,倘若事務更新龐大,則回滾就會帶來大量的性能損耗,所以我們通常會采用更加主動的策略,即使用等待圖來進行死鎖檢測:
- 圖中每個節點即為一個事務。
- 每條指向其他節點的線則代表著正在等待該節點的資源。
- 當存在回路時,則代表著事務互相等待,此時就意味著存在死鎖。
每當事務請求鎖并發生等待時,都會主動判斷等待圖中是否存在回路,如果存在則代表著有死鎖產生,此時就會主動選擇 undo量最小的事務 來打破死鎖。在現版本的 InnoDB
中,通常采用 深度優先搜索(老版本使用遞歸) 來檢測死鎖的存在。
鎖升級
在數據庫為了保證安全,大量的并發下必定存在著大量的鎖,但鎖是一種稀有資源,為了避免大量鎖的開銷,數據庫中存在著鎖升級的機制。
鎖升級指的是將當前的鎖升級為更粗粒度的鎖 例如我們可以將多個行鎖升級為一個頁鎖,又或者將多個頁鎖升級為一個表鎖。這種升級減少了鎖的數量、保護了系統資源,防止系統使用太多內存來維護大量的鎖,在一定程度上提高了效率。
在 SQL Server
中,鎖升級是很常見的現象,當滿足以下條件中其中一個時則會進行鎖升級:
- 鎖資源占用的內存超過了激活內存的
40%
- 一條單獨的
SQL語句
在一個對象上持有的鎖數量超過了閾值,閾值默認為5000
而在 MySQL
的 InnoDB
中,則不存在鎖升級的問題。其根據每個事務訪問的每個頁對鎖進行管理,并且使用位圖來標記,所以一個事務無論鎖住頁中多少條記錄,開銷都相同。