天天看點

MySQL InnoDB存儲引擎之鎖

    概念:

        鎖是用來管理對共享檔案的并發通路。innodb會在行級别上對資料庫上鎖。不過innodb存儲引擎會在資料庫内部其他多個地方使用鎖,進而允許對不同資源提供并發通路。例如操作緩沖池中的LRU清單,删除,添加,移動LRU清單中的元素,為了保證一緻性,必須有鎖的介入。MyISAM引擎是表鎖,而InnoDB提供一緻性的非鎖定讀、行級鎖,且行級鎖沒有相關額外的開銷。

    鎖

        table-level locking(表級鎖)

            整個表被客戶鎖定。根據鎖定的類型,其他客戶不能向表中插入記錄,甚至從中讀資料也受到限制MyISAM、MEMORY預設鎖級别,個别時候,InnoDB也會更新為表級鎖

        row-level locking(行級鎖)

            隻有線程目前使用的行被鎖定,其他行對于其他線程都是可用的InnoDB預設行級鎖。是基于索引資料結構來實作的,而不是像ORACLE的鎖,是基于block的。InnoDB也會更新為表級鎖,全表/全索引更新,請求autoinc鎖等

        page-level locking(頁級鎖)

            鎖定表中某些行集合(稱做頁),被鎖定的行隻對鎖定最初的線程是可行。如果另外一個線程想要向這些行寫資料,它必須等到鎖被釋放。不過其他頁的行仍然可以使用BDB預設頁級鎖

    lock與latch

        latch稱為闩鎖(輕量級的鎖),因為其要求鎖定的時間必須非常短。若持續的時間長,則應用的性能會非常差。在InnoDB存儲引擎中,又可以分為mutex(互斥量)和rwlock(讀寫鎖)。其目的是用來保證并發線程操作臨界資源的正确性,并且通常沒有死鎖檢測的機制。latch可以通過指令show engine innodb mutex來進行檢視。如圖:

MySQL InnoDB存儲引擎之鎖

        由上圖可以看出列Type顯示的總是InnoDB,列Name顯示latch的資訊以及所在源碼的行數,列Status中顯示的os_waits表示作業系統等待的次數。

        lock的對象是事務,用來鎖定的是資料庫中的對象,如表、頁、行。并且一般lock的對象僅在事務commit或者rollback後釋放(不同僚務隔離級别釋放的時間可能不一樣)。有死鎖機制。二則的差別如下:

MySQL InnoDB存儲引擎之鎖

    特點:

    InnoDB是通過對索引上的索引項加鎖來實作行鎖。這種特點也就意味着,隻有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB将使用表鎖。

    鎖的類型:

        有兩種标準的行級鎖:

            共享鎖(S lock):允許一個事務去讀一行,阻止其他事務獲得相同資料集的排他鎖.SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

            排它鎖(X lock):允許獲得排他鎖的事務更新資料,阻止其他事務取得相同資料集的共享讀鎖和排他鎖.SELECT * FROM table_name WHERE ... FOR UPDATE

        InnoDB存儲引擎支援意向鎖且設計比較簡練,分為兩種内部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。(意向鎖是InnoDB自動加的)

            意向共享鎖(IS):事務打算給資料行加行共享鎖,事務在給一個資料行加共享鎖前必須先取得該表的IS鎖.

            意向獨占鎖(IX):事務打算給資料行加行排他鎖,事務在給一個資料行加排他鎖前必須先取得該表的IX鎖.

        表級意向鎖與行級鎖的相容情況如下圖:

MySQL InnoDB存儲引擎之鎖

    鎖的檢視

        在InnoDB1.0版本之前隻能通過show engine innodb status(transactions行中檢視) 或者 show full processlist來檢視目前庫中鎖的請求。但是在這之後在information_schema架構下新增innodb_trx、innodb_locks和innodb_lock_waits三張表記錄目前庫中鎖的情況。

        三個表的字段說明如下圖

