成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

MySQL引擎特性:InnoDB同步機(jī)制

數(shù)據(jù)庫(kù) MySQL
不同的進(jìn)程或者線程需要協(xié)同工作以完成特征的任務(wù),這就需要一套完善的同步機(jī)制,在Linux內(nèi)核中有相應(yīng)的技術(shù)實(shí)現(xiàn),包括原子操作,信號(hào)量,互斥鎖,自旋鎖,讀寫鎖等。InnoDB考慮到效率和監(jiān)控兩方面的原因,實(shí)現(xiàn)了一套獨(dú)有的同步機(jī)制,提供給其他模塊調(diào)用。

MySQL引擎特性:InnoDB同步機(jī)制

前言

現(xiàn)代操作系統(tǒng)以及硬件基本都支持并發(fā)程序,而在并發(fā)程序設(shè)計(jì)中,各個(gè)進(jìn)程或者線程需要對(duì)公共變量的訪問(wèn)加以制約,此外,不同的進(jìn)程或者線程需要協(xié)同工作以完成特征的任務(wù),這就需要一套完善的同步機(jī)制,在Linux內(nèi)核中有相應(yīng)的技術(shù)實(shí)現(xiàn),包括原子操作,信號(hào)量,互斥鎖,自旋鎖,讀寫鎖等。InnoDB考慮到效率和監(jiān)控兩方面的原因,實(shí)現(xiàn)了一套獨(dú)有的同步機(jī)制,提供給其他模塊調(diào)用。本文的分析默認(rèn)基于MySQL 5.6,CentOS 6,gcc 4.8,其他版本的信息會(huì)另行指出。

基礎(chǔ)知識(shí)

同步機(jī)制對(duì)于其他數(shù)據(jù)庫(kù)模塊來(lái)說(shuō)相對(duì)獨(dú)立,但是需要比較多的操作系統(tǒng)以及硬件知識(shí),這里簡(jiǎn)單介紹一下幾個(gè)有用的概念,便于讀者理解后續(xù)概念。

內(nèi)存模型 :主要分為語(yǔ)言級(jí)別的內(nèi)存模型和硬件級(jí)別的內(nèi)存模型。語(yǔ)言級(jí)別的內(nèi)存模型,C/C++屬于weak memory model,簡(jiǎn)單的說(shuō)就是編譯器在進(jìn)行編譯優(yōu)化的時(shí)候,可以對(duì)指令進(jìn)行重排,只需要保證在單線程的環(huán)境下,優(yōu)化前和優(yōu)化后執(zhí)行結(jié)果一致即可,執(zhí)行中間過(guò)程不保證跟代碼的語(yǔ)義順序一致。所以在多線程的環(huán)境下,如果依賴代碼中間過(guò)程的執(zhí)行順序,程序就會(huì)出現(xiàn)問(wèn)題。硬件級(jí)別的內(nèi)存模型,我們常用的cpu,也屬于弱內(nèi)存模型,即cpu在執(zhí)行指令的時(shí)候,為了提升執(zhí)行效率,也會(huì)對(duì)某些執(zhí)行進(jìn)行亂序執(zhí)行(按照wiki提供的資料,在x86 64環(huán)境下,只會(huì)發(fā)生讀寫亂序,即讀操作可能會(huì)被亂序到寫操作之前),如果在編程的時(shí)候不做一些措施,同樣容易造成錯(cuò)誤。

內(nèi)存屏障 :為了解決弱內(nèi)存模型造成的問(wèn)題,需要一種能控制指令重排或者亂序執(zhí)行程序的手段,這種技術(shù)就叫做內(nèi)存屏障,程序員只需要在代碼中插入特定的函數(shù),就能控制弱內(nèi)存模型帶來(lái)的負(fù)面影響,當(dāng)然,由于影響了亂序和重排這類的優(yōu)化,對(duì)代碼的執(zhí)行效率有一定的影響。具體實(shí)現(xiàn)上,內(nèi)存屏障技術(shù)分三種,一種是full memory barrier,即barrier之前的操作不能亂序或重排到barrier之后,同時(shí)barrier之后的操作不能亂序或重排到barrier之前,當(dāng)然這種full barrier對(duì)性能影響最大,為了提高效率才有了另外兩種:acquire barrier和release barrier,前者只保證barrier后面的操作不能移到之前,后者只保證barrier前面的操作不移到之后。

互斥鎖 :互斥鎖有兩層語(yǔ)義,除了大家都知道的排他性(即只允許一個(gè)線程同時(shí)訪問(wèn))外,還有一層內(nèi)存屏障(full memory barrier)的語(yǔ)義,即保證臨界區(qū)的操作不會(huì)被亂序到臨界區(qū)外。Pthread庫(kù)里面常用的mutex,conditional variable等操作都自帶內(nèi)存屏障這層語(yǔ)義。此外,使用pthread庫(kù),每次調(diào)用都需要應(yīng)用程序從用戶態(tài)陷入到內(nèi)核態(tài)中查看當(dāng)前環(huán)境,在鎖沖突不是很嚴(yán)重的情況下,效率相對(duì)比較低。

自旋鎖 :傳統(tǒng)的互斥鎖,只要一檢測(cè)到鎖被其他線程所占用了,就立刻放棄cpu時(shí)間片,把cpu留給其他線程,這就會(huì)產(chǎn)生一次上下文切換。當(dāng)系統(tǒng)壓力大的時(shí)候,頻繁的上下文切換會(huì)導(dǎo)致sys值過(guò)高。自旋鎖,在檢測(cè)到鎖不可用的時(shí)候,首先cpu忙等一小會(huì)兒,如果還是發(fā)現(xiàn)不可用,再放棄cpu,進(jìn)行切換。互斥鎖消耗cpu sys值,自旋鎖消耗cpu usr值。

遞歸鎖 :如果在同一個(gè)線程中,對(duì)同一個(gè)互斥鎖連續(xù)加鎖兩次,即第一次加鎖后,沒(méi)有釋放,繼續(xù)進(jìn)行對(duì)這個(gè)鎖進(jìn)行加鎖,那么如果這個(gè)互斥鎖不是遞歸鎖,將導(dǎo)致死鎖。可以把遞歸鎖理解為一種特殊的互斥鎖。

死鎖 :構(gòu)成死鎖有四大條件,其中有一個(gè)就是加鎖順序不一致,如果能保證不同類型的鎖按照某個(gè)特定的順序加鎖,就能大大降低死鎖發(fā)生的概率,之所以不能完全消除,是因?yàn)橥环N類型的鎖依然可能發(fā)生死鎖。另外,對(duì)同一個(gè)鎖連續(xù)加鎖兩次,如果是非遞歸鎖,也將導(dǎo)致死鎖。

原子操作

現(xiàn)代的cpu提供了對(duì)單一變量簡(jiǎn)單操作的原子指令,即這個(gè)變量的這些簡(jiǎn)單操作只需要一條cpu指令即可完成,這樣就不用對(duì)這個(gè)操作加互斥鎖了,在鎖沖突不激烈的情況下,減少了用戶態(tài)和內(nèi)核態(tài)的切換,化悲觀鎖為樂(lè)觀鎖,從而提高了效率。此外,現(xiàn)在外面很火的所謂無(wú)鎖編程(類似CAS操作),底層就是用了這些原子操作。gcc為了方便程序員使用這些cpu原子操作,提供了一系列__sync開頭的函數(shù),這些函數(shù)如果包含內(nèi)存屏障語(yǔ)義,則同時(shí)禁止編譯器指令重排和cpu亂序執(zhí)行。

InnoDB針對(duì)不同的操作系統(tǒng)以及編譯器環(huán)境,自己封裝了一套原子操作,在頭文件os0sync.h中。下面的操作基于Linux x86 64位環(huán)境, gcc 4.1以上的版本進(jìn)行分析。

