鎖,在現(xiàn)實(shí)生活中是為我們想要隱藏于外界所使用的一種工具。在計(jì)算機(jī)中,是協(xié)調(diào)多個(gè)進(jìn)程或縣城并發(fā)訪問某一資源的一種機(jī)制。在數(shù)據(jù)庫當(dāng)中,除了傳統(tǒng)的計(jì)算資源(cpu、ram、i/o等等)的爭用之外,數(shù)據(jù)也是一種供許多用戶共享訪問的資源。如何保證數(shù)據(jù)并發(fā)訪問的一致性、有效性,是所有數(shù)據(jù)庫必須解決的一個(gè)問題,鎖的沖突也是影響數(shù)據(jù)庫并發(fā)訪問性能的一個(gè)重要因素。從這一角度來說,鎖對于數(shù)據(jù)庫而言就顯得尤為重要。
1、mysql中的鎖
MySQL中有著Lock和Latch的概念,在數(shù)據(jù)庫中,這兩者都可以被稱為“鎖”,但是兩者有著截然不同的含義。
Latch一般稱為閂鎖(輕量級的鎖),因?yàn)槠湟箧i定的時(shí)間必須非常短。若持續(xù)的時(shí)間長,則應(yīng)用的性能會非常差,在InnoDB引擎中,Latch又可以分為mutex(互斥量)和rwlock(讀寫鎖)。其目的是用來保證并發(fā)線程操作臨界資源的正確性,并且通常沒有死鎖檢測的機(jī)制。
Lock的對象是事務(wù),用來鎖定的是數(shù)據(jù)庫中的對象,如表、頁、行。并且一般lock的對象僅在事務(wù)commit或rollback后進(jìn)行釋放(不同事務(wù)隔離級別釋放的時(shí)間可能不同)。
關(guān)于Latch更詳細(xì)的講解可以參考:關(guān)于MySQL latch爭用深入分析與判斷,本文主要關(guān)注的是Lock鎖。
鎖的類型
對數(shù)據(jù)的操作其實(shí)只有兩種,也就是讀和寫,而數(shù)據(jù)庫在實(shí)現(xiàn)鎖時(shí),也會對這兩種操作使用不同的鎖;InnoDB 實(shí)現(xiàn)了標(biāo)準(zhǔn)的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock)。
-
共享鎖(讀鎖),允許事務(wù)讀一行數(shù)據(jù)。
-
排他鎖(寫鎖),允許事務(wù)刪除或更新一行數(shù)據(jù)。
而它們的名字也暗示著各自的另外一個(gè)特性,共享鎖之間是兼容的,而互斥鎖與其他任意鎖都不兼容:
稍微對它們的使用進(jìn)行思考就能想明白它們?yōu)槭裁匆@么設(shè)計(jì),因?yàn)楣蚕礞i代表了讀操作、互斥鎖代表了寫操作,所以我們可以在數(shù)據(jù)庫中并行讀,但是只能串行寫,只有這樣才能保證不會發(fā)生線程競爭,實(shí)現(xiàn)線程安全。
鎖的粒度
Lock鎖根據(jù)粒度主要分為表鎖、頁鎖和行鎖。不同的存儲引擎擁有的鎖粒度都不同。
表鎖
表級別的鎖定是MySQL各存儲引擎中最大顆粒度的鎖定機(jī)制。該鎖定機(jī)制最大的特點(diǎn)是實(shí)現(xiàn)邏輯非常簡單,帶來的系統(tǒng)負(fù)面影響最小。所以獲取鎖和釋放鎖的速度很快。由于表級鎖一次會將整個(gè)表鎖定,所以可以很好的避免困擾我們的死鎖問題。
當(dāng)然,鎖定顆粒度大所帶來最大的負(fù)面影響就是出現(xiàn)鎖定資源爭用的概率也會最高,致使并發(fā)度大打折扣。
使用表級鎖定的主要是MyISAM,MEMORY,CSV等一些非事務(wù)性存儲引擎。
表鎖的語法很簡單:
#?獲取表鎖 LOCK?TABLES ????tbl_name?[[AS]?alias]?lock_type ????[,?tbl_name?[[AS]?alias]?lock_type]?... ? lock_type: ????READ?[LOCAL] ??|?[LOW_PRIORITY]?WRITE ? #?釋放表鎖 UNLOCK?TABLES
MyISAM在執(zhí)行查詢前,會自動執(zhí)行表的加鎖、解鎖操作,一般情況下不需要用戶手動加、解鎖,但是有的時(shí)候也需要顯示加鎖。比如:檢索某一個(gè)時(shí)刻t1,t2表中數(shù)據(jù)數(shù)量。
LOCK?TABLE?t1?read,?t2?read; select?count(t1.id1)?as?'sum'?from?t1; select?count(t2.id1)?as?'sum'?from?t2; UNLOCK?TABLES;
頁鎖
頁級鎖定是MySQL中比較獨(dú)特的一種鎖定級別,在其他數(shù)據(jù)庫管理軟件中也并不是太常見。頁級鎖定的特點(diǎn)是鎖定顆粒度介于行級鎖定與表級鎖之間,所以獲取鎖定所需要的資源開銷,以及所能提供的并發(fā)處理能力也同樣是介于上面二者之間。另外,頁級鎖定和行級鎖定一樣,會發(fā)生死鎖。
在數(shù)據(jù)庫實(shí)現(xiàn)資源鎖定的過程中,隨著鎖定資源顆粒度的減小,鎖定相同數(shù)據(jù)量的數(shù)據(jù)所需要消耗的內(nèi)存數(shù)量是越來越多的,實(shí)現(xiàn)算法也會越來越復(fù)雜。不過,隨著鎖定資源顆粒度的減小,應(yīng)用程序的訪問請求遇到鎖等待的可能性也會隨之降低,系統(tǒng)整體并發(fā)度也隨之提升。
使用頁級鎖定的主要是BerkeleyDB存儲引擎。
行鎖
行級鎖定最大的特點(diǎn)就是鎖定對象的粒度很小,也是目前各大數(shù)據(jù)庫管理軟件所實(shí)現(xiàn)的鎖定顆粒度最小的。由于鎖定顆粒度很小,所以發(fā)生鎖定資源爭用的概率也最小,能夠給予應(yīng)用程序盡可能大的并發(fā)處理能力而提高一些需要高并發(fā)應(yīng)用系統(tǒng)的整體性能。
雖然能夠在并發(fā)處理能力上面有較大的優(yōu)勢,但是行級鎖定也因此帶來了不少弊端。由于鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發(fā)生死鎖。
使用行級鎖定的主要是InnoDB存儲引擎。
總結(jié)
-
表級鎖:開銷小,加鎖快;不會出現(xiàn)死鎖;鎖定粒度大,發(fā)生鎖沖突的概率最高,并發(fā)度最低。
-
行級鎖:開銷大,加鎖慢;會出現(xiàn)死鎖;鎖定粒度最小,發(fā)生鎖沖突的概率最低,并發(fā)度也最高。
-
頁面鎖:開銷和加鎖時(shí)間界于表鎖和行鎖之間;會出現(xiàn)死鎖;鎖定粒度界于表鎖和行鎖之間,并發(fā)度一般。
從鎖的角度來說,表級鎖更適合于以查詢?yōu)橹?,只有少量按索引條件更新數(shù)據(jù)的應(yīng)用,如Web應(yīng)用;而行級鎖則更適合于有大量按索引條件并發(fā)更新少量不同數(shù)據(jù),同時(shí)又有并發(fā)查詢的應(yīng)用,如一些在線事務(wù)處理(OLTP)系統(tǒng)。
2、InnoDB中的鎖
意向鎖
上節(jié)提到InnoDB 支持多種粒度的鎖,也就是行鎖和表鎖。為了支持多粒度鎖定,InnoDB 存儲引擎引入了意向鎖(Intention Lock)。
那什么是意向鎖呢?我們在這里可以舉一個(gè)例子:如果沒有意向鎖,當(dāng)已經(jīng)有人使用行鎖對表中的某一行進(jìn)行修改時(shí),如果另外一個(gè)請求要對全表進(jìn)行修改,那么就需要對所有的行是否被鎖定進(jìn)行掃描,在這種情況下,效率是非常低的;不過,在引入意向鎖之后,當(dāng)有人使用行鎖對表中的某一行進(jìn)行修改之前,會先為表添加意向互斥鎖(IX),再為行記錄添加互斥鎖(X),在這時(shí)如果有人嘗試對全表進(jìn)行修改就不需要判斷表中的每一行數(shù)據(jù)是否被加鎖了,只需要通過等待意向互斥鎖被釋放就可以了。
與上一節(jié)中提到的兩種鎖的種類相似的是,意向鎖也分為兩種:
-
意向共享鎖(IS):事務(wù)想要在獲得表中某些記錄的共享鎖,需要在表上先加意向共享鎖。
-
意向互斥鎖(IX):事務(wù)想要在獲得表中某些記錄的互斥鎖,需要在表上先加意向互斥鎖。
隨著意向鎖的加入,鎖類型之間的兼容矩陣也變得愈加復(fù)雜:
意向鎖其實(shí)不會阻塞全表掃描之外的任何請求,它們的主要目的是為了表示是否有人請求鎖定表中的某一行數(shù)據(jù)。
行鎖的算法
InnoDB存儲引擎有3種行鎖的算法,其分別是:
-
Record Lock:單個(gè)行記錄上的鎖。
-
Gap Lock:間隙鎖,鎖定一個(gè)范圍,但不包含記錄本身。
-
Next-Key Lock:Gap Lock+Record Lock,鎖定一個(gè)范圍,并且鎖定記錄本身。
Record Lock總是會去鎖住索引記錄,如果InnoDB存儲引擎表在建立的時(shí)候沒有設(shè)置任何一個(gè)索引,那么這時(shí)InnoDB存儲引擎會使用隱式的主鍵來進(jìn)行鎖定。
Next-Key Lock是結(jié)合了Gap Lock和Record Lock的一種鎖定算法,在Next-Key Lock算法下,InnoDB對于行的查詢都是采用這種鎖定算法。例如有一個(gè)索引有10,11,13和20這4個(gè)值,那么該索引可能被Next-Key Locking的區(qū)間為:
除了Next-Key Locking,還有Previous-Key Locking技術(shù)。同樣上述的值,使用Previous-Key Locking技術(shù),那么可鎖定的區(qū)間為:
但是不是所有索引都會加上Next-key Lock的,在查詢的列是唯一索引(包含主鍵索引)的情況下,Next-key Lock會降級為Record Lock。
接下來,我們來通過一個(gè)例子解釋一下。
CREATE?TABLE?z?( ????a?INT, ????b?INT, ????PRIMARY?KEY(a),????//?a是主鍵索引 ????KEY(b)????//?b是普通索引 ); INSERT?INTO?z?select?1,?1; INSERT?INTO?z?select?3,?1; INSERT?INTO?z?select?5,?3; INSERT?INTO?z?select?7,?6; INSERT?INTO?z?select?10,?8;
這時(shí)候在會話A中執(zhí)行 SELECT * FROM z WHERE b = 3 FOR UPDATE ,索引鎖定如下:
這時(shí)候會話B執(zhí)行的語句落在鎖定范圍內(nèi)的都會進(jìn)行waiting
SELECT?*?FROM?z?WHERE?a?=?5?LOCK?IN?SHARE?MODE; INSERT?INTO?z?SELECT?4,?2; INSERT?INTO?z?SELECT?6,?5;
用戶可以通過以下兩種方式來顯示的關(guān)閉Gap Lock:
-
將事務(wù)的隔離級別設(shè)為 READ COMMITED。
-
將參數(shù)innodb_locks_unsafe_for_binlog設(shè)置為1。
從上面的例子可以看出來,Gap Lock的作用是為了阻止多個(gè)事務(wù)將記錄插入到同一個(gè)范圍內(nèi),設(shè)計(jì)它的目的是用來解決Phontom Problem(幻讀問題)。在MySQL默認(rèn)的隔離級別(Repeatable Read)下,InnoDB就是使用它來解決幻讀問題。
幻讀是指在同一事務(wù)下,連續(xù)執(zhí)行兩次同樣的SQL語句可能導(dǎo)致不同的結(jié)果,第二次的SQL可能會返回之前不存在的行,也就是第一次執(zhí)行和第二次執(zhí)行期間有其他事務(wù)往里插入了新的行。
一致性非鎖定讀
一致性非鎖定讀(consistent nonlocking read)是指InnoDB存儲引擎通過多版本控制(MVCC)的方式來讀取當(dāng)前執(zhí)行時(shí)間數(shù)據(jù)庫中行的數(shù)據(jù)。如果讀取的這行正在執(zhí)行DELETE或UPDATE操作,這時(shí)讀取操作不會向XS鎖一樣去等待鎖釋放,而是會去讀一個(gè)快照數(shù)據(jù)。MVCC相關(guān)的知識我已經(jīng)在另外一篇文章中闡述了,這里就不做過多原理的分析了。地址:談?wù)凪ySQL InnoDB存儲引擎事務(wù)的ACID特性
在事務(wù)隔離級別RC和RR下,InnoDB存儲引擎使用非鎖定的一致性讀。然而對于快照數(shù)據(jù)的定義卻不同,在RC級別下,對于快照數(shù)據(jù),非一致性讀總是讀取被鎖定行的最新一份快照數(shù)據(jù)。而在RR級別下,對于快照數(shù)據(jù),非一致性讀總是讀取事務(wù)開始時(shí)的行數(shù)據(jù)版本。
下面我們通過一個(gè)例子來看看大家是否對MVCC理解了。
可以看到,第1步和第2步是非常容易理解的,而在第3步事務(wù)B插入一條新的數(shù)據(jù)后,在第4步事務(wù)A還是查不到,也就是利用了MVCC的特性來實(shí)現(xiàn)。當(dāng)事務(wù)B提交后,第5步的查詢在RC和RR隔離級別下的輸出是不同的,這個(gè)的原因在另一篇博客中也說到了,是因?yàn)樗麄儎?chuàng)建ReadView的時(shí)機(jī)不同。
但是很詭異的是在第6步的時(shí)候,事務(wù)A更新了一條它看不見的記錄,然后查詢就能夠查詢出來了。這里很多人容易迷惑,不可見不代表記錄不存在,它只是利用了可見性判斷忽略了而已。更新成功之后,事務(wù)A順其自然的記錄了這條記錄的Undo log,在隨后的查詢中,因?yàn)樗軌蚩匆娮约旱母膭舆@一個(gè)可見性的判斷,自然就能夠查詢出來了。這里很多名詞需要去深入讀一下此文:談?wù)凪ySQL InnoDB存儲引擎事務(wù)的ACID特性
一致性鎖定讀
前面說到,在默認(rèn)隔離級別RR下,InnoDB存儲引擎的SELECT操作使用一致性非鎖定讀。但是在某些情況下,用戶需要顯式地對數(shù)據(jù)庫讀取操作進(jìn)行加鎖以保證數(shù)據(jù)邏輯的一致性。InnoDB存儲引擎對于SELECT語句支持兩種一致性的鎖定讀(locking read)操作。
-
SELECT … FOR UPDATE (X鎖)
-
SELECT … LOCK IN SHARE MODE (S鎖)
3、鎖帶來的問題
通過鎖定機(jī)制可以實(shí)現(xiàn)事務(wù)隔離性要求,使得事務(wù)可以并發(fā)的工作。鎖提高了并發(fā),但是卻會帶來潛在的問題。不過好在有事務(wù)隔離性的要求,不同的隔離級別解決的鎖的問題也不同,這里只進(jìn)行簡單的介紹,不進(jìn)行舉例分析了。
InnoDB存儲引擎在RR級別就已經(jīng)解決了所有問題,但是它和Serializable的區(qū)別在哪里呢?區(qū)別就在于RR級別還存在一個(gè)丟失更新問題,而SERIALIZABLE無論對于查詢還是更新都會進(jìn)行鎖定操作。
如圖所示,用戶原始金額為100,如果程序中對于轉(zhuǎn)賬和存款的判斷是先查詢再更新的話就會出現(xiàn)丟失更新的問題,也就是后面的更新覆蓋了前面的更新。如果想避免這種問題,只能每次更新的時(shí)候金額基于表里最新的值來做。如果必須要先查詢再更新,可以在更新的條件里判斷金額(樂觀鎖),也可以使用隔離級別最高的SERIALIZABLE。
4、死鎖
死鎖是指兩個(gè)或兩個(gè)以上的事務(wù)在執(zhí)行過程中,因爭奪鎖資源而造成的一種互相等待的現(xiàn)象,這里直接放上之前項(xiàng)目中遇到的一個(gè)死鎖問題以及深入的分析:由一次線上問題帶來的MySQL死鎖問題分析,這里就不再贅述了。
推薦教程:《Mysql教程》