MySQL-鎖總結

鎖機制用於管理對共享資源的併發訪問。

lock和latch

在數據庫中,lock和Latch都稱爲鎖,但是兩者意義不同。

latch稱爲閂鎖(shuang suo),其要求鎖定的時間必須非常短。若持續的時間長,則應用的性能會非常差。在InnoDB存儲引擎中,latch又分爲mutex互斥鎖 和 rwLock讀寫鎖。其目的是爲了保證併發線程操作臨界資源的正確性。通常沒有死鎖的檢測機制。

lock的對象是事務,用來鎖定的是數據庫中的對象,如表、頁、行。並且一般lock的對象僅在事務commit或者rollback後進行釋放。有死鎖檢測機制。

006tNc79gy1g4cmob7jx6j31eu0fg475.jpg

通過show engine innodb mutex可以查看InnoDB存儲引擎的中latch,具體字段詳情如下表:

![image-20190624224824680]()

鎖的類型

有幾個索引,需要分別向索引加鎖。

共享鎖、排他鎖

InnoDB存儲引擎實現瞭如下兩種標準的行級鎖:

共享鎖(S Lock):允許事務讀一行數據

排他鎖(X Lock):允許事務刪除 或 更新一行數據

如果一個事務T1已經獲取了行r的共享鎖,那麼另外的事務T2可以立即獲得行r的共享鎖。因爲讀取並不會改變行的數據,所以可以多個事務同時獲取共享鎖,稱這種情況爲鎖兼容。但若有其他的事務T3想獲得行R的排他鎖,則其必須等待事務T1、T2釋放行r上面的共享鎖,稱這種情況爲鎖不兼容。下面顯示了共享鎖和排他鎖的兼容性:

image-20190624225847173

從表6-3可以看出X鎖與任何鎖都不兼容,而S鎖僅和S鎖兼容。S鎖和X鎖都是行鎖,兼容是指對同一行記錄鎖的兼容情況。

普通 select 語句默認不加鎖,而CUD操作默認加排他鎖。

記錄鎖

Record Lock,僅鎖定一行記錄(如共享鎖、排他鎖)

  • 記錄鎖總是會去鎖定索引記錄,如果表在建立的時候,沒有設置任何一個索引,那麼InnoDB會使用隱式的主鍵來進行鎖定。
  • 查詢條件的列是唯一索引的情況下,臨建鎖退化爲記錄鎖

間隙鎖

Gap Lock,鎖定一個範圍,但不包含記錄本身。

關閉間隙鎖的2種方式:

(1)將事務隔離級別變爲read committed

(2)將參數innodb_locks_unsafe_for_binlog設置爲1

在上述配置下,除了外鍵和唯一性檢查依然需要間隙鎖,其餘情況僅適用行鎖進行鎖定。

臨鍵鎖

Next-Key Lock,等於記錄鎖 + 臨鍵鎖,鎖定一個範圍,並且鎖定記錄本身。主要是阻止多個事務將記錄插入到同一個範圍內,從而避免幻讀。

假如一個索引有10、11、13、20這四個值,那麼該索引可能被鎖定的區間爲:

image-20190728200240969

若事務T1已經通過臨鍵鎖鎖定了如下範圍:
image-20190728200422526

當插入新的記錄12時,則鎖定的範圍變成:

image-20190728200453193

當查詢的索引是唯一索引的時候,InnoDB會將臨鍵鎖優化成記錄鎖,從而提高併發。這時候,將不再由間隙鎖避免幻讀的問題,但是試驗了下,即使優化成記錄鎖,也不會有幻讀的問題,其實是因爲MVCC,在可重複讀的情況下,SELECT操作只會查找行版本號小於當前事務版本號的記錄,其他事務(事務開啓時間比當前事務晚)新插入的記錄版本號不滿足條件,就不會查出來。

對於輔助索引,當執行類似select * from z where b = 3 for update;加鎖語句時,會加上臨鍵鎖,並且下一個鍵值的範圍也會加上間隙鎖。