os_compare_and_swap_xxx(ptr, old_val, new_val)類型的操作底層都使用了gcc包裝的__sync_bool_compare_and_swap(ptr, old_val, new_val)函數(shù),語(yǔ)義為,交換成功則返回true,ptr是交換后的值,old_val是之前的值,new_val是交換后的預(yù)期值。這個(gè)原子操作是個(gè)內(nèi)存屏障(full memory barrier)。

os_atomic_increment_xxx類型的操作底層使用了函數(shù)__sync_add_and_fetch,os_atomic_decrement_xxx類型的操作使用了函數(shù)__sync_sub_and_fetch,分別表示原子遞增和原子遞減。這個(gè)兩個(gè)原子操作也都是內(nèi)存屏障(full memory barrier)。

另外一個(gè)比較重要的原子操作是os_atomic_test_and_set_byte(ptr, new_val),這個(gè)操作使用了__sync_lock_test_and_set(ptr, new_val)這個(gè)函數(shù),語(yǔ)義為,把ptr設(shè)置為new_val,同時(shí)返回舊的值。這個(gè)操作提供了原子改變某個(gè)變量值的操作,InnoDB鎖實(shí)現(xiàn)的同步機(jī)制中,大量的用了這個(gè)操作,因此比較重要。需要注意的是,參看gcc文檔,這個(gè)操作不是full memory barrier,只是一個(gè)acquire barrier,簡(jiǎn)單的說(shuō)就是,代碼中__sync_lock_test_and_set之后操作不能被亂序或者重排到__sync_lock_test_and_set之前,但是__sync_lock_test_and_set之前的操作可能被重排到其之后。

關(guān)于內(nèi)存屏障的專門指令,MySQL 5.7提供的比較完善。os_rmb表示acquire barrier,os_wmb表示release barrier。如果在編程時(shí),需要在某個(gè)位置準(zhǔn)確的讀取一個(gè)變量的值時(shí),記得在讀取之前加上os_rmb,同理,如果需要在某個(gè)位置保證一個(gè)變量已經(jīng)被寫了,記得在寫之后調(diào)用os_wmb。

條件通知機(jī)制

條件通知機(jī)制在多線程協(xié)作中非常有用,一個(gè)線程往往需要等待其他線程完成指定工作后,再進(jìn)行工作,這個(gè)時(shí)候就需要有線程等待和線程通知機(jī)制。Pthread_cond_XXX類似的變量和函數(shù)來(lái)完成等待和通知的工作。InnoDB中,對(duì)Pthread庫(kù)進(jìn)行了簡(jiǎn)單的封裝,并在此基礎(chǔ)上,進(jìn)一步抽象,提供了一套方便易用的接口函數(shù)給調(diào)用者使用。

系統(tǒng)條件變量

在文件os0sync.cc中,os_cond_XXX類似的函數(shù)就是InnoDB對(duì)Pthread庫(kù)的封裝。常用的幾個(gè)函數(shù)如:

os_cond_t是核心的操作對(duì)象,其實(shí)就是pthread_cond_t的一層typedef而已,os_cond_init初始化函數(shù),os_cond_destroy銷毀函數(shù),os_cond_wait條件等待,不會(huì)超時(shí),os_cond_wait_timed條件等待,如果超時(shí)則返回,os_cond_broadcast喚醒所有等待線程,os_cond_signal只喚醒其中一個(gè)等待線程,但是在閱讀源碼的時(shí)候發(fā)現(xiàn),似乎沒(méi)有什么地方調(diào)用了os_cond_signal。。。

此外,還有一個(gè)os_cond_module_init函數(shù),用來(lái)window下的初始化操作。

在InnoDB下,os_cond_XXX模塊的函數(shù)主要是給InnoDB自己設(shè)計(jì)的條件變量使用。

InnoDB條件變量

如果在InnoDB層直接使用系統(tǒng)條件變量的話,主要有四個(gè)弊端,首先,弊端1,系統(tǒng)條件變量的使用需要與一個(gè)系統(tǒng)互斥鎖(詳見下一節(jié))相配合使用,使用完還要記得及時(shí)釋放,使用者會(huì)比較麻煩。接著,弊端2,在條件等待的時(shí)候,需要在一個(gè)循環(huán)中等待,使用者還是比較麻煩。最后,弊端3,也是比較重要的,不方便系統(tǒng)監(jiān)控。

基于以上幾點(diǎn),InnoDB基于系統(tǒng)的條件變量和系統(tǒng)互斥鎖自己實(shí)現(xiàn)了一套條件通知機(jī)制。主要在文件os0sync.cc中實(shí)現(xiàn),相關(guān)數(shù)據(jù)結(jié)構(gòu)以及接口進(jìn)一層的包裝在頭文件os0sync.h中。使用方法如下:

InnoDB條件變量核心數(shù)據(jù)結(jié)構(gòu)為os_event_t,類似pthread_cont_t。如果需要?jiǎng)?chuàng)建和銷毀則分別使用os_event_create和os_event_free函數(shù)。需要等待某個(gè)條件變量,先調(diào)用os_event_reset(原因見下一段),然后使用os_event_wait,如果需要超時(shí)等待,使用os_event_wait_time替換os_event_wait即可,os_event_wait_XXX這兩個(gè)函數(shù),解決了弊端1和弊端2,此外,建議把os_event_reset返回值傳給他們,這樣能防止多線程情況下的無(wú)限等待(詳見下下段)。如果需要發(fā)出一個(gè)條件通知,使用os_event_set。這個(gè)幾個(gè)函數(shù),里面都插入了一些監(jiān)控信息,方便InnoDB上層管理。怎么樣,方便多了吧~

多線程環(huán)境下可能發(fā)生的問(wèn)題

首先來(lái)說(shuō)說(shuō)兩個(gè)線程下會(huì)發(fā)生的問(wèn)題。創(chuàng)建后,正常的使用順序是這樣的,線程A首先os_event_reset(步驟1),然后os_event_wait(步驟2),接著線程B做完該做的事情后,執(zhí)行os_event_set(步驟3)發(fā)送信號(hào),通知線程A停止等待,但是在多線程的環(huán)境中,會(huì)出現(xiàn)以下兩種步驟順序錯(cuò)亂的情況:亂序A: 步驟1--步驟3--步驟2,亂序B: 步驟3--步驟1--步驟2。對(duì)于亂序B,屬于條件通知在條件等待之前發(fā)生,目前InnoDB條件變量的機(jī)制下,會(huì)發(fā)生無(wú)限等待,所以上層調(diào)用的時(shí)候一定要注意,例如在InnoDB在實(shí)現(xiàn)互斥鎖和讀寫鎖的時(shí)候?yàn)榱朔乐拱l(fā)生條件通知在條件等待之前發(fā)生,在等待之前對(duì)lock_word再次進(jìn)行了判斷,詳見InnoDB自旋互斥鎖這一節(jié)。為了解決亂序A,InnoDB在核心數(shù)據(jù)結(jié)構(gòu)os_event中引入布爾型變量is_set,is_set這個(gè)變量就表示是否已經(jīng)發(fā)生過(guò)條件通知,在每次調(diào)用條件通知之前,會(huì)把這個(gè)變量設(shè)置為true(在os_event_reset時(shí)改為false,便于多次通知),在條件等待之前會(huì)檢查一下這變量,如果這個(gè)變量為true,就不再等待了。所以,亂序A也能保證不會(huì)發(fā)生無(wú)限等待。

