踩坑攀登者:mysql/innodb的鎖、隔離與MVCC (上)

感受數據庫有不少說爛了的問題,在實際應用中總仍是容易出問題。正好就一個真實的踩坑場景來討論一下這個話題。一次踩坑每每是咱們理解一個問題的開始,這個想法是《踩坑攀登者》(pitfall climbers)系列文章的開始。java

爲了把握篇幅(在5000字之內)文章分紅上下兩篇,本文主要講兩種reading和innodb的各類鎖的設計。mysql

用戶投訴表

本文以這個table做爲例子來進行講解,注意例子中有1個彙集索引2個次級索引:redis

  • clustered-index:id做爲clustered index彙集索引,惟一;
  • secondary-index:user_id和user_name都是次級索引,not unique;

踩坑問題

一個鏈接但願修改用戶555的全部的數據而且不容許有其餘的新數據產生。sql

select * from tb_complaint_user where user_id = 555 for update; # 寫鎖
--- 修改用戶555的全部數據
commit;
複製代碼

結果大失所望,另外一個線程成功的insert了新的用戶555的數據,但另外一個線程的update 555的語句卻被block住了。數據庫

解決方案

經驗豐富的同窗立刻發現了問題來自隔離級別,查看了隔離級別發現級別REAT-COMMITED。(注意auto_commit都是false)因而嘗試在測試環境把隔離級別改爲REPEATABLE-READ,再次重試insert就被成功的block了。(你們注意不要在生產環境隨便修改配置- -)bash

這裏面整個過程到底發生了什麼呢?session

背景知識

閱讀鎖須要先了解innodb是基於transaction和索引的:1)在innodb中僅select也在一個transaction裏,不是隻有update這種語句纔會開啓transaction;2)innodb是基於索引的設計,行數據存在於彙集索引(clustered-index)的葉子節點上(leaf-records);多線程

另,若是沒有明確指定,例子裏的隔離級別都是read-committed,auto_commit = false;測試

兩種reads

首先先看看reads的一種分類方法,innodb的官方文檔根據reads是否須要請求數據庫鎖將其分爲locking reads和none-locking readsui

none-locking reads是不加鎖的查詢,也就是咱們最經常使用的select。在普通的select語句下,咱們的transaction不會申請任何鎖從而也不會被任何鎖block。

select * from tb_xxxx [where xxxx] [group by xxx] [having xxx] [order by xxx] 
複製代碼

那麼locking reads就很好理解了,須要請求數據庫鎖的就是locking reads了,請求鎖也就極可能須要等鎖被block。下面幾種reading語句都屬於locking reads。

select xxx from tb_xxx for share;
select xxx from tb_xxx for update;
update xxx set xxx = xxx where xxx = xxx; //因爲直接調用update/delete其實也會申請和for update同樣的鎖,
delete from xxx where xxx;
複製代碼

什麼是鎖

首先說說什麼是鎖,鎖的設計其實很是多不只僅是db會用到,redis、多線程開發中都會用到,並且業務開發的時候也經常用到鎖的設計:好比參加雙十一的商品不能再下架,這個時候淘寶系統就能夠對這個商品加一個業務上的商品鎖。

那麼鎖是什麼呢,從實現角度看極可能只是一個行tag,標記了鎖的類型、鎖的對象、鎖的擁有者等等。從含義理解鎖是一種用來限制某種資源的某一能力被使用的機制,這個角度進而帶給咱們幾個理解鎖的角度:鎖什麼資源、鎖限制了別人的什麼能力、鎖賦予了擁有者什麼能力、多個鎖是否共存。

innodb中鎖的分類

(1) data與metaData

定義與設計

innodb對鎖的第一種分類方式就是根據「鎖出現的原因」來分,對metaData好比table,procedure,db的定義而發生改動的鎖叫metaData lock,因爲對具體數據改動或者限制產生的鎖稱爲datalock。好比DDL語句就須要鎖住table定義從而申請的是table meta lock,而修改一個table中的一行申請的的鎖就是datalock。

datalock每每是基於transaction的,結束佔有鎖的trx就會釋放(rollback或者commit),固然和db的這個session直接結束了trx也會結束從而datalock釋放。

