鎖在平時的工作中接觸的比較少(InnDB幫我們做了不少事),所以這里在Java3y 的文章上加上了自己的理解络断。本文講解了鎖的分類或油、使用場景,當工作中碰到鎖的問題能夠有一個清楚的認識和判斷交胚。
1份汗、鎖的分類
鎖的相關(guān)知識又跟存儲引擎椎镣,索引诈火,事務(wù)的隔離級別都是關(guān)聯(lián)的….
2、什么需要學(xué)習(xí)數(shù)據(jù)庫鎖知識
不少人在開發(fā)的時候状答,應(yīng)該很少會注意到這些鎖的問題冷守,也很少會給程序加鎖(除了庫存這些對數(shù)量準確性要求極高的情況下)
一般也就聽過常說的樂觀鎖和悲觀鎖,了解過基本的含義之后就沒了惊科。
定心丸:即使我們不會這些鎖知識教沾,我們的程序在一般情況下還是可以跑得好好的。因為這些鎖數(shù)據(jù)庫隱式幫我們加了译断。
- 1授翻、對于
UPDATE、DELETE孙咪、INSERT
語句堪唐,InnoDB會自動給涉及數(shù)據(jù)集加排他鎖(X) - 2、MyISAM在執(zhí)行查詢語句SELECT前翎蹈,會自動給涉及的所有表加讀鎖淮菠,在執(zhí)行更新操作(UPDATE、DELETE荤堪、INSERT等)前合陵,會自動給涉及的表加寫鎖,這個過程并不需要用戶干預(yù)
只會在某些特定的場景下才需要手動加鎖澄阳,學(xué)習(xí)數(shù)據(jù)庫鎖知識就是為了:
- 1拥知、能讓我們在特定的場景下派得上用場;
- 2碎赢、更好把控自己寫的程序低剔;
- 3、在跟別人聊數(shù)據(jù)庫技術(shù)的時候可以搭上幾句話肮塞;
- 4襟齿、構(gòu)建自己的知識庫體系!在面試的時候不虛枕赵。
3猜欺、表鎖簡單介紹
首先,從鎖的粒度拷窜,我們可以分成兩大類:
表鎖
- 開銷小开皿,加鎖快钓试;不會出現(xiàn)死鎖;鎖定力度大副瀑,發(fā)生鎖沖突概率高弓熏,并發(fā)度最低;
行鎖
- 開銷大糠睡,加鎖慢挽鞠;會出現(xiàn)死鎖;鎖定粒度小狈孔,發(fā)生鎖沖突的概率低信认,并發(fā)度高。
不同的存儲引擎支持的鎖粒度是不一樣的:
- InnoDB行鎖和表鎖都支持均抽!
- MyISAM只支持表鎖嫁赏!
InnoDB只有通過索引條件檢索數(shù)據(jù)才使用行級鎖,否則油挥,InnoDB將使用表鎖潦蝇,也就是說,InnoDB的行鎖是基于索引的深寥!
3.1攘乒、表鎖下又分為兩種模式
表讀鎖(Table Read Lock
)和表寫鎖(Table Write Lock
)。
- 從下圖可以清晰看到惋鹅,在表讀鎖和表寫鎖的環(huán)境下:讀讀不阻塞则酝,讀寫阻塞,寫寫阻塞闰集!
- 讀讀不阻塞:當前用戶在讀數(shù)據(jù)沽讹,其他的用戶也在讀數(shù)據(jù),不會加鎖
- 讀寫阻塞:當前用戶在讀數(shù)據(jù)武鲁,其他的用戶不能修改當前用戶讀的數(shù)據(jù)爽雄,會加鎖!
- 寫寫阻塞:當前用戶在修改數(shù)據(jù)洞坑,其他的用戶不能修改當前用戶正在修改的數(shù)據(jù)盲链,會加鎖!
- 如果某個進程想要獲取讀鎖本慕,同時另外一個進程想要獲取寫鎖排拷。在mysql里邊,寫鎖是優(yōu)先于讀鎖的锅尘!
- 寫鎖和讀鎖優(yōu)先級的問題是可以通過參數(shù)調(diào)節(jié)的:
max_write_lock_count
和low-priority-updates
值得注意的是:
The LOCAL modifier enables nonconflicting INSERT statements (concurrent inserts) by other sessions to execute while the lock is held. (See Section 8.11.3, “Concurrent Inserts”.) However, READ LOCAL cannot be used if you are going to manipulate the database using processes external to the server while you hold the lock. For InnoDB tables, READ LOCAL is the same as READ
-
MyISAM可以支持查詢和插入操作的并發(fā)進行监氢〔颊幔可以通過系統(tǒng)變量
concurrent_insert
來指定哪種模式,在MyISAM中它默認是:如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行)浪腐,MyISAM允許在一個進程讀表的同時纵揍,另一個進程從表尾插入記錄。 - 但是InnoDB存儲引擎是不支持的议街!
參考資料:
4泽谨、行鎖細講
上邊簡單講解了表鎖的相關(guān)知識,我們使用Mysql一般是使用InnoDB存儲引擎的特漩。InnoDB和MyISAM有兩個本質(zhì)的區(qū)別:
- InnoDB支持行鎖
- InnoDB支持事務(wù)
從上面也說了:我們是很少手動加表鎖的吧雹。表鎖對我們程序員來說幾乎是透明的,即使InnoDB不走索引涂身,加的表鎖也是自動的雄卷!
我們應(yīng)該更加關(guān)注行鎖的內(nèi)容,因為InnoDB一大特性就是支持行鎖蛤售!
InnoDB實現(xiàn)了以下兩種類型的行鎖丁鹉。
- 共享鎖(S鎖):允許一個事務(wù)去讀一行,阻止其他事務(wù)獲得相同數(shù)據(jù)集的排他鎖悴能。
- 也叫做讀鎖:讀鎖是共享的鳄炉,多個客戶可以同時讀取同一個資源,但不允許其他客戶修改搜骡。
- 排他鎖(X鎖):允許獲得排他鎖的事務(wù)更新數(shù)據(jù)拂盯,阻止其他事務(wù)取得相同數(shù)據(jù)集的共享讀鎖和排他寫鎖。
- 也叫做寫鎖:寫鎖是排他的记靡,寫鎖會阻塞其他的寫鎖和讀鎖谈竿。
看完上面的有沒有發(fā)現(xiàn),在一開始所說的:X鎖摸吠,S鎖空凸,讀鎖,寫鎖寸痢,共享鎖呀洲,排它鎖其實總共就兩個鎖,只不過它們有多個名字罷了~~~
Intention locks do not block anything except full table requests (for example, LOCK TABLES … WRITE). The main purpose of intention locks is to show that someone is locking a row, or going to lock a row in the table.
另外啼止,為了允許行鎖和表鎖共存道逗,實現(xiàn)多粒度鎖機制,InnoDB還有兩種內(nèi)部使用的意向鎖(Intention Locks
)献烦,這兩種意向鎖都是表鎖:
- 意向共享鎖(IS):事務(wù)打算給數(shù)據(jù)行加行共享鎖滓窍,事務(wù)在給一個數(shù)據(jù)行加共享鎖前必須先取得該表的IS鎖。
- 意向排他鎖(IX):事務(wù)打算給數(shù)據(jù)行加行排他鎖巩那,事務(wù)在給一個數(shù)據(jù)行加排他鎖前必須先取得該表的IX鎖吏夯。
- 意向鎖也是數(shù)據(jù)庫隱式幫我們做了此蜈,不需要程序員操心!
參考資料:
https://www.zhihu.com/question/51513268
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
5噪生、MVCC和事務(wù)的隔離級別
數(shù)據(jù)庫事務(wù)有不同的隔離級別裆赵,不同的隔離級別對鎖的使用是不同的,鎖的應(yīng)用最終導(dǎo)致不同事務(wù)的隔離級別
MVCC(Multi-Version Concurrency Control)
多版本并發(fā)控制跺嗽,可以簡單地認為:MVCC就是行級鎖的一個變種(升級版)战授。
- 事務(wù)的隔離級別就是通過鎖的機制來實現(xiàn),只不過隱藏了加鎖細節(jié)
- 在表鎖中我們讀寫是阻塞的抛蚁,基于提升并發(fā)性能的考慮陈醒,MVCC一般讀寫是不阻塞的(所以說MVCC很多情況下避免了加鎖的操作)
-
MVCC
實現(xiàn)的讀寫不阻塞正如其名:多版本并發(fā)控制:通過一定機制生成一個數(shù)據(jù)請求時間點的一致性數(shù)據(jù)快照(Snapshot),并用這個快照來提供一定級別(語句級或事務(wù)級)的一致性讀取瞧甩。從用戶的角度來看钉跷,好像是數(shù)據(jù)庫可以提供同一數(shù)據(jù)的多個版本。
快照有兩個級別:
- 語句級:針對于
Read committed
隔離級別肚逸; - 事務(wù)級別:針對于
Repeatable read
隔離級別爷辙。
我們在初學(xué)的時候已經(jīng)知道,事務(wù)的隔離級別有4種:
1朦促、Read uncommitted:會出現(xiàn)臟讀膝晾,不可重復(fù)讀,幻讀务冕;
2血当、Read committed:會出現(xiàn)不可重復(fù)讀,幻讀禀忆;
3臊旭、Repeatable read:會出現(xiàn)幻讀(但在Mysql實現(xiàn)的Repeatable read配合gap鎖不會出現(xiàn)幻讀!)箩退;
4离熏、Serializable:串行,避免以上的情況戴涝!
Read uncommitted
會出現(xiàn)的現(xiàn)象--->臟讀:一個事務(wù)讀取到另外一個事務(wù)未提交的數(shù)據(jù)
- 例子:A向B轉(zhuǎn)賬滋戳,A執(zhí)行了轉(zhuǎn)賬語句,但A還沒有提交事務(wù)啥刻,B讀取數(shù)據(jù)奸鸯,發(fā)現(xiàn)自己賬戶錢變多了!B跟A說郑什,我已經(jīng)收到錢了府喳。A回滾事務(wù)【rollback】,等B再查看賬戶的錢時蘑拯,發(fā)現(xiàn)錢并沒有多钝满。
- 出現(xiàn)臟讀的本質(zhì)就是因為操作(修改)完該數(shù)據(jù)就立馬釋放掉鎖,導(dǎo)致讀的數(shù)據(jù)就變成了無用的或者是錯誤的數(shù)據(jù)申窘。
Read committed
避免臟讀的做法其實很簡單:
- 就是把釋放鎖的位置調(diào)整到事務(wù)提交之后弯蚜,此時在事務(wù)提交前,其他進程是無法對該行數(shù)據(jù)進行讀取的剃法,包括任何操作
但Read committed
出現(xiàn)的現(xiàn)象--->不可重復(fù)讀:一個事務(wù)讀取到另外一個事務(wù)已經(jīng)提交的數(shù)據(jù)碎捺,也就是說一個事務(wù)可以看到其他事務(wù)所做的修改
注:A查詢數(shù)據(jù)庫得到數(shù)據(jù),B去修改數(shù)據(jù)庫的數(shù)據(jù)贷洲,導(dǎo)致A多次查詢數(shù)據(jù)庫的結(jié)果都不一樣【危害:A每次查詢的結(jié)果都是受B的影響的收厨,那么A查詢出來的信息就沒有意思了】
上面也說了,Read committed
是語句級別的快照优构!每次讀取的都是當前最新的版本诵叁!
Repeatable read
避免不可重復(fù)讀是事務(wù)級別的快照!每次讀取的都是當前事務(wù)的版本钦椭,即使被修改了拧额,也只會讀取當前事務(wù)版本的數(shù)據(jù)。
呃…如果還是不太清楚彪腔,我們來看看InnoDB的MVCC是怎么樣的吧(摘抄《高性能MySQL》)
至于虛讀(幻讀):是指在一個事務(wù)內(nèi)讀取到了別的事務(wù)插入的數(shù)據(jù)侥锦,導(dǎo)致前后讀取不一致。
- 注:和不可重復(fù)讀類似德挣,但虛讀(幻讀)會讀到其他事務(wù)的插入的數(shù)據(jù)恭垦,導(dǎo)致前后讀取不一致
- MySQL的
Repeatable read
隔離級別加上GAP間隙鎖已經(jīng)處理了幻讀了。
參考資料:
擴展閱讀:
6格嗅、樂觀鎖和悲觀鎖
無論是Read committed
還是Repeatable read
隔離級別番挺,都是為了解決讀寫沖突的問題。
單純在Repeatable read
隔離級別下我們來考慮一個問題:
此時吗浩,用戶李四的操作就丟失掉了:
- 丟失更新:一個事務(wù)的更新覆蓋了其它事務(wù)的更新結(jié)果建芙。
ps:暫時沒有想到比較好的例子來說明更新丟失的問題,雖然上面的例子也是更新丟失懂扼,但一定程度上是可接受的..不知道有沒有人能想到不可接受的更新丟失例子呢…
解決的方法:
- 使用Serializable隔離級別禁荸,事務(wù)是串行執(zhí)行的!
- 樂觀鎖
- 悲觀鎖
1阀湿、樂觀鎖是一種思想赶熟,具體實現(xiàn)是,表中有一個版本字段陷嘴,第一次讀的時候映砖,獲取到這個字段。處理完業(yè)務(wù)邏輯開始更新的時候灾挨,需要再次查看該字段的值是否和第一次的一樣邑退。如果一樣更新竹宋,反之拒絕。之所以叫樂觀地技,因為這個模式?jīng)]有從數(shù)據(jù)庫加鎖蜈七,等到更新的時候再判斷是否可以更新。實現(xiàn)數(shù)據(jù)版本有兩種方式莫矗,第一種是使用版本號飒硅,第二種是使用時間戳。****(如何實現(xiàn)樂觀鎖)
2作谚、悲觀鎖是數(shù)據(jù)庫層面加鎖三娩,都會阻塞去等待鎖。
所以妹懒,按照上面的例子雀监。我們使用悲觀鎖的話其實很簡單(手動加行鎖就行了):
select * from xxxx for update
在select
語句后邊加了for update
相當于加了排它鎖(寫鎖),加了寫鎖以后彬伦,其他的事務(wù)就不能對它修改了滔悉!需要等待當前事務(wù)修改完之后才可以修改.
也就是說,如果張三使用select … for update
单绑,李四就無法對該條記錄修改了~
樂觀鎖不是數(shù)據(jù)庫層面上的鎖回官,是需要自己手動去加的鎖。一般我們添加一個版本字段來實現(xiàn):
具體過程是這樣的:
張三::select * from table
--->會查詢出記錄出來搂橙,同時會有一個version字段
李四:select * from table
--->會查詢出記錄出來歉提,同時會有一個version字段
李四對這條記錄做修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
,判斷之前查詢到的version
與現(xiàn)在的數(shù)據(jù)的version
進行比較区转,同時會更新version字段
此時數(shù)據(jù)庫記錄如下:
張三也對這條記錄修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
苔巨,但失敗了!因為當前數(shù)據(jù)庫中的版本跟查詢出來的版本不一致废离!
參考資料:
https://zhuanlan.zhihu.com/p/31537871---什么是悲觀鎖和樂觀鎖
https://www.zhihu.com/question/27876575---樂觀鎖和 MVCC 的區(qū)別侄泽?
7、間隙鎖GAP
當我們用范圍條件檢索數(shù)據(jù)而不是相等條件檢索數(shù)據(jù)蜻韭,并請求共享或排他鎖時悼尾,InnoDB會給符合范圍條件的已有數(shù)據(jù)記錄的索引項加鎖;對于鍵值在條件范圍內(nèi)但并不存在的記錄肖方,叫做“間隙(GAP)”闺魏。InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖俯画。
值得注意的是:間隙鎖只會在Repeatable read
隔離級別下使用~
事務(wù)加鎖后鎖住的是表記錄的某一個區(qū)間析桥,當表的相鄰ID之間出現(xiàn)空隙則會形成一個區(qū)間,遵循左開右閉原則。
比如下面的表里面的數(shù)據(jù)ID 為 1,4,5,7,10 ,那么會形成以下幾個間隙區(qū)間泡仗,-n-1區(qū)間埋虹,1-4區(qū)間,7-10區(qū)間沮焕,10-n區(qū)間 (-n代表負無窮大吨岭,n代表正無窮大)
間隙鎖出現(xiàn)的條件:范圍查詢并且查詢未命中記錄拉宗,查詢條件必須命中索引峦树、間隙鎖只會出現(xiàn)在REPEATABLE_READ(重復(fù)讀)的事務(wù)級別中。
例如:對應(yīng)上圖的表執(zhí)行select * from user_info where id>1 and id<4(這里的id是唯一索引) 旦事,這個SQL查詢不到對應(yīng)的記錄魁巩,那么此時會使用間隙鎖。
間隙鎖作用姐浮?
防止幻讀問題谷遂,事務(wù)并發(fā)的時候,如果沒有間隙鎖卖鲤,就會發(fā)生如下圖的問題肾扰,在同一個事務(wù)里,A事務(wù)的兩次查詢出的結(jié)果會不一樣蛋逾。
例子:假如emp表中只有101條記錄集晚,其empid的值分別是1,2,…,100,101
Select * from emp where empid > 100 for update;
上面是一個范圍查詢,InnoDB不僅會對符合條件的empid值為101的記錄加鎖区匣,也會對empid大于101(這些記錄并不存在)的“間隙”加鎖偷拔。
InnoDB使用間隙鎖的目的有兩個:
-
為了防止幻讀(上面也說了,
Repeatable read
隔離級別下再通過GAP鎖即可避免了幻讀) - 滿足恢復(fù)和復(fù)制的需要
- MySQL的恢復(fù)機制要求:在一個事務(wù)未提交前亏钩,其他并發(fā)事務(wù)不能插入滿足其鎖定條件的任何記錄莲绰,也就是不允許出現(xiàn)幻讀
并發(fā)的問題就少不了死鎖,在MySQL中同樣會存在死鎖的問題姑丑。
但一般來說MySQL通過回滾幫我們解決了不少死鎖的問題了蛤签,但死鎖是無法完全避免的,可以通過以下的經(jīng)驗參考栅哀,來盡可能少遇到死鎖:
- 1震肮、以固定的順序訪問表和行。比如對兩個job批量更新的情形昌屉,簡單方法是對id列表先排序钙蒙,后執(zhí)行,這樣就避免了交叉等待鎖的情形间驮;將兩個事務(wù)的sql順序調(diào)整為一致躬厌,也能避免死鎖。
- 2、大事務(wù)拆小扛施。大事務(wù)更傾向于死鎖鸿捧,如果業(yè)務(wù)允許,將大事務(wù)拆小疙渣。
- 3匙奴、在同一個事務(wù)中,盡可能做到一次鎖定所需要的所有資源妄荔,減少死鎖概率泼菌。
- 4、降低隔離級別啦租。如果業(yè)務(wù)允許哗伯,將隔離級別調(diào)低也是較好的選擇,比如將隔離級別從RR調(diào)整為RC篷角,可以避免掉很多因為gap鎖造成的死鎖焊刹。
- 5、為表添加合理的索引恳蹲∨翱椋可以看到如果不走索引將會為表的每一行記錄添加上鎖,死鎖的概率大大增大嘉蕾。
參考資料:
8贺奠、死鎖產(chǎn)生的必要產(chǎn)生條件
產(chǎn)生死鎖必須同時滿足以下四個條件:只要其中任意一條不成立,死鎖就不會發(fā)生荆针。
- 1敞嗡、
互斥條件
:進程要求對分配的資源(如打印機)進行排他性控制,即在一段時間內(nèi)某一資源僅為一個進程所占有航背。此時若有其他進程請求該資源喉悴,則請求進程只能等待。 - 2玖媚、
不可剝奪條件
:進程所獲得的資源在未使用完畢之前箕肃,不能被其他進程強行奪走,即只能有該資源的進程自己來釋放今魔,只能是主動釋放勺像。 - 3、
請求和保持條件
:進程已經(jīng)保持了一個資源错森,但又提出了新的資源請求吟宦,而該資源已經(jīng)被其他進程占有,此時請求進程被阻塞涩维,但對自己已獲得的資源保持不放殃姓。 - 4、
循環(huán)等待條件
:存在一種進程資源的循環(huán)等待鏈,鏈中每一個進程已獲得的資源同步被鏈中的下一個進程占有蜗侈。
9篷牌、鎖總結(jié)
上面說了一大堆關(guān)于MySQL數(shù)據(jù)庫鎖的東西,現(xiàn)在來簡單總結(jié)一下踏幻。
表鎖其實我們程序員是很少關(guān)心它的:
- 在MyISAM存儲引擎中枷颊,當執(zhí)行SQL語句的時候是自動加的。
- 在InnoDB存儲引擎中该面,如果沒有使用索引夭苗,表鎖也是自動加的。
現(xiàn)在我們大多數(shù)使用MySQL都是使用InnoDB吆倦,InnoDB支持行鎖:
- 共享鎖--讀鎖--S鎖
- 排它鎖--寫鎖--X鎖
在默認的情況下听诸,select是不加任何行鎖的~事務(wù)可以通過以下語句顯示給記錄集加共享鎖或排他鎖。
- 共享鎖(S):
SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
蚕泽。 - 排他鎖(X):
SELECT * FROM table_name WHERE … FOR UPDATE
。
InnoDB基于行鎖還實現(xiàn)了MVCC多版本并發(fā)控制桥嗤,MVCC在隔離級別下的Read committed
和Repeatable read
下工作须妻。MVCC能夠?qū)崿F(xiàn)讀寫不阻塞!
InnoDB實現(xiàn)的Repeatable read
隔離級別配合GAP間隙鎖已經(jīng)避免了幻讀泛领!
- 樂觀鎖其實是一種思想荒吏,正如其名:認為不會鎖定的情況下去更新數(shù)據(jù),如果發(fā)現(xiàn)不對勁渊鞋,才不更新(回滾)绰更。在數(shù)據(jù)庫中往往添加一個version字段來實現(xiàn)。
- 悲觀鎖用的就是數(shù)據(jù)庫的行鎖锡宋,認為數(shù)據(jù)庫會發(fā)生并發(fā)沖突儡湾,直接上來就把數(shù)據(jù)鎖住,其他事務(wù)不能修改执俩,直至提交了當前事務(wù)
10徐钠、 參考資料
https://zhuanlan.zhihu.com/p/29150809--Mysql
https://blog.csdn.net/mysteryhaohao/article/details/51669741--MySQL
https://segmentfault.com/a/1190000015596126--MySQL
《高性能MySQL 第三版》