接著我們來(lái)說(shuō)說(shuō)大于兩個(gè)線程下可能會(huì)發(fā)生的問(wèn)題。線程A和C是等待線程,等待同一個(gè)條件變量,B是通知線程,通知A和C結(jié)束等待。考慮一個(gè)亂序C:線程A執(zhí)行os_event_reset(步驟1),線程B馬上就執(zhí)行os_event_set(步驟2)了,接著線程C執(zhí)行了os_event_reset(步驟3),最后線程A執(zhí)行os_event_wait(步驟4),線程C執(zhí)行os_event_wait(步驟5)。乍一眼看,好像看不出啥問(wèn)題,但是實(shí)際上你會(huì)發(fā)現(xiàn)A和C線程在無(wú)限等待了。原因是,步驟2,把is_set這個(gè)變量設(shè)置為false,但是在步驟3,線程C通過(guò)reset又把它給重新設(shè)回false了。。然后線程A和C在os_event_wait中誤以為還沒(méi)有發(fā)生過(guò)條件通知,就開始無(wú)限等待了。為了解決這個(gè)問(wèn)題,InnoDB在核心數(shù)據(jù)結(jié)構(gòu)os_event中引入64位整形變量signal_count,用來(lái)記錄已經(jīng)發(fā)出條件信號(hào)的次數(shù)。每次發(fā)出一個(gè)條件通知,這個(gè)變量就遞增1。os_event_reset的返回值就把當(dāng)前的signal_count值取出來(lái)。os_event_wait如果發(fā)現(xiàn)有這個(gè)參數(shù)的傳入,就會(huì)判斷傳入的參數(shù)與當(dāng)前的signal_count值是否相同,如果不相同,表示這個(gè)已經(jīng)通知過(guò)了,就不會(huì)進(jìn)入等待了。舉個(gè)例子,假設(shè)亂序C,一開始的signal_count為100,步驟1把這個(gè)參數(shù)傳給了步驟4,在步驟4中,os_event_wait會(huì)發(fā)現(xiàn)傳入值100與當(dāng)前的值101(步驟2中遞增了1)不同,所以線程A認(rèn)為信號(hào)已經(jīng)發(fā)生過(guò)了,就不會(huì)再等待了。。。然而。。線程C呢?步驟3返回的值應(yīng)該是101,傳給步驟5后,發(fā)生于當(dāng)前值一樣。。繼續(xù)等待。。。仔細(xì)分析可以發(fā)現(xiàn),線程C是屬于條件變量通知發(fā)生在等待之前(步驟2,步驟3,步驟5),上一段已經(jīng)說(shuō)過(guò)了,針對(duì)這種通知提前發(fā)出的,目前InnoDB沒(méi)有非常好的解法,只能調(diào)用者自己控制。

總結(jié)一下, InnoDB條件變量能方便InnoDB上層做監(jiān)控,也簡(jiǎn)化了條件變量使用的方法,但是調(diào)用者上層邏輯必須保證條件通知不能過(guò)早的發(fā)出,否則就會(huì)有無(wú)限等待的可能。

互斥鎖

互斥鎖保證一段程序同時(shí)只能一個(gè)線程訪問(wèn),保證臨界區(qū)得到正確的序列化訪問(wèn)。同條件變量一樣,InnoDB對(duì)Pthread的mutex簡(jiǎn)單包裝了一下,提供給其他模塊用(主要是輔助其他自己實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),不用InnoDB自己的互斥鎖是為了防止遞歸引用,詳見輔助結(jié)構(gòu)這一節(jié))。但與條件變量不同的是,InnoDB自己實(shí)現(xiàn)的一套互斥鎖并沒(méi)有依賴Pthread庫(kù),而是依賴上述的原子操作(如果平臺(tái)不支持原子操作則使用Pthread庫(kù),但是這種情況不太會(huì)發(fā)生,因?yàn)間cc在4.1就支持原子操作了)和上述的InnoDB條件變量。

系統(tǒng)互斥鎖

相比與系統(tǒng)條件變量,系統(tǒng)互斥鎖除了包裝Pthread庫(kù)外,還做了一層簡(jiǎn)單的監(jiān)控統(tǒng)計(jì),結(jié)構(gòu)名為os_mutex_t。在文件os0sync.cc中,os_mutex_create創(chuàng)建mutex,并調(diào)用os_fast_mutex_init_func創(chuàng)建pthread的mutex,值得一提的是,創(chuàng)建pthread mutex的參數(shù)是my_fast_mutexattr的東西,其在MySQL server層函數(shù)my_thread_global_init初始化 ,只要pthread庫(kù)支持,則默認(rèn)成初始化為PTHREAD_MUTEX_ADAPTIVE_NP和PTHREAD_MUTEX_ERRORCHECK。前者表示,當(dāng)鎖釋放,之前在等待的鎖進(jìn)行公平的競(jìng)爭(zhēng),而不是按照默認(rèn)的優(yōu)先級(jí)模式。后者表示,如果發(fā)生了遞歸的加鎖,即同一個(gè)線程對(duì)同一個(gè)鎖連續(xù)加鎖兩次,第二次加鎖會(huì)報(bào)錯(cuò)。另外三個(gè)有用的函數(shù)為,銷毀鎖os_mutex_free,加鎖os_mutex_enter,解鎖os_mutex_exit。

一般來(lái)說(shuō),InnoDB上層模塊不需要直接與系統(tǒng)互斥鎖打交道,需要用鎖的時(shí)候一般用InnoDB自己實(shí)現(xiàn)的一套互斥鎖。系統(tǒng)互斥鎖主要是用來(lái)輔助實(shí)現(xiàn)一些數(shù)據(jù)結(jié)構(gòu),例如最后一節(jié)提到的一些輔助結(jié)構(gòu),由于這些輔助結(jié)構(gòu)可能本身就要提供給InnoDB自旋互斥鎖用,為了防止遞歸引用,就暫時(shí)用系統(tǒng)互斥鎖來(lái)代替。

InnoDB自旋互斥鎖

為什么InnoDB需要實(shí)現(xiàn)自己的一套互斥鎖,不直接用上述的系統(tǒng)互斥鎖呢?這個(gè)主要有以下幾個(gè)原因,首先,系統(tǒng)互斥鎖是基于pthread mutex的,Heikki Tuuri(同步模塊的作者,也是Innobase的創(chuàng)始人)認(rèn)為在當(dāng)時(shí)的年代pthread mutex上下文切換造成的cpu開銷太大,使用spin lock的方式在多處理器的機(jī)器上更加有效,尤其是在鎖競(jìng)爭(zhēng)不是很嚴(yán)重的時(shí)候,Heikki Tuuri還總結(jié)出,在spin lock大概自旋20微秒的時(shí)候在多處理的機(jī)器下效率最高。其次,不使用pthread spin lock的原因是,當(dāng)時(shí)在1995年左右的時(shí)候,spin lock的類似實(shí)現(xiàn),效率很低,而且當(dāng)時(shí)的spin lock不支持自定義自旋時(shí)間,要知道自旋鎖在單處理器的機(jī)器上沒(méi)什么卵用。最后,也是為了更加完善的監(jiān)控需求。總的來(lái)說(shuō),有歷史原因,有監(jiān)控需求也有自定義自旋時(shí)間的需求,然后就有了這一套InnoDB自旋互斥鎖。

InnoDB自旋互斥鎖的實(shí)現(xiàn)主要在文件sync0sync.cc和sync0sync.ic中,頭文件sync0sync.h定義了核心數(shù)據(jù)結(jié)構(gòu)ib_mutex_t。使用方法很簡(jiǎn)單,mutex_create創(chuàng)建鎖,mutex_free釋放鎖,mutex_enter嘗試獲得鎖,如果已經(jīng)被占用了,則等待。mutex_exit釋放鎖,同時(shí)喚醒所有等待的線程,拿到鎖的線程開始執(zhí)行,其余線程繼續(xù)等待。mutex_enter_nowait這個(gè)函數(shù)類似pthread的trylock,只要已檢測(cè)到鎖不用,就直接返回錯(cuò)誤,不進(jìn)行自旋等待??傮w來(lái)說(shuō),InnoDB自旋互斥鎖的用法和語(yǔ)義跟系統(tǒng)互斥鎖一模一樣,但是底層實(shí)現(xiàn)卻大相徑庭。