metadata lock每每不是基於transaction而是基於session的,因此metadata的lock沒法經過結束當前trx來結束,unlock須要顯示調用、或者等申請鎖的DDL執行完畢自動解鎖、再或者直接close session- -。

LOCK TABLES table_name [READ | WRITE]
UNLOCK TABLES; 
複製代碼

查看方式

查看innodb中正在使用的datalock和metalock都在performance_schema這個db中(mysql中schema和db屬於等價概念),使用下面語句就能夠查看當前正在使用中的locks了。固然直接查看innodb status也能看到鎖的具體信息。

select * from performance_schema.metadata_locks;
select * from performance_schema.data_locks;

show engine innodb status;

複製代碼

(2) X與S的

這一部分講的互斥鎖(X=exclusive)和共享鎖(S=shared)更多的是講互斥和共享的設計,而不是具體的一種數據庫鎖,這種共享和互斥關係針對具體的資源不管一個行記錄(record)仍是表(table)都適用。

S鎖正如其名shared能夠被多個trx(T1,T2,T3均可以得到),獲取S鎖表明該資源(record/table)正在被很」認真「的讀,限制的是在這個讀的過程當中不能發生修改刪除(dml)操做。若是java的GC回收認爲沒法回收的對象是被root節點鎖住了,那麼這種鎖就是一種shared鎖,多個roots能夠共享限制了回收能力。

X鎖exclusive也就是排它互斥的意思,同一時刻只能被一個trx來得到,下一個trx申請鎖每每會被block直至鎖被釋放,得到X鎖認爲該資源正在被一個trx修改從而不能被S鎖加鎖讀也不能被其餘線程加X鎖寫。java中的sync和lock都是一種排他鎖,限制了對象代碼的執行權限。

(3)行鎖: record, gap, next-key

根據行鎖鎖定的範圍,row lock這一個籠統的概念能夠再細粒分爲record lock,insert-lock, gap lock,next-key lock。next-key實際上是record和gap的組合,因此重點在於理解recard,insert和gap lock。

(3.1) record lock

設計

record lock應該具叫index record lock才更準確。這種鎖的目標就是命中sql條件的一個個index records, 這裏的index最重要的是執行計劃使用到的key,此外也會在pk上加記錄鎖。因此一個命中記錄極可能會加多個記錄鎖

另外一個經常誤解的知識點事index record的粒度是「一個個」。什麼叫一個呢,若是使用的key不是unique key,這一行的定位應該是(key=xxx,pk=xxx)(也就是次級索引葉子節點裏的內容:key+pk)。這裏不能籠統的理解爲對(key=xxx)加了鎖由於key=xxx是一個多行表明的是從xxx到xxx+1的這個範圍。

記錄鎖在innodb系統中的標識LockMode注意不是X,S而是(X/S,LOCK_REC_NOT_GAP),也就是lock is record not gap的意思

案例分析

下面咱們看看例子理解一下上面的設定,如今咱們user_complaint表上有2行數據:

咱們經過userid和名字來select,explain sql發現執行計劃使用userid做爲索引。查看data_locks表裏的recordMode和indexName,發現如同上文所說的innodb在pk和userid上使用了recordLock。查看lockData這一列的數據發現user_id這個次級索引上的record(userId=555, id=1)和pk這個unique的彙集索引上的記錄是(id=1)。

explain select * from tb_user_complaint where user_id = 555 and user_name = "macavity" for update;
select * from tb_user_complaint where user_id = 555 and user_name = "macavity" for update;
select * from performance_schema.data_locks;
rollback;
複製代碼

若是把select中的userid條件刪掉會發現recordlock加載了pk和userName這兩個index上,由於這個時候執行計劃使用的key就是userName了; 若是把select的條件改爲id=1那麼執行計劃用的index就是pk也就只會有pk這一個index lock。

X/S record locks

當record與X和S的機制結合在一塊兒時,就是咱們最常說的行同享排它鎖。能夠獲得瞭如下互斥圖。注意,這個互斥圖中的X與S是指同一行的行鎖,不要理解成其餘行更不要表的x與s和行的x與s混淆在這張圖裏

(3.2) insert intention lock 行插入意向鎖