MySQL InnoDB存儲引擎之鎖
MySQL InnoDB存儲引擎之鎖
MySQL InnoDB存儲引擎之鎖

    一緻性非鎖定讀(consistent nonlocking read)

        一緻性的非鎖定讀是指InnoDB存儲引擎通過行多版本控制(multi_versioning)的方式來讀取目前執行時間資料庫中行的資料。如果讀取的行正在執行delete或者update操作,這時讀取操作不會是以去等待行上鎖的釋放。相反地,InnoDB存儲引擎會去讀取行的一個快照資料(目前行資料的曆史版本)。快照資料是指該行的之前版本的資料,該實作是通過undo段來完成。而undo用來在事務復原資料,是以快照資料本身是沒有額外開銷。而且,讀取快照資料是不需要上鎖的。一緻性非鎖定讀是InnoDB存儲引擎的預設讀取方式(在讀取不會占用和等待表上的鎖)。但是在不同僚務隔離級别下,讀取的方式不同,并不是在每個事務隔離級别下都是采用非鎖定的一緻性讀。即使都是使用非鎖定的一緻性讀,但是對于快照資料的定義格式也各不相同。在事務隔離級别READ COMMITTED(RC)和REPEATABLE READ(RR,InnoDB存儲引擎的預設事務隔離級别)下,InnoDB存儲引擎使用非鎖定的一緻性讀。然而,對于快照資料的定義去不相同。在RC事務隔離級别下,對于快照資料,非一緻性讀總是讀取被鎖定行的最新一份快照資料。而在RR事務隔離解綁下,對于快照資料,非一緻性讀總是讀取事務開始時的行資料版本。

    一緻性鎖定讀

        有上文知道,預設的事務隔離級别(RR)模式下,InnoDB存儲引擎的select操作使用一緻性非鎖定讀。但是在某些情況下,使用者需要顯式地對資料庫讀操作加鎖以保證資料邏輯的一緻性。InnoDB存儲引擎對于select語句支援兩種一緻性的鎖定讀操作:

            select ... for update:對讀取的行記錄加X鎖,其他事物不能對該行加任何鎖。

            select ... lock in share mode:對讀取的行記錄加S鎖,其他事物可以對該行加S鎖,但是如果加X鎖,則會被阻塞。

    自增長與鎖

        在InnoDB存儲引擎的記憶體結構中,對每個含有自增長值的表都有一個自增長計數器(auto-increment counter)。當對含有自增長的計數器的表進行插入操作是,這個計數器會被初始化,執行如下的語句來得到計數器的值:select max(auto_inc_col) from t for update。插入操作會依據這個自增長的計數器值加1賦予自增長列。這個實作方式稱為AUTO-INC Locking,這是一種特殊的表鎖機制,為了提高插入的性能,鎖不是在一個事務完成之後才釋放,而是在完成對自增長值插入的SQL語句後會立即釋放。AUTO-INC Locking在一定程度上提高了并發插入的效率,但是還存在一些性能上的問題。首先,對于有自增長值的列的并發插入性能較差,事務必須等待前一個插入的完成(不用等待事務的完成)。其次,對于insert ... select的大資料量的插入會影響插入的性能,因為另一個事務中的插入會被阻塞。從MySQL5.1.22版本開始,InnoDB存儲引擎提供了一種輕量級互斥量的自增長實作機制,這種機制大大提高了自增長值插入的性能。通過參數innodb_autoinc_lock_mode來控制自增長的模式(預設為1)。自增長的插入進行分類如圖:

MySQL InnoDB存儲引擎之鎖

        innodb_autoinc_lock_mode的參數值及其對自增長的影響如下圖:

MySQL InnoDB存儲引擎之鎖

        MyISAM存儲引擎是表鎖,自增長不用考慮并發插入的問題。需要注意的是:在InnoDB存儲引擎中,自增長值的列必須是索引,同時必須是索引的第一個列,如果不是第一個列,MySQL是會抛出異常的。異常如圖

MySQL InnoDB存儲引擎之鎖

    外鍵與鎖

        外鍵主要用于完整性的限制檢查。在InnoDB存儲引擎中,對于一個外鍵列,如果沒有顯式地對這個列加索引,InnoDB存儲引擎會自動對其加一個索引,避免表鎖。對于外鍵值的插入或者更新,首先需要查詢父表中的記錄,對于父表的select操作,不是使用的一緻性非鎖定讀的方式,因為這樣會發生資料不一緻的問題,是以這時使用的是select ... lock in share mode方式,即主動給父表加一個S鎖。

    鎖的問題

        dirty read 髒讀

            髒讀就是讀取到髒資料(未送出的資料)。一個事務(A)讀取到另一個事務(B)中修改後但尚未送出的資料,并在這個資料的基礎上操作。這時,如果B事務復原,那麼A事務讀到的資料是無效的。不符合一緻性。如圖