在ib_mutex_t這個(gè)核心數(shù)據(jù)結(jié)構(gòu)中,最重要的是前面兩個(gè)變量:event和lock_word。lock_word為0表示鎖空閑,1表示鎖被占用,InnoDB自旋互斥鎖使用__sync_lock_test_and_set這個(gè)函數(shù)對(duì)lock_word進(jìn)行原子操作,加鎖的時(shí)候,嘗試把其設(shè)置為1,函數(shù)返回值不指示是否成功,指示的是嘗試設(shè)置之前的值,因此如果返回值是0,表示加鎖成功,返回是1表示失敗。如果加鎖失敗,則會(huì)自旋一段時(shí)間,然后等待在條件變量event(os_event_wait)上,當(dāng)鎖占用者釋放鎖的時(shí)候,會(huì)使用os_event_set來(lái)喚醒所有的等待者。簡(jiǎn)單的來(lái)說(shuō),byte類型的lock_word基于平臺(tái)提供的原子操作來(lái)實(shí)現(xiàn)互斥訪問(wèn),而event是InnoDB條件變量類型,用來(lái)實(shí)現(xiàn)鎖釋放后喚醒等待線程的操作。

接下來(lái),詳細(xì)介紹一下,mutex_enter和mutex_exit的邏輯,InnoDB自旋互斥鎖的精華都在這兩個(gè)函數(shù)中。