值得注意的是,對於唯一鍵值的鎖定,由臨鍵鎖優化爲記錄鎖,僅存在於查詢所有的唯一索引。若唯一索引由多列組成,而查詢僅是查找多個唯一索引中的一個,那麼查詢其實是range類型查詢,而不是point類型查詢,故InnoDB存儲引擎還是繼續使用臨鍵鎖。

image-20190803232019608

image-20190803231957959

在InnoDB存儲引擎中,通過使用臨鍵鎖來避免不可重複讀的問題(即幻讀)。在使用臨鍵鎖的情況下,對於索引的掃描,不僅僅鎖住掃描的到索引,而且還鎖住這些索引覆蓋的範圍。因此,在這些範圍內插入都是不允許的。這樣子就避免了其他事務在這些範圍內插入數據導致不可重複讀的問題。

意向鎖

概念:未來的某個時刻,事務可能要加共享/排它鎖了,先提前聲明一個意向

意向鎖有這樣一些特點:

(1)意向鎖是表級別的鎖

(2)意向鎖分爲:

  • 意向共享鎖(intention shared lock, IS),它預示着,事務有意向對錶中的某些行加共享S鎖
  • 意向排它鎖(intention exclusive lock, IX),它預示着,事務有意向對錶中的某些行加排它X鎖

(3)意向鎖協議:

  • 事務要獲得某些行的共享鎖,必須先獲得的意向共享鎖IS
  • 事務要獲取某些行的排他鎖,必須先獲得的意向排他鎖IX

(4)由於意向鎖僅僅表明意向,它其實是比較弱的鎖,意向鎖之間並不相互互斥,而是可以並行,其兼容互斥表如下:

​ IS IX

IS 兼容 兼容

IX 兼容 兼容

(5)既然意向鎖之間都相互兼容,那其意義在哪裏呢?它會與共享鎖/排它鎖互斥,其兼容互斥表如下:

​ S X

IS 兼容 互斥

IX 互斥 互斥

(排它鎖是很強的鎖,不與其他類型的鎖兼容。這也很好理解,修改和刪除某一行的時候,必須獲得強鎖,禁止這一行上的其他併發,以保障數據的一致性。)

InnoDB支持多粒度鎖定,這種鎖定允許事務在行級上的鎖和表級上的鎖同時存在。爲了支持不同粒度上進行加鎖操作,InnoDB存儲引擎支持一種額外的鎖方式,稱之爲意向鎖。意向鎖是將鎖定的對象分爲多個層次,意向鎖意味着事務希望在更細的粒度上進行加鎖。如圖6-3所示:

006tNc79gy1g4cndxw7kpj31690u07ou.jpg

若將上鎖的對象看成一棵樹,那麼對最下層的對象上鎖,也就是對最細粒度的對象上鎖,那麼首先需要對粗粒度的對象進行上鎖。如上圖,如果需要對頁上的記錄上X鎖,那麼需要分別對數據庫A、表、頁 上意向鎖IX,最後對記錄r上排他鎖X。

若其中任何一部分導致等待,那麼該操作需要等待粗粒度鎖的完成。舉例來說,事務T1在對記錄r加X鎖之前,已有事務T2對錶1進行了S表鎖,那麼表1上面已經存在S鎖,之後事務T1試圖在表1上加IX鎖(獲取記錄r的X鎖必須先獲取表1的IX鎖),由於不兼容,所以事務T1需要等待事務T2釋放表鎖。

![image-20190624233058036]()

插入意向鎖

對已有數據行的修改與刪除,必須加強互斥鎖X鎖,那對於數據的插入,是否還需要加這麼強的鎖,來實施互斥呢?插入意向鎖,孕育而生。

插入意向鎖,是間隙鎖(Gap Locks)的一種(所以,也是實施在索引上的),它是專門針對insert操作的。

