JAVA面試系列 - Mysql InnoDB 鎖機制介紹

概述

在mysql中,鎖機制看起來很複雜,一堆名詞:排他鎖、共享鎖、表鎖、間隙鎖、意向鎖等等,搞的初學者雲裏霧裏。同時鎖的相關知識又跟事務隔離級別、索引等概念有千絲萬縷的關係,是面試中的常規問題。mysql

Innodb鎖.png

上面的腦圖是對mysql鎖相關知識的一個梳理,但願可以幫到你們,讓你們可以:面試

  • 能讓咱們在特定的場景下派得上用場
  • 更好把控本身寫的程序
  • 在跟別人聊數據庫技術的時候能夠搭上幾句話
  • 構建本身的知識庫體系!在面試的時候不虛

讀、寫鎖

按鎖的應用場景來看,分爲讀鎖和寫鎖,讀鎖又可稱爲S鎖和共享鎖;寫鎖又可稱爲X鎖和排他鎖。簡單來講,讀鎖 = S鎖 = 共享鎖,一樣,寫鎖 = X鎖 = 排他鎖。
正如他們的取名,只要碰到排他鎖,那麼就會阻塞,具體阻塞狀況以下表:sql

讀鎖 寫鎖
讀鎖
寫鎖
  • 讀讀不阻塞:當前用戶在讀數據,其餘的用戶也在讀數據,不會加鎖
  • 讀寫阻塞:當前用戶在讀數據,其餘的用戶不能修改當前用戶讀的數據,會加鎖!
  • 寫寫阻塞:當前用戶在修改數據,其餘的用戶不能修改當前用戶正在修改的數據,會加鎖!

讀鎖和寫鎖是互斥的,讀寫操做是串行數據庫

行鎖

咱們使用Mysql通常是使用InnoDB存儲引擎的。InnoDB和MyISAM有兩個本質的區別:併發

  • InnoDB支持行鎖
  • InnoDB支持事務

對於行鎖來講,也分2種類型的鎖mvc

  • 共享鎖(S鎖),容許一個事務去讀一行,阻止其餘事務得到相同數據集的排他鎖。也叫作讀鎖,讀鎖是共享的,多個客戶能夠同時讀取同一個資源,但不容許其餘客戶修改。
  • 排他鎖(X鎖),容許得到排他鎖的事務更新數據,阻止其餘事務取得相同數據集的共享讀鎖和排他寫鎖。也叫作寫鎖,寫鎖是排他的,寫鎖會阻塞其餘的寫鎖和讀鎖。

其實事務隔離級別就是經過鎖機制來實現的,只不過隱藏了加鎖的細節,下面來看看二者的關係。post

事務隔離級別

你們都知道,innodb的事務隔離級別有4種性能

  • Read uncommitted,會出現髒讀、不可重複讀、幻讀

髒讀就是一個事務讀取到另外一個事務未提交的數據。出現髒讀的本質就是由於操做(修改)完該數據就立馬釋放掉鎖,致使讀的數據就變成了無用的或者是錯誤的數據。spa

  • Read committed,會出現不可重複讀、幻讀

避免髒讀的作法很簡單:就是把釋放鎖的位置調整到事務提交以後,此時在事務提交前,其餘進程是沒法對該行數據進行讀取的。即讀寫是串行的。
但Read committed會出現不可重複讀,即一個事務讀取到另一個事務已經提交的數據,也就是說一個事務能夠看到其餘事務所作的修改。屢次查詢數據庫的結果都不同。code

  • Repeatable read,會出現幻讀

和不可重複讀相似,但虛讀(幻讀)會讀到其餘事務的插入的數據,致使先後讀取不一致。能夠把不可重複讀理解爲數據更新,幻讀是數據插入。
innodb經過MVCC解決了不可重複讀的問題,MVCC的具體原理下面介紹。同時結合間隙鎖,避免了幻讀。即innodb的Repeatable read其實不會出現幻讀的問題,innodb的事務默認級別就是Repeatable read

  • Serializable,串行,避免以上全部問題