mutex_enter的偽代碼如下:

 

  1. if (__sync_lock_test_and_set(mutex->lock_word, 1) == 0) { 
  2.  
  3.     get mutex successfully; 
  4.  
  5.     return
  6.  
  7.  
  8. loop1: 
  9.  
  10.     i = 0; 
  11.  
  12. loop2: 
  13.  
  14.     /*指示點(diǎn)1*/ 
  15.  
  16.     while (mutex->lock_word ! = 0 && i < SPIN_ROUNDS) { 
  17.  
  18.              random spin using ut_delay, spin max time depend on SPIN_WAIT_DELAY; 
  19.  
  20.              i++; 
  21.  
  22.  
  23. if (i == SPIN_ROUNDS) { 
  24.  
  25.     yield_cpu; 
  26.  
  27.  
  28. /*指示點(diǎn)2*/ 
  29.  
  30. if (__sync_lock_test_and_set(mutex->lock_word, 1) == 0) { 
  31.  
  32.     get mutex successfully; 
  33.  
  34.     return
  35.  
  36.  
  37. if (i < SPIN_ROUNDS) { 
  38.  
  39.      goto loop2 
  40.  
  41.  
  42. /*指示點(diǎn)4*/ 
  43.  
  44. get cell from sync array and call os_event_reset(mutex->event); 
  45.  
  46. mutex->waiter =1; 
  47.  
  48. /*指示點(diǎn)3*/ 
  49.  
  50. for (i = 0; i < 4; i++) { 
  51.  
  52.     if (__sync_lock_test_and_set(mutex->lock_word, 1) == 0) { 
  53.  
  54.         get mutex successfully; 
  55.  
  56.         free cell; 
  57.  
  58.         return
  59.  
  60.     } 
  61.  
  62.  
  63. sync array wait and os_event_wait(mutex->event); 
  64.  
  65. goto loop1; 

代碼還是有點(diǎn)小復(fù)雜的。這里分析幾點(diǎn)如下:

1. SPIN_ROUNDS控制了在放棄cpu時(shí)間片(yield_cpu)之前,一共進(jìn)行多少次忙等,這個(gè)參數(shù)就是對(duì)外可配置的innodb_sync_spin_loops,而SPIN_WAIT_DELAY控制了每次忙等的時(shí)間,這個(gè)參數(shù)也就是對(duì)外可配置的innodb_spin_wait_delay。這兩個(gè)參數(shù)一起決定了自旋的時(shí)間。Heikki Tuuri建議在單處理器的機(jī)器上調(diào)小spin的時(shí)間,在對(duì)稱多處理器的機(jī)器上,可以適當(dāng)調(diào)大。比較有意思的是innodb_spin_wait_delay的單位,這個(gè)是100MHZ的奔騰處理器處理1毫秒的時(shí)間,默認(rèn)innodb_spin_wait_delay配置成6,表示最多在100MHZ的奔騰處理器上自旋6毫秒。由于現(xiàn)在cpu都是按照GHZ來(lái)計(jì)算的,所以按照默認(rèn)配置自旋時(shí)間往往很短。此外,自旋不真是cpu傻傻的在那邊100%的跑,在現(xiàn)代的cpu上,給自旋專門提供了一條指令,在筆者的測(cè)試環(huán)境下,這條指令是pause,查看Intel的文檔,其對(duì)pause的解釋是:不會(huì)發(fā)生用戶態(tài)和內(nèi)核態(tài)的切換,cpu在用戶態(tài)自旋,因此不會(huì)發(fā)生上下文切換,同時(shí)這條指令不會(huì)消耗太多的能耗。。。所以那些說(shuō)spin lock太浪費(fèi)電的不攻自破了。。。另外,編譯器也不會(huì)把ut_delay給優(yōu)化掉,因?yàn)槠淅锩婀烙?jì)修改了一個(gè)全局變量。

2. yield_cpu 操作在筆者的環(huán)境中,就是調(diào)用了pthread_yield函數(shù),這個(gè)函數(shù)把放棄當(dāng)前cpu的時(shí)間片,然后把當(dāng)前線程放到cpu可執(zhí)行隊(duì)列的末尾。

3. 在指示點(diǎn)1后面的循環(huán),沒(méi)有采用原子操作讀取數(shù)據(jù),是因?yàn)?,Heikki Tuuri認(rèn)為由于原子操作在內(nèi)存和cpu cache之間會(huì)產(chǎn)生過(guò)的數(shù)據(jù)交換,如果只是讀本地的cache,可以減少總線的爭(zhēng)用。即使本地讀到臟的數(shù)據(jù),也沒(méi)關(guān)系,因?yàn)樵谔鲅h(huán)的指示點(diǎn)2,依然會(huì)再一次使用原子操作進(jìn)行校驗(yàn)。

4. get cell這個(gè)操作是從sync array執(zhí)行的,sync array詳見輔助數(shù)據(jù)結(jié)構(gòu)這一節(jié),簡(jiǎn)單的說(shuō)就是提供給監(jiān)控線程使用的。

5. 注意一下,os_event_reset和os_event_wait這兩個(gè)函數(shù)的調(diào)用位置,另外,有一點(diǎn)必須清楚,就是os_event_set(鎖持有者釋放所后會(huì)調(diào)用這個(gè)函數(shù)通知所有等待者)可能在這整段代碼執(zhí)行到任意位置出現(xiàn),有可能出現(xiàn)在指示點(diǎn)4的位置,這樣就構(gòu)成了條件變量通知在條件變量等待之前,會(huì)造成無(wú)限等待。為了解決這個(gè)問(wèn)題,才有了指示點(diǎn)3下面的代碼,需要重新再次檢測(cè)一下lock_word,另外,即使os_event_set發(fā)生在os_event_reset之后,有了這些代碼,也能讓當(dāng)前線程提前拿到鎖,不用執(zhí)行后續(xù)os_event_wait的代碼,一定程度上提高了效率。

mutex_exit的偽代碼就簡(jiǎn)單多了,如下:

 

  1. __sync_lock_test_and_set(mutex->lock_word, 0);  
  2.  
  3. /* A problem: we assume that mutex_reset_lock word                                                                     
  4.  
  5.         is a memory barrier, that is when we read the waiters                                                                   
  6.  
  7.         field next, the read must be serialized in memory                                                                       
  8.  
  9.         after the reset. A speculative processor might                                                                         
  10.  
  11.         perform the read first, which could leave a waiting                                                                     
  12.  
  13.         thread hanging indefinitely.                                                                                                                                                                                                                         
  14.  
  15. Our current solution call every second                                                                                 
  16.  
  17.         sync_arr_wake_threads_if_sema_free()                                                                                   
  18.  
  19.         to wake up possible hanging threads if                                                                                 
  20.  
  21.         they are missed in mutex_signal_object. */ 
  22.  
  23.   
  24.  
  25. if (mutex->waiter != 0) { 
  26.  
  27.      mutex->waiter = 0; 
  28.  
  29.      os_event_set(mutex->event); 
  30.  

 

1. waiter是ib_mutex_t中的一個(gè)變量,用來(lái)表示當(dāng)前是否有線程在等待這個(gè)鎖。整個(gè)代碼邏輯很簡(jiǎn)單,就是先把lock_word設(shè)置為0,然后如果發(fā)現(xiàn)有等待者,就把所有等待者給喚醒。facebook的mark callaghan在2014年測(cè)試過(guò),相比現(xiàn)在已經(jīng)比較完善的pthread庫(kù),InnoDB自旋互斥鎖只在并發(fā)量相對(duì)較低(小于256線程)和鎖等待時(shí)間比較短的情況下有優(yōu)勢(shì),在高并發(fā)且較長(zhǎng)的鎖等待時(shí)間情況下,退化比較嚴(yán)重,其中一個(gè)很重要的原因就是InnoDB自旋互斥鎖在鎖釋放的時(shí)候需要喚醒所有等待者。由于os_event_ret底層通過(guò)pthread_cond_boardcast來(lái)通知所有的等待者,一種改進(jìn)是把pthread_cond_boardcast改成pthread_cond_signal,即只喚醒一個(gè)線程,但I(xiàn)naam Rana Mark測(cè)試后發(fā)現(xiàn),如果只喚醒一個(gè)線程的話,在高并發(fā)的情況下,這個(gè)線程可能不會(huì)立刻被cpu調(diào)度到。。由此看來(lái),似乎喚醒一個(gè)特定數(shù)量的等待者是一個(gè)比較好的選擇。

2. 偽代碼中的這段注釋筆者估計(jì)加上去的,大意是由于編譯器或者cpu的指令重排亂序執(zhí)行,mutex->waiter這個(gè)變量的讀取可能在發(fā)生在原子操作之前,從而導(dǎo)致一些無(wú)線等待的問(wèn)題。然后還專門開了一個(gè)叫做sync_arr_wake_threads_if_sema_free的函數(shù)來(lái)做清理。這個(gè)函數(shù)是在后臺(tái)線程srv_error_monitor_thread中做的,每隔1秒鐘執(zhí)行一次。在現(xiàn)代的cpu和編譯器上,完全可以用內(nèi)存屏障的技術(shù)來(lái)防止指令重排和亂序執(zhí)行,這個(gè)函數(shù)可以被去掉,官方的意見貌似是,不要這么激進(jìn),萬(wàn)一其他地方還需要這個(gè)函數(shù)呢。。詳見BUG #79477。

總體來(lái)說(shuō),InnoDB自旋互斥鎖的底層實(shí)現(xiàn)還是比較有意思的,非常適合學(xué)習(xí)研究。這套鎖機(jī)制在現(xiàn)在完善的Pthread庫(kù)和高達(dá)4GMHZ的cpu下,已經(jīng)有點(diǎn)力不從心了,mark callaghan研究發(fā)現(xiàn),在高負(fù)載的壓力下,使用這套鎖機(jī)制的InnoDB,大部分cpu時(shí)間都給了sys和usr,基本沒(méi)有空閑,而pthread mutex在相同情況下,卻有平均80%的空閑。同時(shí),由于ib_mutex_t這個(gè)結(jié)構(gòu)體體積比較龐大,當(dāng)buffer pool比較大的時(shí)候,會(huì)發(fā)現(xiàn)鎖占用了很多的內(nèi)存。最后,從代碼風(fēng)格上來(lái)說(shuō),有不少代碼沒(méi)有解耦,如果需要把鎖模塊單獨(dú)打成一個(gè)函數(shù)庫(kù),比較困難。

基于上述幾個(gè)缺陷,MySQL 5.7及后續(xù)的版本中,對(duì)互斥鎖進(jìn)行了大量的重新,包括以下幾點(diǎn)(WL#6044):

1. 使用了C++中的類繼承關(guān)系,系統(tǒng)互斥鎖和InnoDB自己實(shí)現(xiàn)的自旋互斥鎖都是一個(gè)父類的子類。

2. 由于bool pool的鎖對(duì)性能要求比較高,因此使用靜態(tài)繼承(也就是模板)的方式來(lái)減少繼承中虛指針造成的開銷。

3. 保留舊的InnoDB自旋互斥鎖,并實(shí)現(xiàn)了一種基于futex的鎖。簡(jiǎn)單的說(shuō),futex鎖與上述的原子操作類似,能減少用戶態(tài)和內(nèi)核態(tài)切換的開銷,但同時(shí)保留類似mutex的使用方法,大大降低了程序編寫的難度。

InnoDB讀寫鎖

與條件變量、互斥鎖不同,InnoDB里面沒(méi)有Pthread庫(kù)的讀寫鎖的包裝,其完全依賴依賴于原子操作和InnoDB的條件變量,甚至都不需要依賴InnoDB的自旋互斥鎖。此外,讀寫鎖還實(shí)現(xiàn)了寫操作的遞歸鎖,即同一個(gè)線程可以多次獲得寫鎖,但是同一個(gè)線程依然不能同時(shí)獲得讀鎖和寫鎖。InnoDB讀寫鎖的核心數(shù)據(jù)結(jié)構(gòu)rw_lock_t中,并沒(méi)有等待隊(duì)列的信息,因此不能保證先到的請(qǐng)求一定會(huì)先進(jìn)入臨界區(qū)。這與系統(tǒng)互斥量用PTHREAD_MUTEX_ADAPTIVE_NP來(lái)初始化有異曲同工之妙。

InnoDB讀寫鎖的核心實(shí)現(xiàn)在源文件sync0rw.cc和sync0rw.ic中,核心數(shù)據(jù)結(jié)構(gòu)rw_lock_t定義在sync0rw.h中。使用方法與InnoDB自旋互斥鎖很類似,只不過(guò)讀請(qǐng)求和寫請(qǐng)求要調(diào)用不同的函數(shù)。加讀鎖調(diào)用rw_lock_s_lock, 加寫鎖調(diào)用rw_lock_x_lock,釋放讀鎖調(diào)用rw_lock_s_unlock, 釋放寫鎖調(diào)用rw_lock_x_unlock,創(chuàng)建讀寫鎖調(diào)用rw_lock_create,釋放讀寫鎖調(diào)用rw_lock_free。函數(shù)rw_lock_x_lock_nowait和rw_lock_s_lock_nowait表示,當(dāng)加讀寫鎖失敗的時(shí)候,直接返回,而不是自旋等待。

核心機(jī)制

rw_lock_t中,核心的成員有以下幾個(gè):lock_word, event, waiters, wait_ex_event,writer_thread, recursive。

與InnoDB自旋互斥鎖的lock_word不同,rw_lock_t中的lock_word是int 型,注意不是unsigned的,其取值范圍是(-2X_LOCK_DECR, X_LOCK_DECR],其中X_LOCK_DECR為0x00100000,差不多100多W的一個(gè)數(shù)。在InnoDB自旋互斥鎖互斥鎖中,lock_word的取值范圍只有0,1,因?yàn)檫@兩個(gè)狀態(tài)就能把互斥鎖的所有狀態(tài)都表示出來(lái)了,也就是說(shuō),只需要查看一下這個(gè)lock_word就能確定當(dāng)前的線程是否能獲得鎖。rw_lock_t中的lock_word也扮演了相同的角色,只需要查看一下當(dāng)前的lock_word落在哪個(gè)取值范圍中,就確定當(dāng)前線程能否獲得鎖。至于rw_lock_t中的lock_word是如何做到這一點(diǎn)的,這其實(shí)是InnoDB讀寫鎖乃至InnoDB同步機(jī)制中最神奇的地方,下文我們會(huì)詳細(xì)分析。

event是一個(gè)InnoDB條件變量,當(dāng)當(dāng)前的鎖已經(jīng)被一個(gè)線程以寫鎖方式獨(dú)占時(shí),后續(xù)的讀鎖和寫鎖都等待在這個(gè)event上,當(dāng)這個(gè)線程釋放寫鎖時(shí),等待在這個(gè)event上的所有讀鎖和寫鎖同時(shí)競(jìng)爭(zhēng)。waiters這變量,與event一起用,當(dāng)有等待者在等待時(shí),這個(gè)變量被設(shè)置為1,否則為0,鎖被釋放的時(shí)候,需要通過(guò)這個(gè)變量來(lái)判斷有沒(méi)有等待者從而執(zhí)行os_event_set。

與InnoDB自旋互斥鎖不同,InnoDB讀寫鎖還有wait_ex_event和recursive兩個(gè)變量。wait_ex_event也是一個(gè)InnoDB條件變量,但是它用來(lái)等待第一個(gè)寫鎖(因?yàn)閷懻?qǐng)求可能會(huì)被先前的讀請(qǐng)求堵?。?,當(dāng)先前到達(dá)的讀請(qǐng)求都讀完了,就會(huì)通過(guò)這個(gè)event來(lái)喚醒這個(gè)寫鎖的請(qǐng)求。

由于InnoDB讀寫鎖實(shí)現(xiàn)了寫鎖的遞歸,因此需要保存當(dāng)前寫鎖被哪個(gè)線程占用了,后續(xù)可以通過(guò)這個(gè)值來(lái)判斷是否是這個(gè)線程的寫鎖請(qǐng)求,如果是則加鎖成功,否則失敗,需要等待。線程的id就保存在writer_thread這個(gè)變量中。

recursive是個(gè)bool變量,用來(lái)表示當(dāng)前的讀寫鎖是否支持遞歸寫模式,在某些情況下,例如需要另外一個(gè)線程來(lái)釋放這個(gè)讀寫鎖(insert buffer需要這個(gè)功能)的時(shí)候,就不要開啟遞歸模式了。

接下來(lái),我們來(lái)詳細(xì)介紹一下lock_word的變化規(guī)則:

1. 當(dāng)有一個(gè)讀請(qǐng)求加鎖成功時(shí),lock_word原子遞減1。

2. 當(dāng)有一個(gè)寫請(qǐng)求加鎖成功時(shí),lock_word原子遞減X_LOCK_DECR。

3. 如果讀寫鎖支持遞歸寫,那么第一個(gè)遞歸寫鎖加鎖成功時(shí),lock_word依然原子遞減X_LOCK_DECR,而后續(xù)的遞歸寫鎖加鎖成功是,lock_word只是原子遞減1。

在上述的變化規(guī)則約束下,lock_word會(huì)形成以下幾個(gè)區(qū)間:

  • lock_word == X_LOCK_DECR: 表示鎖空閑,即當(dāng)前沒(méi)有線程獲得了這個(gè)鎖。
  • 0 < lock_word < X_LOCK_DECR: 表示當(dāng)前有X_LOCK_DECR – lock_word個(gè)讀鎖
  • lock_word == 0: 表示當(dāng)前有一個(gè)寫鎖
  • -X_LOCK_DECR < lock_word < 0: 表示當(dāng)前有-lock_word個(gè)讀鎖,他們還沒(méi)完成,同時(shí)后面還有一個(gè)寫鎖在等待
  • lock_word <= -X_LOCK_DECR: 表示當(dāng)前處于遞歸鎖模式,同一個(gè)線程加了2 – (lock_word + X_LOCK_DECR)次寫鎖。

另外,還可以得出以下結(jié)論

1. 由于lock_word的范圍被限制(rw_lock_validate)在(-2X_LOCK_DECR, X_LOCK_DECR]中,結(jié)合上述規(guī)則,可以推斷出,一個(gè)讀寫鎖最多能加X(jué)_LOCK_DECR個(gè)讀鎖。在開啟遞歸寫鎖的模式下,一個(gè)線程最多同時(shí)加X(jué)_LOCK_DECR+1個(gè)寫鎖。

2. 在讀鎖釋放之前,lock_word一定處于(-X_LOCK_DECR, 0)U(0, X_LOCK_DECR)這個(gè)范圍內(nèi)。

3. 在寫鎖釋放之前,lock_word一定處于(-2*X_LOCK_DECR, -X_LOCK_DECR]或者等于0這個(gè)范圍內(nèi)。

4. 只有在lock_word大于0的情況下才可以對(duì)它遞減。有一個(gè)例外,就是同一個(gè)線程需要加遞歸寫鎖的時(shí)候,lock_word可以在小于0的情況下遞減。

接下來(lái),舉個(gè)讀寫鎖加鎖的例子,方便讀者理解讀寫鎖底層加鎖的原理。假設(shè)有讀寫加鎖請(qǐng)求按照以下順序依次到達(dá):R1->R2->W1->R3->W2->W3->R4,其中W2和W3是屬于同一個(gè)線程的寫加鎖請(qǐng)求,其他所有讀寫請(qǐng)求均來(lái)自不同線程。初始化后,lock_word的值為X_LOCK_DECR(十進(jìn)制值為1048576)。R1讀加鎖請(qǐng)求首先到,其發(fā)現(xiàn)lock_word大于0,表示可以加讀鎖,同時(shí)lock_word遞減1,結(jié)果為1048575,R2讀加鎖請(qǐng)求接著來(lái)到,發(fā)現(xiàn)lock_word依然大于0,繼續(xù)加讀鎖并遞減lock_word,最終結(jié)果為1048574。注意,如果R1和R2幾乎是同時(shí)到達(dá),即使時(shí)序上是R1先請(qǐng)求,但是并不保證R1首先遞減,有可能是R2首先拿到原子操作的執(zhí)行權(quán)限。如果在R1或者R2釋放鎖之前,寫加鎖請(qǐng)求W1到來(lái),他發(fā)現(xiàn)lock_word依舊大于0,于是遞減X_LOCK_DECR,并把自己的線程id記錄在writer_thread這個(gè)變量里,再檢查lock_word的值(此時(shí)為-2),由于結(jié)果小于0,表示前面有未完成的讀加鎖請(qǐng)求,于是其等待在wait_ex_event這個(gè)條件變量上。后續(xù)的R3, W2, W3, R4請(qǐng)求發(fā)現(xiàn)lock_word小于0,則都等待在條件變量event上,并且設(shè)置waiter為1,表示有等待者。假設(shè)R1先釋放讀鎖(lock_word遞增1),R2后釋放(lock_word再次遞增1)。R2釋放后,由于lock_word變?yōu)?了,其會(huì)在wait_ex_event上調(diào)用os_event_set,這樣W3就被喚醒了,他可以執(zhí)行臨界區(qū)內(nèi)的代碼了。W3執(zhí)行完后,lock_word被恢復(fù)為X_LOCK_DECR,然后其發(fā)現(xiàn)waiter為1,表示在其后面有新的讀寫加鎖請(qǐng)求在等待,然后在event上調(diào)用os_event_set,這樣R3, W2, W3, R4同時(shí)被喚醒,進(jìn)行原子操作執(zhí)行權(quán)限爭(zhēng)搶(可以簡(jiǎn)單的理解為誰(shuí)先得到cpu調(diào)度)。假設(shè)W2首先搶到了執(zhí)行權(quán)限,其會(huì)把lock_word再次遞減為0并自己的線程id記錄在writer_thread這個(gè)變量里,當(dāng)檢查lock_word的時(shí)候,發(fā)現(xiàn)值為0,表示前面沒(méi)有讀請(qǐng)求了,于是其就進(jìn)入臨界區(qū)執(zhí)行代碼了。假設(shè)此時(shí),W3得到了cpu的調(diào)度,由于lock_word只有在大于0的情況下才能遞減,所以其遞減lock_word失敗,但是其通過(guò)對(duì)比writer_thread和自己的線程id,發(fā)現(xiàn)前面的寫鎖是自己加的,如果這個(gè)時(shí)候開啟了遞歸寫鎖,即recursive值為true,他把lock_word再次遞減X_LOCK_DECR(現(xiàn)在lock_word變?yōu)?X_LOCK_DECR了),然后進(jìn)入臨界區(qū)執(zhí)行代碼。這樣就保證了同一個(gè)線程多次加寫鎖也不發(fā)生死鎖,也就是遞歸鎖的概念。后續(xù)的R3和R4發(fā)現(xiàn)lock_word小于等于0,就直接等待在event條件變量上,并設(shè)置waiter為1。直到W2和W3都釋放寫鎖,lock_word又變?yōu)閄_LOCK_DECR,最后一個(gè)釋放的,檢查waiter變量發(fā)現(xiàn)非0,就會(huì)喚醒event上的所有等待者,于是R3和R4就可以執(zhí)行了。