DML中insert操做和update、delete不太相同,udpate和delete是對已經有的數據(index records)進行操做,而insert操做(update更改index的操做實際上是另外一種update+insert)是在間隙中新增index records。因此insert操做涉及到的鎖再也不是record lock而是insert intention lock

因爲insert的行還未存在(命中unique key duplicate error的除外),insert鎖是沒法被record鎖排斥的。這也就是案例中的問題。爲了防止insert鎖,innodb設計了一種轉麼針對不存在鎖

(3.3) gap lock

設計

gaplock,顧名思義它再也不鎖定一個個的index record,而是鎖定一個records之間的空隙。間隙鎖的lockmode爲(X/S,Gap)。這裏必定注意間隙鎖表示的是空隙,並不表示構成間隙的邊界(前提是邊界上本身沒有加行鎖),因此gap lock不會與表示實體行的record lock互斥而會對一樣限制間隙的insert lock互斥

innodb只有在isolation>=repeatble-read(簡稱RR)的隔離級別纔會採用間隙鎖,因此咱們這裏須要把修改隔離級別。 隔離級別和鎖的關係會在下一篇詳細介紹,本篇的目的是理解鎖。

案例

咱們再次執行下面sql。

set @@session.transaction_isolation='repeatable-read';
commit;
select * from tb_user_complaint where user_id = 555 for update;
複製代碼

當隔離級別變成RR以後咱們再次執行加鎖語句,發現多了一行gaplock。gaplock標記着gap的結束點在(userId=580,id=36)的位置,也就是(555,16)以後的第一個record。這個時候(555, 16)到(580, 36)這個區間都沒法再insert任何數據。

+------+----------------------------+---------+------------------------+------------+----------------------------+-----------+
| id   | created_at                 | user_id | contents               | is_archive | last_updated_at            | user_name |
+------+----------------------------+---------+------------------------+------------+----------------------------+-----------+
| 1    | 2020-02-12 15:12:11.922491 | 555     | complaint-test-1       | ^@          | 2020-02-12 20:12:44.989804 | macavity  |
| 2    | 2020-02-12 15:12:19.214543 | 222     | complaint-test-1       | ^@          | 2020-02-12 15:12:19.214543 | macavity  |
| 10   | 2020-02-12 15:26:50.164283 | 111     | complaint-test-1       | ^@          | 2020-02-12 15:26:50.164283 | macavity  |
| 16   | 2020-02-12 15:42:58.989849 | 555     | complaint-typed-by Bob | ^@          | 2020-02-12 15:42:58.989849 | macavity  |
| 36   | 2020-02-12 18:03:55.962341 | 580     | complaint-test-1       | ^@          | 2020-02-12 18:03:55.962341 | macavity  |
| 1012 | 2020-02-12 20:52:51.732739 | 600     | complaint-test-1       | ^@          | 2020-02-12 20:52:51.732739 | kiki      |
+------+----------------------------+---------+------------------------+------------+----------------------------+-----------+
複製代碼

這裏須要注意的是(X,Gap)表明間隙鎖並不表明記錄鎖,也就是(580,36)這行數據並無被行鎖鎖定。select for update這行仍是能夠得到這行的記錄鎖也就是(X, REC_NOT_GAP)鎖。

gap與insert lock的互斥與同享

下面咱們看一下間隙鎖和insert lock的同享互斥關係:

  1. Gap之間相互compatible,不一樣的trx能夠擁有相重疊區間的間隙鎖;(gap鎖裏的x和s更多的是標識是什麼語句致使的間隙鎖)
  2. 先加gap鎖能夠阻礙區間內的inserts
  3. 先insert還沒有commit:也不會致使gap鎖block,可是因爲insert行已經存在其會影響gap鎖的區間

(3.4) next-key lock

在上文間隙鎖的案例裏,當咱們改變隔離級別,gap lock中除了紅色的X,GAP以外的變動,其實綠色的部分也隨着隔離級別發生了變動。以前這兩行的記錄是X,REC_NOT_GAP,在這裏變成了X。

X/S是下一鍵鎖的lockmode,表示一個record-lock R和一個gap-lock (R-1, R)。這個命名實在是太容易引發迷惑了。, 舉個例子來講,被標記X的(555,1)其實表明了兩個鎖:

  • (555,1)X,rec_not_gap
  • ((222, 2),(555,1)) X, gap 也就是555和以前一個record之間的間隙寫鎖