那麼MVCC到底是一種什麼機制,可以解決不可重複讀的問題?

MVCC

MVCC即多版本併發控制,能夠簡單認爲 是行級鎖的一個升級版。前面提到,只有讀-讀場景是不阻塞的,其餘只有要寫(排他鎖)場景,都是阻塞的,必定程度上影響了讀寫效率。基於提高併發性能的考慮,MVCC通常讀寫是不阻塞的,因此說MVCC不少狀況下避免了加鎖的操做。

InnoDB中的MVCC,是經過在每行記錄後面保存兩個隱藏的列來實現的。這兩個列,一個保存了行的建立時間,一個保存行的刪除時間。固然存儲的並非實際的時間值,而是系統版本號。沒開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會做爲此事務的版本號,用來和查詢到的每行記錄的版本號進行比較。

舉個select的例子,InnoDB會根據如下兩個條件檢查每行記錄:

  1. InnoDB只查找版本早於當前事務版本的數據行,這樣能夠確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的
  2. 行的刪除版本要麼未定義,要麼大於當前事務版本號,這能夠確保事務讀取到的行,在事務開始以前未被刪除。

簡單總結下,多版本併發控制(MVCC)是一種用來解決讀-寫衝突的無鎖併發控制,也就是爲事務分配單向增加的時間戳,爲每一個修改保存一個版本,版本與事務時間戳關聯,讀操做只讀該事務開始前的數據庫的快照。 這樣在讀操做不用阻塞寫操做,寫操做不用阻塞讀操做的同時,避免了髒讀和不可重複讀。

MVCC雖然解決了不可重複讀問題,可是沒法解決幻讀,須要配合間隙鎖。

間隙鎖(解決幻讀)

首先咱們看個例子,初始表以下:

id x y 建立時間 刪除時間
1 30 10 1 undefined

很簡單,一個自增id,一列x,一列y,假設有個限制條件:x+y <= 100。而後兩個事務同時併發執行:

  • T2(事務id=2):set y=60,事務隔離級別是不可重複度,因此此事務內,x=30, y=10,更新y後,x+y=30+60 = 90,知足條件
  • T3(事務id=3):set x=50,事務隔離級別是不可重複度,因此此事務內,x=50, y=10,更新y後,x+y=50+10 = 60,知足條件

T2和T3提交後,x+y=50+60=110 不符合小於100的要求。

Update的本質是 read --> write,MySQL(innodb)爲了解決這個問題,強行把 read 分紅了 snapshot read(快照讀)和 locking read (當前讀)。在 UPDATE 或者 SELECT ... FOR UPDATE 的時候,innodb 引擎實際執行的是當前讀。
在一個支持MVCC的併發系統中, 咱們須要支持兩種讀, 一個是快照讀, 一個是當前讀。
快照讀:簡單的select操做,屬於快照讀,不加鎖。
當前讀:特殊的讀操做,插入/更新/刪除操做,屬於當前讀,須要加鎖, 讀取的是最新數據。

給一個幻讀的例子:

  • 事務A種執行修改
update user set col1='new_val' where id=1;
結果: 
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
  • 事務B插入一條數據,並提交
begin;
insert into user values('A');
commit;
  • 事務A種再次執行修改
update user set col1='new_val' where id=1;
結果:
Query OK, 1 rows affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

問題的本質是由於 update語句的查找階段至關於select ... for update,這會更新事務A的 ReadView,從而能夠讀到「其餘事務已提交的修改」。即出現了幻讀

把上面的例子用事務版本號來解釋:

  • 事務A(事務版本號=1)種執行修改,此時是空表,因此update的select結果是0
  • 事務B(事務版本號=2)插入一條數據,並提交
id col1 建立時間 刪除時間
1 A 2 undefined
  • 此時若是事務A進行快照讀(snapshot select),那麼按照剛纔mvcc的解釋,因爲庫中id=1的記錄的建立時間(事務版本號)爲2,大於當前事務的版本號1,因此不會被查找出來,符合Repeatable read。可是若是此時執行的是update操做,執行的讀是當前讀(select for update)(InnoDB執行UPDATE,其實是新插入了一行記錄,並保存其建立時間爲當前事務的ID,同時保存當前事務ID到要UPDATE的行的刪除時間),成功更新id=1的記錄,即幻讀