讀寫鎖的核心函數(shù)函數(shù)結(jié)構(gòu)跟InnoDB自旋互斥鎖的基本相同,主要的區(qū)別就是用rw_lock_x_lock_low和rw_lock_s_lock_low替換了__sync_lock_test_and_set原子操作。rw_lock_x_lock_low和rw_lock_s_lock_low就按照上述的lock_word的變化規(guī)則來(lái)原子的改變(依然使用了__sync_lock_test_and_set)lock_word這個(gè)變量。

在MySQL 5.7中,讀寫鎖除了可以加讀鎖(Share lock)請(qǐng)求和加寫鎖(exclusive lock)請(qǐng)求外,還可以加share exclusive鎖請(qǐng)求,鎖兼容性如下:

  1. LOCK COMPATIBILITY MATRIX 
  2.  
  3.     S SX  X 
  4.  
  5. S  +  +  - 
  6.  
  7. SX +  -  - 
  8.  
  9. X  -  -  - 

按照WL#6363的說(shuō)法,是為了修復(fù)index->lock這個(gè)鎖的沖突。

輔助結(jié)構(gòu)

InnoDB同步機(jī)制中,還有很多使用的輔助結(jié)構(gòu),他們的作用主要是為了監(jiān)控方便和死鎖的預(yù)防和檢測(cè)。這里主要介紹sync array, sync thread level array和srv_error_monitor_thread。

