???????數(shù)據(jù)庫(kù)鎖的設(shè)計(jì)泳炉,目的是為了處理并發(fā)問(wèn)題憾筏。作為多用戶共享的資源,當(dāng)出現(xiàn)并發(fā)問(wèn)題的時(shí)候花鹅,數(shù)據(jù)庫(kù)需要合理地控制資源的訪問(wèn)規(guī)則氧腰。而鎖就是用來(lái)實(shí)現(xiàn)這些訪問(wèn)規(guī)則的重要數(shù)據(jù)結(jié)構(gòu)。
???????根據(jù)加鎖的范圍刨肃,MySQL里面的鎖大致可以分成全局鎖古拴、表級(jí)鎖和行鎖三類。
全局鎖
???????全局鎖真友,就是對(duì)整個(gè)數(shù)據(jù)庫(kù)實(shí)例加鎖黄痪。MySQL提供了一個(gè)加全局讀鎖的方法,命令就是FTWRL盔然,即Flush tables with read lock桅打。當(dāng)需要讓整個(gè)數(shù)據(jù)庫(kù)處于只讀狀態(tài)的時(shí)候,可以使用這個(gè)命令愈案,之后其他線程的數(shù)據(jù)更新語(yǔ)句(數(shù)據(jù)的增刪改)挺尾、數(shù)據(jù)定義語(yǔ)句(建表、修改表結(jié)構(gòu)等)和更新類事務(wù)的提交語(yǔ)句都會(huì)被阻塞站绪。
???????全局鎖的典型使用場(chǎng)景遭铺,做全庫(kù)邏輯備份。也就是把整個(gè)庫(kù)每個(gè)表都select出來(lái)存成文本崇众。以前的做法就是通過(guò)FTWRL確保不會(huì)有其他線程對(duì)數(shù)據(jù)庫(kù)更新掂僵,然后對(duì)整個(gè)庫(kù)做備份航厚。在備份過(guò)程中整個(gè)庫(kù)完全處于只讀狀態(tài)顷歌,如果在主庫(kù)上做備份,則在備份期間不能執(zhí)行更新幔睬,業(yè)務(wù)基本上就得停擺眯漩;如果在從庫(kù)上備份,則在備份期間從庫(kù)不能執(zhí)行主庫(kù)同步過(guò)來(lái)的binlog,會(huì)導(dǎo)致主從延遲赦抖。也就是說(shuō)舱卡,如果不加鎖的話,備份系統(tǒng)備份得到的庫(kù)不是一個(gè)邏輯時(shí)間點(diǎn)队萤,這個(gè)視圖是邏輯不一致的轮锥。解決該問(wèn)題的方法,就是在可重復(fù)讀隔離級(jí)別下開(kāi)啟一個(gè)事務(wù)要尔。
???????官方自帶的邏輯備份工具是mysqldump舍杜。當(dāng)mysqldump使用參數(shù)-single-transction的時(shí)候,導(dǎo)數(shù)據(jù)之前就會(huì)啟動(dòng)一個(gè)事務(wù)赵辕,來(lái)確保拿到一致性視圖既绩。而由于MVCC的支持,這個(gè)過(guò)程中是可以正常更新的还惠。該功能雖好饲握,但有前提,前提就是引擎要支持這個(gè)隔離級(jí)別蚕键,例如:對(duì)于MyISAM這種不支持事務(wù)的引擎來(lái)說(shuō)救欧,就需要使用FTWRL命令,不然在備份過(guò)程中有更新锣光,總是能取到最新的數(shù)據(jù)颜矿,那么就破壞了備份的一致性。這也是DBA要求業(yè)務(wù)開(kāi)發(fā)人員使用InnoDB替代MyISAM的原因之一嫉晶。
???????其實(shí)骑疆,讓全庫(kù)處于只讀狀態(tài)還有一種方法:使用set global readonly=true。但是還是建議使用FTWRL方式替废。原因如下:
- 在某些系統(tǒng)中箍铭,readonly的值會(huì)被用來(lái)做其他邏輯,比如用來(lái)判斷一個(gè)庫(kù)是主庫(kù)還是備庫(kù)椎镣。因此诈火,修改global變量的方式影響面更大,故不建議用状答。
- 在異常處理機(jī)制上有差異冷守。如果執(zhí)行FTWRL命令之后,由于客戶端發(fā)生異常斷開(kāi)惊科,那么mysql會(huì)自動(dòng)釋放這個(gè)全局鎖拍摇,這個(gè)庫(kù)回到可以正常更新的狀態(tài)。而將這個(gè)庫(kù)設(shè)置為readonly之后馆截,如果客戶端發(fā)生異常充活,則數(shù)據(jù)庫(kù)就會(huì)一直保持readonly狀態(tài)蜂莉,這樣會(huì)導(dǎo)致整個(gè)庫(kù)長(zhǎng)時(shí)間處于不可寫(xiě)狀態(tài),風(fēng)險(xiǎn)較高混卵。
表級(jí)鎖
???????MySQL里面表級(jí)別的鎖有兩種:表鎖和元數(shù)據(jù)鎖映穗。
表鎖
???????表鎖的語(yǔ)法是lock tables ... read/write∧凰妫可以用unlock tables主動(dòng)釋放鎖蚁滋,也可以在客戶端斷開(kāi)的時(shí)候自動(dòng)釋放。但注意lock tables語(yǔ)法除了會(huì)限制別的線程的讀寫(xiě)外赘淮,也限定了本線程接下來(lái)的操作對(duì)象枢赔。例如:線程A中執(zhí)行l(wèi)ock tables t1 read,t2 write;這個(gè)語(yǔ)句拥知,則其他線程寫(xiě)t1踏拜、讀寫(xiě)t2的語(yǔ)句都會(huì)被阻塞。同時(shí)低剔,線程A在執(zhí)行unlock tables之前速梗,也只能執(zhí)行讀t1、讀寫(xiě)t2的操作襟齿。連寫(xiě)t1都不允許姻锁,自然也不能訪問(wèn)其他表。
元數(shù)據(jù)鎖(MDL猜欺,metadata lock)
???????MDL不需要顯式使用位隶,在訪問(wèn)一個(gè)表的時(shí)候會(huì)被自動(dòng)加上。MDL的作用是保證讀寫(xiě)的正確性开皿〗Щ疲可以想象一下,如果一個(gè)查詢正在遍歷一個(gè)表中的數(shù)據(jù)赋荆,而執(zhí)行期間另一個(gè)線程對(duì)這個(gè)表結(jié)構(gòu)做變更笋妥,刪了一列,那么查詢線程拿到的結(jié)果跟表結(jié)構(gòu)對(duì)不上窄潭,肯定是不行的春宣。因此,在MySQL5.5版本中引入了MDL嫉你,當(dāng)對(duì)一個(gè)表做增刪改查操作的時(shí)候月帝,加MDL讀鎖;當(dāng)要對(duì)表做結(jié)構(gòu)變更操作的時(shí)候幽污,加MDL寫(xiě)鎖嚷辅。
- 讀鎖之間不互斥,因此可以有多個(gè)線程同時(shí)對(duì)一張表增刪改查油挥;
- 讀寫(xiě)鎖之間潦蝇、寫(xiě)鎖之間是互斥的款熬,用來(lái)保證變更表結(jié)構(gòu)操作的安全性深寥。因此攘乒,如果有兩個(gè)線程要同時(shí)給一個(gè)表加字段,其中一個(gè)要等另一個(gè)執(zhí)行完才能開(kāi)始執(zhí)行惋鹅。
行鎖
???????MySQL的行鎖是在引擎層由各個(gè)引擎自己實(shí)現(xiàn)的则酝,但并不是所有的引擎都支持行鎖,例如:MyISAM引擎就不支持行鎖闰集。不支持行鎖意味著并發(fā)控制只能使用表鎖沽讹,對(duì)于這種引擎的表,同一張表上任何時(shí)刻只能有一個(gè)更新在執(zhí)行武鲁,這就會(huì)影響到業(yè)務(wù)并發(fā)度爽雄。InnoDB是支持行鎖的,這是MyISAM被InnoDB替代的重要原因之一沐鼠。
???????行鎖挚瘟,顧名思義,就是針對(duì)數(shù)據(jù)表中行記錄的鎖饲梭。這也很好理解乘盖,比如事務(wù)A更新了一行,而這時(shí)候事務(wù)B也要更新同一行憔涉,則必須等事務(wù)A的操作完成后才能進(jìn)行更新订框。
兩階段鎖
???????在InnoDB事務(wù)中,行鎖是在需要的時(shí)候才加上的兜叨,但并不是不需要了就立刻釋放穿扳,而是要等到事務(wù)結(jié)束才釋放。這就是兩階段鎖協(xié)議国旷。在這個(gè)協(xié)議下纵揍,如果事務(wù)中需要鎖多行,要把最可能造成鎖沖突议街、最可能影響并發(fā)度的鎖盡量往后放泽谨。這樣做可以最大程度地減少了事務(wù)之間的鎖等待,提升了并發(fā)度特漩。
死鎖和死鎖檢測(cè)
???????當(dāng)并發(fā)系統(tǒng)中不同線程出現(xiàn)循環(huán)資源依賴吧雹,涉及的線程都在等待別的線程釋放資源時(shí),就會(huì)導(dǎo)致這幾個(gè)線程都進(jìn)入無(wú)限等待的狀態(tài)涂身,稱為死鎖雄卷。當(dāng)出現(xiàn)死鎖以后,有兩種策略:
- 直接進(jìn)入等待蛤售,直到超時(shí)丁鹉。這個(gè)超時(shí)時(shí)間可以通過(guò)參數(shù)innodb_lock_wait_timeout來(lái)設(shè)置妒潭。
- 發(fā)起死鎖檢測(cè),發(fā)現(xiàn)死鎖后揣钦,主動(dòng)回滾死鎖鏈條中的某個(gè)事務(wù)雳灾,讓其他事務(wù)得以繼續(xù)執(zhí)行。將參數(shù)inndb_deadlock_detect設(shè)置為on冯凹,表示開(kāi)啟這個(gè)邏輯谎亩。
???????在InnoDB中,innodb_lock_wait_timeout的默認(rèn)值是50s宇姚,意味著如果采用第一個(gè)策略,當(dāng)出現(xiàn)死鎖以后匈庭,第一個(gè)被鎖住的線程要過(guò)50才會(huì)超時(shí)退出,然后其他線程才有可能繼續(xù)執(zhí)行浑劳。對(duì)于在線服務(wù)來(lái)說(shuō)阱持,這個(gè)等待時(shí)間往往是無(wú)法接受的。但是魔熏,如果我們把這個(gè)值設(shè)得特別的小衷咽,當(dāng)出現(xiàn)死鎖的時(shí)候,確實(shí)很快就可以解開(kāi)道逗,但是如果不是死鎖兵罢,而是簡(jiǎn)單的鎖等待,那就“誤傷”了這個(gè)線程滓窍。
???????如果采用的是第二種策略卖词,即:主動(dòng)死鎖檢測(cè),而且innodb_deadlock_detect的默認(rèn)值本身就是on吏夯。主動(dòng)死鎖檢測(cè)在發(fā)死鎖的時(shí)候此蜈,是能夠快速發(fā)現(xiàn)并進(jìn)行處理的,但是這個(gè)策略也有額外的負(fù)擔(dān)噪生。因?yàn)檫@個(gè)過(guò)程是這樣的裆赵,每當(dāng)一個(gè)事務(wù)被鎖住的時(shí)候,就要看看它所依賴的線程有沒(méi)有被別人鎖住跺嗽,如此循環(huán)战授,最后判斷是否出現(xiàn)了循環(huán)等待,也就是死鎖桨嫁。
???????我們可以想想植兰,每個(gè)新來(lái)的被堵住的線程,都要判斷會(huì)不會(huì)由于自己的加入導(dǎo)致了死鎖璃吧,這是一個(gè)時(shí)間復(fù)雜度是O(n)的操作楣导。假設(shè)有1000個(gè)并發(fā)線程要同時(shí)更新同一行,那么死鎖檢測(cè)操作就是100萬(wàn)這個(gè)量級(jí)的畜挨。雖然最終檢測(cè)結(jié)果是沒(méi)有死鎖筒繁,但是這個(gè)期間要消耗大量的CPU資源噩凹。因此,就會(huì)出現(xiàn)cpu利用率很高毡咏,但是每秒?yún)s執(zhí)行不了幾個(gè)事務(wù)驮宴。
???????接下來(lái)討論如何去解決由這種熱點(diǎn)行更新導(dǎo)致的性能問(wèn)題,這個(gè)問(wèn)題的癥結(jié)在于血当,死鎖檢測(cè)要消耗大量的CPU資源幻赚。解決方法如下: - 若能確保這個(gè)業(yè)務(wù)一定不會(huì)出現(xiàn)死鎖禀忆,可以臨時(shí)把死鎖檢測(cè)關(guān)掉臊旭。但是這種操作本身帶有一定的風(fēng)險(xiǎn),因?yàn)闃I(yè)務(wù)設(shè)計(jì)的時(shí)候一般不會(huì)把死鎖當(dāng)作一個(gè)嚴(yán)重錯(cuò)誤箩退,畢竟出現(xiàn)死鎖了离熏,就回滾,然后通過(guò)業(yè)務(wù)重試一般就沒(méi)問(wèn)題了戴涝,這是業(yè)務(wù)無(wú)損的滋戳。而關(guān)掉死鎖檢測(cè)意味著可能會(huì)出現(xiàn)大量的超時(shí),這是業(yè)務(wù)有損的啥刻。
- 控制并發(fā)度奸鸯。如果并發(fā)能夠控制住,比如用一行同時(shí)最多只有10個(gè)線程在更新可帽,那么死鎖檢測(cè)的成本很低娄涩,就不會(huì)出現(xiàn)這個(gè)問(wèn)題一個(gè)直接的想法就是,在客戶端做并發(fā)控制映跟,但是這個(gè)方法不可行蓄拣,因?yàn)榭蛻舳撕芏唷W霾l(fā)控制要做在數(shù)據(jù)庫(kù)服務(wù)器努隙,可以使用中間件球恤,也可以修改mysql的源碼,基本思路就是荸镊,對(duì)于相同行的更新咽斧,在進(jìn)入引擎之前排隊(duì)。