id col1 建立時間 刪除時間
1 A 2 1
1 A 1 undefined

InnoDB 經過加間隙鎖的方式,解決幻讀。innodb對於鍵值在條件範圍內但並不存在的記錄(叫作『間隙』)加鎖,這種鎖機制就是所謂的間隙鎖。 相對的,能夠把上面不一樣的行鎖稱爲記錄鎖
間隙鎖產生的條件分惟一索引和普通索引:

惟一索引

  • 對於指定查詢某一條記錄的加鎖語句,若是該記錄不存在,會產生記錄鎖和間隙鎖,若是記錄存在,則只會產生記錄鎖
  • 對於查找某一範圍內的查詢語句,會產生間隙鎖,如:WHERE id BETWEEN 5 AND 7 FOR UPDATE

普通索引

  • 在普通索引列上,不論是何種查詢,只要加鎖,都會產生間隙鎖,這跟惟一索引不同
  • 在普通索引跟惟一索引中,數據間隙的分析,數據行是優先根據普通索引排序,再根據惟一索引排序。
具體實驗例子能夠參考 MySQL的鎖機制 - 記錄鎖、間隙鎖、臨鍵鎖,很是詳細。

針對以上幻讀的例子,update語句select * from user where id=1 for update,id是惟一索引,可是因爲id=1的記錄不存在,因而產生了間隙鎖(排他),能夠阻塞其餘事務的insert操做。

MySQL(innodb)的選擇是容許在快照讀以後執行當前讀,而且更新 snapshot 鏡像的版本。嚴格來講,這個結果違反了 repeatable read 隔離級別,,可是 who cares 呢,畢竟官方都說了:「This is not a bug but an intended and documented behavior.」

表鎖

表鎖相對來講就很簡單了,表鎖顧名思義,就是鎖針對的範圍是整張表。表鎖開銷小,加鎖快,不會出現死鎖;鎖定力度大,發生鎖衝突機率高,併發度最低。如今考慮這樣一個情景:

事務A獲取了某一行的排他鎖,並未提交:

SELECT * FROM users WHERE id = 6 FOR UPDATE;

事務B想要獲取users表的表鎖:

LOCK TABLES users READ;

由於共享鎖與排他鎖互斥,因此事務B得確保:

  • 當前沒有其餘事務持有 users 表的排他鎖。
  • 當前沒有其餘事務持有 users 表中任意一行的排他鎖 。

爲了檢測是否知足第二個條件,事務 B 必須在確保 users表不存在任何排他鎖的前提下,去檢測表中的每一行是否存在排他鎖。掃描全部行這明顯是一個效率不好的作法,因而提出了意向鎖

意向鎖

事務在獲取行鎖(包括讀鎖和寫鎖)的同時會獲取表的意向鎖(包括讀鎖和寫鎖)。
意向鎖之間是相互兼容的:

意向共享鎖 意向排他鎖
意向共享鎖 兼容 兼容
意向排他鎖 兼容 兼容

意向鎖和表級鎖之間存在互斥狀況

意向共享鎖 意向排他鎖
表級共享鎖 兼容 互斥
表級排他鎖 互斥 互斥

這裏再次強調下:
這裏的排他 / 共享鎖指的都是表鎖!!!意向鎖不會與行級的共享 / 排他鎖互斥!!!
意向鎖不會與行級的共享 / 排他鎖互斥!!!
如今再回過頭來看剛纔的例子:
事務A獲取了某一行的排他鎖,並未提交:

SELECT * FROM users WHERE id = 6 FOR UPDATE;

此時,事務A也獲取了users表的意向排他鎖,

事務B想要獲取users表的表鎖:

LOCK TABLES users READ;

發現此表存在乎向排他鎖,因而事務B被阻塞,直到意向排他鎖被釋放。

參考文檔

相關文章
相關標籤/搜索