sync array主要的數(shù)據(jù)結(jié)構(gòu)是sync_array_t,可以把他理解為一個(gè)數(shù)據(jù),數(shù)組中的元素為sync_cell_t。當(dāng)一個(gè)鎖(InnoDB自旋互斥鎖或者InnoDB讀寫鎖,下同)需要發(fā)生os_event_wait等待時(shí),就需要在sync array中申請(qǐng)一個(gè)sync_cell_t來(lái)保存當(dāng)前的信息,這些信息包括等待鎖的指針(便于死鎖檢測(cè)),在哪一個(gè)文件以及哪一行發(fā)生了等待(也就是mutex_enter, rw_lock_s_lock或者rw_lock_x_lock被調(diào)用的地方,只在debug模式下有效),發(fā)生等待的線程(便于死鎖檢測(cè))以及等待開始的時(shí)間(便于統(tǒng)計(jì)等待的時(shí)間)。當(dāng)鎖釋放的時(shí)候,就把相關(guān)聯(lián)的sync_cell_t重置為空,方便復(fù)用。sync_cell_t在sync_array_t中的個(gè)數(shù),是在初始化同步模塊時(shí)候就指定的,其個(gè)數(shù)一般為OS_THREAD_MAX_N,而OS_THREAD_MAX_N是在InnoDB初始化的時(shí)候被計(jì)算,其包括了系統(tǒng)后臺(tái)開啟的所有線程,以及max_connection指定的個(gè)數(shù),還預(yù)留了一些。由于一個(gè)線程在某一個(gè)時(shí)刻最多只能發(fā)生一個(gè)鎖等待,所以不用擔(dān)心sync_cell_t不夠用。從上面也可以看出,在每個(gè)鎖進(jìn)行等待和釋放的時(shí)候,都需要對(duì)sync array操作,因此在高并發(fā)的情況下,單一的sync array可能成為瓶頸,在MySQL 5.6中,引入了多sync array, 個(gè)數(shù)可以通過(guò)innodb_sync_array_size進(jìn)行控制,這個(gè)值默認(rèn)為1,在高并發(fā)的情況下,建議調(diào)高。

InnoDB作為一個(gè)成熟的存儲(chǔ)引擎,包含了完善的死鎖預(yù)防機(jī)制和死鎖檢測(cè)機(jī)制。在每次需要鎖等待時(shí),即調(diào)用os_event_wait之前,需要啟動(dòng)死鎖檢測(cè)機(jī)制來(lái)保證不會(huì)出現(xiàn)死鎖,從而造成無(wú)限等待。在每次加鎖成功(lock_word遞減后,函數(shù)返回之前)時(shí),都會(huì)啟動(dòng)死鎖預(yù)防機(jī)制,降低死鎖出現(xiàn)的概率。當(dāng)然,由于死鎖預(yù)防機(jī)制和死鎖檢測(cè)機(jī)制需要掃描比較多的數(shù)據(jù),算法上也有遞歸操作,所以只在debug模式下開啟。

死鎖檢測(cè)機(jī)制主要依賴sync array中保存的信息以及死鎖檢測(cè)算法來(lái)實(shí)現(xiàn)。死鎖檢測(cè)機(jī)制通過(guò)sync_cell_t保存的等待鎖指針和發(fā)生等待的線程以及教科書上的有向圖環(huán)路檢測(cè)算法來(lái)實(shí)現(xiàn),具體實(shí)現(xiàn)在sync_array_deadlock_step和sync_array_detect_deadlock中實(shí)現(xiàn),仔細(xì)研究后發(fā)現(xiàn)個(gè)小問(wèn)題,由于sync_array_find_thread函數(shù)僅僅在當(dāng)前的sync array中遍歷,當(dāng)有多個(gè)sync array時(shí)(innodb_sync_array_size > 1),如果死鎖發(fā)生在不同的sync array上,現(xiàn)有的死鎖檢測(cè)算法將無(wú)法發(fā)現(xiàn)這個(gè)死鎖。