它的用處是:多個事務,在同一個索引,同一個範圍區間插入記錄時,如果插入的位置不衝突,不會阻塞彼此。

示例

在MySQL,InnoDB,RR下:

t(id unique PK, name);

 

數據表中有數據:

10, shenjian

20, zhangsan

30, lisi

 

事務A先執行,在10與20兩條記錄中插入了一行,還未提交:

insert into t values(11, xxx);

 

事務B後執行,也在10與20兩條記錄中插入了一行:

insert into t values(12, ooo);

 

(1)會使用什麼鎖?

(2)事務B會不會被阻塞呢?

 

回答:雖然事務隔離級別是RR,雖然是同一個索引,雖然是同一個區間,但插入的記錄並不衝突,故這裏:

使用的是插入意向鎖

並不會阻塞事務B

自增鎖

自增鎖是MySQL一種特殊的鎖,如果表中存在自增字段,MySQL便會自動維護一個自增鎖。

在InnoDB存儲引擎的內存結構中,對每個含有自增長值的表都有一個自增長計數器。當對含有自增長計數器的表進行插入操作時,這個這個計數器會被初始化,執行如下操作來得到計數器的值:

select max(auto_inc_col) from t for update

插入操作會依據這個自增長的計數器值加1賦予自增長列。這個實現方式成爲Auto-Inc Locking。這種鎖其實是採用一種表鎖的機制,爲了提高插入的性能,鎖不是在一個事務完成以後才釋放,而是在完成對自增長值插入的SQL語句後立即釋放。

雖然Auto-Inc Locking從一定程度上提高了併發插入的效率,但還是存在一些性能上的問題。對於有自增長值的列的併發插入性能較差,事務必須等待前一個插入的完成(雖然不用等待事務的完成)。

從MySQL5.12版本開始,InnoDB存儲引擎提供了一種輕量級互斥量的自增長實現方式。這種方式大大提高了自增長值插入的性能。並且從該版本開始,InnoDB存儲引起提供了一個參數innodb_innodb_autoinc_lock_mode來控制自增長模式,該參數的默認值爲1。首先看下自增長的插入分類,如下圖:

image-20190722221544377