MySQL InnoDB存儲引擎之鎖

            首先事務的隔離級别有預設的RR改為RU,由上述例子可以看出會話B中兩次select操作取得了不同的結果,并且這2條記錄是會話A中并未送出的資料,這就産生了髒讀。由此可以得出結論:髒讀發生的條件是事務的隔離級别為RU。

        unrepeatable read 不可重複讀

            事務(A)讀取到了另一個事務(B)已經送出的更改資料,不符合隔離性。不可重複讀和髒讀的差別是:髒讀是讀到未送出的資料,而不可重複讀則讀到的是已經送出的資料。首先将事務隔離級别調整為RC,然後操作下邊的例子:

MySQL InnoDB存儲引擎之鎖

        phantom read 幻讀

            事務(A)讀取到了另一個事務(B)送出的新增資料,不符合隔離性。

    鎖的範圍(鎖的算法):

        1.Record Lock :單個記錄上的鎖,總是會去鎖住索引記錄,如果InnoDB存儲引擎表在建立的時候沒有設定任何一個索引,那麼這時InnoDB存儲引擎會使用隐式的主鍵來進行鎖定。

        2.Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身。

        3.Next-key Lock: 鎖定一個範圍和本身 Record Lock + Gap Lock,防止幻讀。

        主鍵索引和唯一輔助索引 = record lock

        非唯一輔助索引 = next-key lock

    阻塞

        不同鎖之間的相容性關系,在有些時刻一個事務中的鎖需要等待另外一個事務中的鎖釋放它所占用的資源,這就是阻塞。阻塞并不是一件壞事,其是為了確定事務可以并發且正常地運作。在InnoDB存儲引擎中,參數innodb_lock_wait_timeout用來動态的控制等待的時間(預設50秒),innodb_rollback_on_timeout用來靜态的設定釋放在等待逾時時對進行的事務進行復原操作(預設OFF,代表不復原)。

    死鎖

        死鎖是指兩個或者兩個以上的事務在執行過程中,因争奪資源而造成的一種互相等待的現象。解決死鎖最簡單的一種方式是逾時,即當兩個事務互相等待是,當一個等待時間超過設定的某一閥值時,其中一個事務進行復原,另外一個等待的事務就能繼續進行。在InnoDB存儲引擎中,參數innodb_lock_wait_timeout用來設定逾時時間。但若逾時的事務所占權重比較大,如果事務操作更新了很多行,占用了較多的undo log,這時采用FIFO的方式就不合适啦,因為復原這個事務的時間相對于另一個事務所占用的時間可能會很多。是以,除了逾時機制,目前資料庫都普遍采用wait-for graph(等待圖)的方式來進行死鎖檢測。要求資料庫報錯以下兩種資訊:a.鎖的資訊連結清單;b.事務等待連結清單。通過上述連結清單可以構造一張圖,而在這個圖中若存在回路,就代表存在死鎖。在wait-for graph中,事務為圖中的節點。如圖:

MySQL InnoDB存儲引擎之鎖

        如圖可以發現存在回路(1,2),是以存在死鎖,這時InnoDB存儲引擎選擇復原undo量最小的事務。wait-for graph的死鎖檢測通常采用深度優先的算法實作。        

    注意:

        1.S X IS IX,表示的是,本鎖和其他鎖共存的方式,是互斥還是相容

        2.RECORD LOCK、GAP LOCK、NEXT-KEY LOCK,表示的是,這些鎖要加載的範圍,是行記錄本身,還是行記錄+間隙,甚至更大的範圍

    重要的結論:

        1、任何輔助索引上的鎖,或者非索引列上的鎖,最終都要回溯到主鍵上,在主鍵上也要加一把鎖

        2、任何葉子節點上的S或X鎖之前,都會在根節點加一個IS或IX鎖,也就是表級别的IS、IX鎖

        3、主鍵索引上的鎖,都是record lock

        4、唯一索引輔助索引上的鎖,也都是record lock

        5、非唯一索引輔助索引上的鎖,則是next-key lock

        6、不會有單獨的gap lock出現,隻會伴随着record lock出現,依附于它

繼續閱讀