死鎖預(yù)防機(jī)制是由sync thread level array和全局鎖優(yōu)先級(jí)共同保證的。InnoDB為了降低死鎖發(fā)生的概率,上層的每種類型的鎖都有一個(gè)優(yōu)先級(jí)。例如回滾段鎖的優(yōu)先級(jí)就比文件系統(tǒng)page頁(yè)的優(yōu)先級(jí)高,雖然兩者底層都是InnoDB互斥鎖或者InnoDB讀寫鎖。有了這個(gè)優(yōu)先級(jí),InnoDB規(guī)定,每個(gè)鎖創(chuàng)建是必須制定一個(gè)優(yōu)先級(jí),同一個(gè)線程的加鎖順序必須從優(yōu)先級(jí)高到低,即如果一個(gè)線程目前已經(jīng)加了一個(gè)低優(yōu)先級(jí)的鎖A,在釋放鎖A之前,不能再請(qǐng)求優(yōu)先級(jí)比鎖A高(或者相同)的鎖。形成死鎖需要四個(gè)必要條件,其中一個(gè)就是不同的加鎖順序,InnoDB通過(guò)鎖優(yōu)先級(jí)來(lái)降低死鎖發(fā)生的概率,但是不能完全消除。原因是可以把鎖設(shè)置為SYNC_NO_ORDER_CHECK這個(gè)優(yōu)先級(jí),這是最高的優(yōu)先級(jí),表示不進(jìn)行死鎖預(yù)防檢查,如果上層的程序員把自己創(chuàng)建的鎖都設(shè)置為這個(gè)優(yōu)先級(jí),那么InnoDB提供的這套機(jī)制將完全失效,所以要養(yǎng)成給鎖設(shè)定優(yōu)先級(jí)的好習(xí)慣。sync thread level array是一個(gè)數(shù)組,每個(gè)線程單獨(dú)一個(gè),在同步模塊初始化時(shí)分配了OS_THREAD_MAX_N個(gè),所以不用擔(dān)心不夠用。這個(gè)數(shù)組中記錄了某個(gè)線程當(dāng)前鎖擁有的所有鎖,當(dāng)新加了一個(gè)鎖B時(shí),需要掃描一遍這個(gè)數(shù)組,從而確定目前線程所持有的鎖的優(yōu)先級(jí)都比鎖B高。

最后,我們來(lái)講講srv_error_monitor_thread這個(gè)線程。這是一個(gè)后臺(tái)線程,在InnoDB啟動(dòng)的時(shí)候啟動(dòng),每隔1秒鐘執(zhí)行一下指定的操作。跟同步模塊相關(guān)的操作有兩點(diǎn),去除無(wú)限等待的鎖和報(bào)告長(zhǎng)時(shí)間等待的異常鎖。

去除無(wú)線等待的鎖,如上文所屬,就是sync_arr_wake_threads_if_sema_free這個(gè)函數(shù)。這個(gè)函數(shù)通過(guò)遍歷sync array,如果發(fā)現(xiàn)鎖已經(jīng)可用(sync_arr_cell_can_wake_up),但是依然有等待者,則直接調(diào)用os_event_set把他們喚醒。這個(gè)函數(shù)是為了解決由于cpu亂序執(zhí)行或者編譯器指令重排導(dǎo)致鎖無(wú)限等待的問(wèn)題,但是可以通過(guò)內(nèi)存屏障技術(shù)來(lái)避免,所以可以去掉。

報(bào)告長(zhǎng)時(shí)間等待的異常鎖,通過(guò)sync_cell_t里面記錄的鎖開始等待時(shí)間,我們可以很方便的統(tǒng)計(jì)鎖等待發(fā)生的時(shí)間。在目前的實(shí)現(xiàn)中,當(dāng)鎖等待超過(guò)240秒的時(shí)候,就會(huì)在錯(cuò)誤日志中看到信息。如果同一個(gè)鎖被檢測(cè)到等到超過(guò)600秒且連續(xù)10次被檢測(cè)到,則InnoDB會(huì)通過(guò)assert來(lái)自殺。。。相信當(dāng)做運(yùn)維DBA的同學(xué)一定看到過(guò)如下的報(bào)錯(cuò):

  1. InnoDB: Warning: a long semaphore wait: 
  2.  
  3. --Thread 139774244570880 has waited at log0read.h line 765 for 241.00 seconds the semaphore: 
  4.  
  5. Mutex at 0x30c75ca0 created file log0read.h line 522, lock var 1 
  6.  
  7. Last time reserved in file /home/yuhui.wyh/mysql/storage/innobase/include/log0read.h line 765, waiters flag 1 
  8.  
  9. InnoDB: ###### Starts InnoDB Monitor for 30 secs to print diagnostic info: 
  10.  
  11. InnoDB: Pending preads 0, pwrites 0 

一般出現(xiàn)這種錯(cuò)誤都是pread或者pwrite長(zhǎng)時(shí)間不返回,導(dǎo)致鎖超時(shí)。至于pread或者pwrite長(zhǎng)時(shí)間不返回的root cause常常是有很多的讀寫請(qǐng)求在極短的時(shí)間內(nèi)到達(dá)導(dǎo)致磁盤扛不住或者磁盤已經(jīng)壞了。。。

總結(jié)

 

本文詳細(xì)介紹了原子操作,條件變量,互斥鎖以及讀寫鎖在InnoDB引擎中的實(shí)現(xiàn)。原子操作由于其能減少不必要的用戶態(tài)和內(nèi)核態(tài)的切換以及更精簡(jiǎn)的cpu指令被廣泛的應(yīng)用到InnoDB自旋互斥鎖和InnoDB讀寫鎖中。InnoDB條件變量使用更加方便,但是一定要注意條件通知必須在條件等待之后,否則會(huì)有無(wú)限等待發(fā)生。InnoDB自旋互斥鎖加鎖和解鎖過(guò)程雖然復(fù)雜但是都是必須的操作。InnoDB讀寫鎖神奇的lock_word控制方法給我們留下了深刻影響。正因?yàn)镮nnoDB底層同步機(jī)制的穩(wěn)定、高效,MySQL在我們的服務(wù)器上才能運(yùn)行的如此穩(wěn)定。 

責(zé)任編輯:龐桂玉 來(lái)源: 數(shù)據(jù)庫(kù)開發(fā)
相關(guān)推薦

2016-09-20 15:21:35

LinuxInnoDBMysql

2017-12-14 21:30:05

MySQLInnoDBIO子系統(tǒng)

2019-05-27 14:40:43

Java同步機(jī)制多線程編程

2011-11-23 10:09:19

Java線程機(jī)制

2012-07-27 10:02:39

C#

2012-07-09 09:25:13

ibmdw

2024-07-05 08:32:36

2024-06-28 08:45:58

2009-08-12 13:37:01

Java synchr

2010-03-15 16:31:34

Java多線程

2025-03-31 00:01:12

2019-08-22 14:30:21

技術(shù)Redis設(shè)計(jì)

2021-10-08 20:30:12

ZooKeeper選舉機(jī)制

2019-06-11 16:11:16

MySQLMyISAMInnoDB

2010-11-23 11:27:53

MySQL MyISA

2010-05-11 15:06:24

MySQL MyISA

2010-05-21 16:23:52

MySQL MyISA

2019-11-22 18:52:31

進(jìn)程同步機(jī)制編程語(yǔ)言

2024-07-08 12:51:05

2012-11-26 10:17:44

InnoDB
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 97精品国产| 九九亚洲| 久久免费精品视频 | 国产精品久久a | 国产精品一区二区视频 | 久久不卡 | 久久99视频 | 天天操天天射天天 | a级网站| 毛片免费在线观看 | 在线国产一区 | 综合五月婷 | 夜夜骑首页 | 黄色免费网站在线看 | 中文在线日韩 | 久久久国产一区二区三区四区小说 | 成人毛片一区二区三区 | 999国产视频 | av一区二区三区 | 国产精品免费一区二区三区 | 国产一区二区三区高清 | 中文字幕一页二页 | 最大av在线| 密桃av | 欧美国产日韩一区二区三区 | 亚洲免费视频在线观看 | 国产精品毛片一区二区在线看 | 欧美成视频 | 日韩高清一区 | 亚洲成人av一区二区 | 四虎午夜剧场 | 精品综合久久久 | 国产亚洲一区二区三区在线观看 | 国产高清一区 | 天堂综合网久久 | 国产91综合一区在线观看 | 麻豆精品一区二区三区在线观看 | 精品久久精品 | 在线国产小视频 | 精品国产乱码久久久久久影片 | 一区二区三区在线观看免费视频 |