下圖展示了innodb_innodb_autoinc_lock_mode的不同值對自增的影響:(值爲1、2的時候,看不懂。。

image-20190722221852110

InnoDB存儲引擎中自增長的實現和MyISAM不同。MyISAM存儲引擎是表鎖設計,自增長不用考慮併發插入的問題在InnoDB存儲引擎中,自增長值的列必須是索引,同時必須是索引的第一個列,如果不是第一個列,則MySQL會拋出異常。MyISAM存儲引擎沒有這個問題。

參考:http://blog.itpub.net/15498/v...

外鍵與鎖

如果沒有爲外鍵顯示添加索引,InnoDB自動爲外鍵創建索引,這樣子避免表鎖。

對於外鍵值的插入或更新,首先需要查詢父表中的記錄,即select父表。但是不是使用一致性非鎖定讀,因爲這樣子會發生數據不一致的問題。因此這時使用的是select…lock in share mode,即主動對父表加一個共享鎖。如果這時父表已經加了X鎖,子表上面的操作將會被阻塞,如下圖:

image-20190722230035134

image-20190722230121265

MVCC多版本

又稱爲一致性非鎖定讀。指InnoDB通過行多版本控制的方式來讀取當前執行時間數據庫中行的數據。如果讀取的行正在執行delete或者update操作,這時讀操作不會因此去等待行上鎖的釋放。相反的,InnoDB存儲引擎會去讀取行的一個快照數據。

在默認配置下,即事務的隔離界別爲REPEATABLE READ(可重複讀)模式下,InnoDB存儲引擎的SELECT操作使用一致性非鎖定讀。

快照數據是指該行的之前版本的數據,該實現是通過undo段來完成。而undo用來在事務中回滾數據,因此快照數據本身是沒有額外的開銷。此外讀取快照數據是不需要上鎖的,因爲沒有事務需要對歷史的數據進行修改操作。

非鎖定度機制極大的提高了數據庫的併發性。這是InnoDB默認的讀取方式,即讀取不會佔用表上的鎖。但是在不同事務隔離界別下,讀取的方式不同,並不是在每個事務隔離界別下都是採用非鎖定的一致性讀。此外,即使都是使用非鎖定的一致性讀,但是對於快照數據的定義也是各不相同。

快照數據其實就是當前行數據之前的歷史版本,每行記錄可能有多個版本。一個行記錄可能有不止一個快照數據,一般稱這種技術爲行多版本技術,由此帶來的併發控制,稱之爲多版本併發控制 MVCC。

在事務隔離界別read committed 和 repeatable read(InnoDB默認的事務隔離界別)下,InnoDB使用非鎖定一致性讀。然而,對於快照數據的定義卻不相同。對於快照數據,非一致性讀總是讀取被鎖定行的最新一份快照數據(如果沒有被鎖定,則讀取行的最新數據;如果行鎖定了,則讀取該行的最新一個快照)。而在repeatable read事務隔離級別下,對於快照數據,非一致性讀總是讀取事務開始時的行數據版本。

MVCC的優缺點

MVCC在大多數情況下代替了行鎖,實現了對讀的非阻塞,讀不加鎖,讀寫不衝突。缺點是每行記錄都需要額外的存儲空間,需要做更多的行維護和檢查工作。注意寫寫不能並行

MVCC的實現原理

undo log

undo log是爲回滾而用,具體內容就是copy事務前的數據庫內容(行)到undo buffer,在適合的時間把undo buffer中的內容刷新到磁盤。undo buffer與redo buffer一樣,也是環形緩衝,但當緩衝滿的時候,undo buffer中的內容會也會被刷新到磁盤;與redo log不同的是,磁盤上不存在單獨的undo log文件,所有的undo log均存放在主ibd數據文件中(表空間),即使客戶端設置了每表一個數據文件也是如此。在Innodb中,undo log被劃分爲多個段,具體某行的undo log就保存在某個段中,稱爲回滾段。可以認爲undo log和回滾段是同一意思

爲了便於理解MVCC的實現原理,這裏簡單介紹一下undo log的工作過程

在不考慮redo log 的情況下利用undo log工作的簡化過程爲:

序號    動作
1       開始事務
2         記錄數據行數據備份到undo log
3         更新數據
4         將undo log寫到磁盤
5         將數據寫到磁盤
6         提交事務

(1)爲了保證數據的持久性,數據要在事務提交之前持久化
(2)undo log的持久化必須在在數據持久化之前,這樣才能保證系統崩潰時,可以用undo log來回滾事務

(3)Innodb通過undo log保存了已更改行的舊版本的快照。

redo log

redo log就是保存執行的SQL語句到一個指定的Log文件,當MySQL執行recovery(修復)時重新執行redo log記錄的SQL操作即可。當客戶端執行每條SQL(更新語句)時,redo log會被首先寫入log buffer;當客戶端執行COMMIT命令時,log buffer中的內容會被視情況刷新到磁盤。redo log在磁盤上作爲一個獨立的文件存在,即Innodb的log文件。

Innodb中的隱藏列

InnoDB的內部實現中爲每一行數據增加了三個隱藏列用於實現MVCC。

列名 長度(字節) 作用
DB_TRX_ID 6 插入或更新行的最後一個事務的事務標識符。(刪除視爲更新,將其標記爲已刪除)
DB_ROLL_PTR 7 寫入回滾段的撤消日誌記錄(若行已更新,則撤消日誌記錄包含在更新行之前重建行內容所需的信息)
DB_ROW_ID 6 行標識(隱藏單調自增id)

一行記錄的結構如下:

數據列 .. DB_ROW_ID DB_TRX_ID DB_ROLL_PTR
MVCC工作過程

MVCC只在READ COMMITED 和 REPEATABLE READ 兩個隔離級別下工作。READ UNCOMMITTED總是讀取最新的數據行,而不是符合當前事務版本的數據行。而SERIALIZABLE 則會對所有讀取的行都加鎖。另外事務的版本號是遞增的。

SELECT

InnoDB 會根據兩個條件來檢查每行記錄:

  • InnoDB只查找版本(DB_TRX_ID)早於當前事務版本的數據行(行的系統版本號<=事務的系統版本號,這樣可以確保數據行要麼是在開始之前已經存在了,要麼是事務自身插入或修改過的)
  • 行的刪除版本號(DB_ROLL_PTR)要麼未定義(未更新過),要麼大於當前事務版本號(在當前事務開始之後更新的)。這樣可以確保事務讀取到的行,在事務開始之前未被刪除。

INSERT

InnoDB爲新插入的每一行保存當前系統版本號作爲行版本號

DELETE

InnoDB爲刪除的每一行保存當前的系統版本號作爲行刪除標識

UPDATE

innodb爲插入一行新紀錄,保存當前系統版本號作爲行版本號,同時保存當前系統版本號到原來的行作爲行刪除標示。

MVCC插入示例

image-20190803133625261

F1~F6是字段名稱,1~6是對應的數據。後面3個隱藏字段分別對應行ID、事務ID、回滾指針。

初始狀態

假如有一條新增的數據,可以認爲行ID爲1,其他兩個字段爲空。

事務1更改該行的值

image-20190803134019783

當事務1更改該行的值時,會進行如下操作:

  • 用排他鎖鎖定該行
  • 記錄redo log
  • 把該行修改前的值複製到undo log,即上圖中下面的行
  • 修改當前的行的值,填寫事務編號,使回滾指針指向undo log中修改的行
  • 釋放鎖

事務2更改該行的值

image-20190803134329790

與事務1相同,此時undo log中有2條記錄,並且通過回滾指針連在一起。

因此,如果undo log一直不刪除,則可以通過當前記錄的回滾指針回溯到該行創建時的初始內容,所幸的是在InnoDB中存在清理線程,它會查詢比現在最老的事務還早的undo log,並刪除它們,從而保證undo log文件不會無限增長。

事務提交

當事務正常提交時,InnoDB只需要更改事務狀態爲COMMIT即可,不需要做其他額外的工作,而回滾則複雜一點,需要根據回滾指針找出事務修改前的版本,並且恢復。如果事務影響的行非常多,回滾則可能變得效率不高。

一致性非鎖定讀(見共享鎖、排他鎖)

在某些情況下,用戶需要顯式的對數據庫讀取操作進行加鎖以保證數據邏輯的一致性。而這要求數據庫支持加鎖語句。InnoDB存儲引擎對於SELECT語句支持兩種一致性的鎖定度操作:

  • select … for update

    共享鎖(S鎖, share locks)。其他事務可以讀取數據,但不能對該數據進行修改,直到所有的共享鎖被釋放。

​ 如果事務對某行數據加上共享鎖之後,可進行讀寫操作;其他事務可以對該數據加共享鎖,但不能加排他鎖,且只能讀數據,不能修改數據。

  • select … lock in share mode

​ 如果事務對數據加上排他鎖之後,則其他事務不能對該數據加任何的鎖。獲取排他鎖的事務既能讀取數據,也能修改數據。

select…for update對讀取的行記錄加一個X鎖,其他事務不能對已鎖定的行加上任何鎖。select…lock in share mode對讀取的行記錄加一個S鎖,其他事務可以向被鎖定的行加S鎖,但是如果加X鎖,則會被阻塞。

對於一致性非鎖定讀,即時讀取的行已經被執行了select..for update,也是可以進行讀取的。

如果不加篩選條件(或者篩選條件不走索引),會升級爲表鎖

索引數據重複率太高會導致全表掃描:當表中索引字段數據重複率太高,則MySQL可能會忽略索引,進行全表掃描,此時使用表鎖。可使用 force index 強制使用索引。

參考:https://blog.csdn.net/u012099...

鎖問題

髒讀

髒數據:指的是事務對緩衝池中行記錄的修改,並且還沒有提交。即事務未提交的數據。

髒讀:指當前事務可以讀到其他事務的未提交的數據。如果讀到了髒數據,即一個事務可以讀到另外一個事務中未提交的數據,顯然違反了事務的隔離性。

髒讀的條件:需要事務的隔離級別爲讀未提交

示例image-20190804114458983

不可重複讀

不可重複讀:指在在一個事務內多次讀取同一個數據集合,在這個事務還沒有結束時,另外一個事務也訪問了同一個數據集合,並且做了一些DML操作。因此,在第一個事務的兩次讀數據之間,由於第二個事務的修改,第一個事務兩次讀取到的數據可能是不一樣的(具體看隔離級別)。這種稱爲不可重複讀

示例image-20190804114524677

丟失更新

丟失更新:指一個事務的更新操作被另外一個事務的更新操作所覆蓋,從而導致數據的不一致。

丟失更新的實例

image-20190804120327921

解決辦法:對用戶讀取的記錄加上一個排他鎖,這樣子其他事務就必須等待前一個事務的完成。從而避免併發問題。

解決辦法的示例

image-20190804120415922

阻塞

阻塞:事務因爲等待其他事務釋放鎖而等待

超時:等待其他事務釋放鎖,超過超時時間,就認爲是超時。

innodb_lock_wait_timeout:用來控制超時時間,默認是50秒。可以在MYSQL運行時進行設置。

innodb_rollback_on_timeout:用來設定是否在等待超時時對進行中的事務進行回滾操作。默認是OFF,不回滾。不可以在MySQL啓動時進行修改。用戶在超時的情況下,必須判斷是是否需要commit或者rollback,之後再進行下一步的操作。

死鎖

概念:死鎖是指兩個或者兩個以上的事務,因爭奪資源而造成的一種互相等待的現象。若無外力作用,所有事務都將無法推進下去。

解決數據庫死鎖最簡單的方法:設置超時時間。即當兩個事務互相等待時,當一個等待時間超過設置的閾值時,其中一個事務進行回滾,另外一個等待的事務就能繼續執行。

超時機制雖然簡單,但是其使用FIFO的方式來選擇超時回滾的事務,假如第一個超時的事務 更新了很多行,遠比第二個事務多,因此佔用了更多的undo log,這時FIFO的方式,就顯得不適用了,因爲第一個事務回滾時間明顯比第二個事務回滾時間長很多。

等待圖

因爲FIFO處理死鎖可能不適用,所以數據庫普遍採用了wait-for graph(等待圖)的方式來進行死鎖檢測。和超時機制比較,這是一種更爲主動的死鎖檢測方式,InnoDB也採用了這種方式。

等待圖要求數據庫保存以下兩種信息:

(1)鎖的信息鏈表(見圖6-5)

(2)事務等待鏈表(見圖6-5)

通過上述鏈表可以構造出一張圖,而在這個圖中存在迴路,則代表存在死鎖。在等待圖中,事務爲圖中的節點。在圖中,事務T1指向事務T2邊的定義爲:

(1)事務T1等待事務T2所佔用的資源

(2)事務之間在等待相同的資源,而事務T1在事務T2之後

image-20190804134158816

image-20190804134632126

發現死鎖後,InnoDB會馬上回滾一個事務。

鎖升級

概念:將當前鎖的粒度降低,比如說把行鎖升級爲表鎖,那樣子會導致併發性能降低。

InnoDB不是根據每個記錄來產生行鎖的,而是根據每個事務訪問的每個頁對鎖進行管理的,採用的是位圖的方式,因此不管一個事務鎖住頁中一條還是多條記錄,都是用一個鎖,其開銷通常是一致的。

image-20190804140229826

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章