(3.5) row lock總結

在講table鎖以前總結一下講過的行鎖,請參考下圖:

select * from tb_user_complaints where user_id = 555 for update;
複製代碼

(4) 表鎖:IX與IS,X與S

相對行鎖,表鎖就沒有那麼經常遇到了。

(4.1) 表意向鎖

在row record上加鎖時,table schema可能會被直接修改掉,或者被drop掉。爲了防止這種table級別的操做,innodb設計了意向鎖(intention lock)來幫助快速確認一個table內部是否是有transaction正在進行鎖操做或者其餘的DML操做。總結一下 意向鎖是一種表級鎖,lockType是table,lockMode爲IX/IS;但意向鎖用意是表內有行操做因此要對錶的操做作出限制,因此意向鎖更多的是一種datalock並不算是真正的metalock。 對行的S鎖操做觸發的意向鎖爲IS鎖,對行的寫行爲(update,insert相關)觸發的意向鎖爲IX鎖。

因爲IX和IS並不會致使table自己被鎖定因此IX和IS都是一種共享鎖。在metaLock的角度,IX是shared write鎖,IS是shared read鎖。

當咱們執行select * from tb_user_complaint where user_id = 555 for share;時會發現datalocks和metadata_locks表中會出現意向鎖的記錄:

(4.2) 表的讀寫鎖

表的另外一種鎖就是和行鎖對應的X鎖與S鎖,table級別的x鎖與s鎖和row級別的x鎖和s鎖至關相似,只是它鎖的對象是table相關的元數據,從而這種鎖是一種真正的metalock。 當咱們進行DDL時,好比rename,alter或者drop首先要得到metalock的寫鎖。若是要顯示的得到和釋放table X/S lock須要執行下面語句。

LOCK TABLES table_name [READ | WRITE]
UNLOCK TABLES; 
複製代碼

(4.3) 表鎖的同享互斥關係

關於table對象上的表也能夠經過一個表格來表示互斥共享關係:

  • X鎖必定與全部鎖互斥,包括本身

這個很好理解,由於X鎖每每表明要對lock object作修改,那麼lock object自己的其餘修改就要排隊,object內部數據的鎖定也要排隊。因此X鎖與全部意向鎖(表明table內部數據)也都互斥。

  • S鎖與X,IX互斥,與S,IS兼容

S與S類的鎖(S,IS)兼容這個很好理解,你們都是鎖定讀不發生寫事件,互不干擾;S鎖與X鎖互斥也很好理解,這個機制和row的x與s互斥同樣。可是筆者以前一直不能理解S鎖爲何要與IX鎖互斥呢,鎖定table的meta data不能變更和改動table內部data的IX鎖有什麼關係呢?其實若是跳出meta這個侷限把S鎖理解成對table一個總體加的鎖就很好理解了,S鎖鎖住了整個table,table的一切都只能讀不能寫不管是meta仍是data- -!

  • I鎖和I鎖之間兼容

因爲I鎖表明的是行數據的鎖狀況,因此I鎖之間不會發生互斥問題。就算是由於I鎖對應的是同一行,互斥問題也由行鎖來處理,畢竟I鎖是表級所已經再也不負責具體的行信息了。

  • IX與表的XS都互斥,與IXIS兼容

意向鎖的兼容剛剛講了,IX與XS互斥若是理解了XS就很好理解了,X和S都凍結了整個表的更改固然不能容忍標識行更改的IX存在了。

  • IS只與X互斥

IS與S不互斥由於都是讀互不影響,IS與IXIS不互斥由於都是標識的data,IS與X互斥因爲X每每表明要對schema發生變動而schema變動每每會致使一行發生變動與S鎖的目的相違背因此不能共存。

下集預告

下一篇主要講一下幾個問題:4種隔離級別,不一致性的幾種狀況(髒讀幻讀和不可重複讀),在不一樣隔離級別下none-locking reading是如何作到consistent的,在不一樣的隔離級別下locking reads又是怎麼加鎖解決不一致問題的。

但願你們以爲有所幫助的不吝點贊哦,你們的支持是我繼續整理案例的動力!

相關文章
相關標籤/搜索