不少人在開發(fā)的時(shí)候扼倘,應(yīng)該很少會注意到這些鎖的問題按价,也很少會給程序加鎖(除了庫存這些對數(shù)量準(zhǔn)確性要求極高的情況下)
一般也就聽過常說的樂觀鎖和悲觀鎖赚爵,了解過基本的含義之后就沒了~~~
即使我們不會這些鎖知識瓢湃,我們的程序在一般情況下還是可以跑得好好的矢炼。因?yàn)檫@些鎖數(shù)據(jù)庫隱式幫我們加了。
對于UPDATE慷垮,DELETE揖闸,INSERT語句,InnoDB會自動給涉及到的數(shù)據(jù)集加排它鎖料身。
MyISAM在執(zhí)行SELECT之前汤纸,會自動給涉及到的所有表加讀鎖,在執(zhí)行更新操作(UPDATE芹血,DELETE贮泞,INSERT)前楞慈,會自動給涉及到的表加寫鎖,這個(gè)過程不需要用戶干預(yù)啃擦。
只有某些特定的情況才需要我們手動加鎖囊蓝,那么我們學(xué)習(xí)鎖的目的是什么呢?當(dāng)然是為了裝逼令蛉,人生不裝逼聚霜,那還有什么意義。
一珠叔、鎖簡單介紹
從鎖的粒度蝎宇,可以分為兩大類:
表鎖:開銷小,加鎖快祷安,不會出現(xiàn)死鎖夫啊,鎖定力度大,發(fā)生鎖沖突的概率高辆憔,并發(fā)度低。
行鎖:開銷大报嵌,加鎖慢虱咧,會出現(xiàn)死鎖,鎖定力度小锚国,發(fā)生鎖沖突的概率低腕巡,并發(fā)讀高。
不同的存儲引擎支持的鎖力度是不一樣的血筑。
InnoDB支持表鎖和行鎖绘沉。
MyISAM只支持表鎖。
InnoDB只有通過索引條件檢索數(shù)據(jù)才使用行級鎖豺总,否則车伞,InnoDB將使用表鎖。也就是說喻喳,InnoDB的行鎖是基于索引的另玖。
表鎖下又分為兩種模式:
- 表讀鎖(Table Read Lock)
- 表寫鎖(Table Write Lock)
從下圖可以清晰看到,在表讀鎖和表寫鎖的環(huán)境下:讀讀不阻塞表伦,讀寫阻塞谦去,寫寫阻塞!
讀讀不阻塞:當(dāng)前用戶在讀數(shù)據(jù)蹦哼,其他的用戶也在讀數(shù)據(jù)鳄哭,不會加鎖。
讀寫阻塞:當(dāng)前用戶在讀數(shù)據(jù)纲熏,其他的用戶不能修改當(dāng)前用戶讀的數(shù)據(jù)妆丘,會加鎖锄俄!
寫寫阻塞:當(dāng)前用戶在修改數(shù)據(jù),其他的用戶不能修改當(dāng)前用戶正在修改的數(shù)據(jù)飘痛,會加鎖珊膜!
從上面已經(jīng)看到了:讀鎖和寫鎖是互斥的,讀寫操作是串行宣脉。
- 如果某個(gè)進(jìn)程想要獲取讀鎖车柠,同時(shí)另外一個(gè)進(jìn)程想要獲取寫鎖。在mysql里邊塑猖,寫鎖是優(yōu)先于讀鎖的竹祷!
- 寫鎖和讀鎖優(yōu)先級的問題可以通過參數(shù)調(diào)節(jié):max_write_lock_count和low-priority-updates
值得注意的是:
- MyISAM可以支持查詢和插入操作的并發(fā)進(jìn)行⊙蚬叮可以通過系統(tǒng)變量concurrent_insert來指定哪種模式塑陵,在MyISAM中它默認(rèn)是:如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個(gè)進(jìn)程讀表的同時(shí)蜡励,另一個(gè)進(jìn)程從表尾插入記錄令花。
-
但是InnoDB存儲引擎是不支持的!
二凉倚、MySQL的事務(wù)隔離級別
在這之前兼都,先了解一下MySQL的事務(wù)隔離級別以及并發(fā)帶來的問題定续。
事務(wù)的并發(fā)問題
1后控、臟讀:事務(wù)A讀取了事務(wù)B更新的數(shù)據(jù),然后B回滾操作堤瘤,那么A讀取到的數(shù)據(jù)是臟數(shù)據(jù)
2杏糙、不可重復(fù)讀:事務(wù) A 多次讀取同一數(shù)據(jù)慎王,事務(wù) B 在事務(wù)A多次讀取的過程中,對數(shù)據(jù)作了更新并提交宏侍,導(dǎo)致事務(wù)A多次讀取同一數(shù)據(jù)時(shí)赖淤,結(jié)果 不一致。
3谅河、幻讀:系統(tǒng)管理員A將數(shù)據(jù)庫中所有學(xué)生的成績從具體分?jǐn)?shù)改為ABCDE等級漫蛔,但是系統(tǒng)管理員B就在這個(gè)時(shí)候插入了一條具體分?jǐn)?shù)的記錄,當(dāng)系統(tǒng)管理員A改結(jié)束后發(fā)現(xiàn)還有一條記錄沒有改過來旧蛾,就好像發(fā)生了幻覺一樣莽龟,這就叫幻讀。
小結(jié):不可重復(fù)讀和幻讀很容易混淆锨天,不可重復(fù)讀側(cè)重于修改毯盈,幻讀側(cè)重于新增或刪除。解決不可重復(fù)讀的問題只需鎖住滿足條件的行病袄,解決幻讀需要鎖表
MySQL事務(wù)隔離級別
事務(wù)隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
讀未提交(read-uncommitted) | 是 | 是 | 是 |
讀已提交(read-committed) | 否 | 是 | 是 |
可重復(fù)讀(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
MySQL的默認(rèn)隔離級別是可重復(fù)讀搂赋,可以通過下面的命令查看
select @@tx_isolation;
隔離級別越高赘阀,越能保證數(shù)據(jù)的完整性和一致性,但是對并發(fā)性能的影響也越大脑奠,魚和熊掌不可兼得啊基公。對于多數(shù)應(yīng)用程序,可以優(yōu)先考慮把數(shù)據(jù)庫系統(tǒng)的隔離級別設(shè)為Read Committed宋欺,它能夠避免臟讀取轰豆,而且具有較好的并發(fā)性能。盡管它會導(dǎo)致不可重復(fù)讀齿诞、幻讀這些并發(fā)問題酸休,在可能出現(xiàn)這類問題的個(gè)別場合,可以由應(yīng)用程序采用悲觀鎖或樂觀鎖來控制祷杈。
可以通過下面的命令設(shè)置事務(wù)隔離級別
set session transaction isolation level xxxx;
三斑司、樂觀鎖和悲觀鎖
無論是Read committed還是Repeatable read隔離級別,都是為了解決讀寫沖突的問題但汞。
單純在Repeatable read隔離級別下我們來考慮一個(gè)問題:
此時(shí)宿刮,用戶李四的操作就丟失掉了
丟失更新:一個(gè)事務(wù)的更新覆蓋了其它事務(wù)的更新結(jié)果。
解決的方法:
- 使用Serializable隔離級別私蕾,事務(wù)是串行執(zhí)行的僵缺!
- 樂觀鎖
- 悲觀鎖
樂觀鎖是一種思想,具體實(shí)現(xiàn)是是目,表中有一個(gè)版本字段,第一次讀的時(shí)候标捺,獲取到這個(gè)字段懊纳。處理完業(yè)務(wù)邏輯開始更新的時(shí)候,需要再次查看該字段的值是否和第一次的一樣亡容。如果一樣更新嗤疯,反之拒絕。之所以叫樂觀闺兢,因?yàn)檫@個(gè)模式?jīng)]有從數(shù)據(jù)庫加鎖茂缚,等到更新的時(shí)候再判斷是否可以更新。
悲觀鎖是數(shù)據(jù)庫層面加鎖屋谭,都會阻塞去等待鎖脚囊。
1.悲觀鎖
所以,按照上面的例子桐磁。我們使用悲觀鎖的話其實(shí)很簡單(手動加行鎖就行了):
select * from xxxx for update
在select 語句后邊加了 for update相當(dāng)于加了排它鎖(寫鎖)悔耘,加了寫鎖以后,其他的事務(wù)就不能對它修改了我擂!需要等待當(dāng)前事務(wù)修改完之后才可以修改衬以。也就是說缓艳,如果張三使用select ... for update,李四就無法對該條記錄修改了看峻。
2.樂觀鎖
樂觀鎖不是數(shù)據(jù)庫層面上的鎖阶淘,不需要自己手動去加的鎖。一般我們添加一個(gè)版本字段來實(shí)現(xiàn):
具體過程是這樣的:
張三select * from table --->會查詢出記錄出來互妓,同時(shí)會有一個(gè)version字段
name | age | version |
---|---|---|
male | 25 | 1 |
李四select * from table --->會查詢出記錄出來溪窒,同時(shí)會有一個(gè)version字段
name | age | version |
---|---|---|
male | 25 | 1 |
李四對這條記錄做修改
update A set age=30,version=version+1 where name=#{name} and version=#{version}
判斷之前查詢到的version與現(xiàn)在的數(shù)據(jù)的version進(jìn)行比較,同時(shí)會更新version字段车猬。
此時(shí)數(shù)據(jù)庫記錄如下:
name | age | version |
---|---|---|
male | 30 | 2 |
張三也對這條記錄修改:update A set name=female,version=version+1 where name=#{name} and version=#{version}霉猛,但失敗了!因?yàn)楫?dāng)前數(shù)據(jù)庫中的版本跟查詢出來的版本不一致珠闰!
四惜浅、間隙鎖GPA
當(dāng)我們用范圍條件檢索數(shù)據(jù)而不是相等條件檢索數(shù)據(jù),并請求共享或排他鎖時(shí)伏嗜,InnoDB會給符合范圍條件的已有數(shù)據(jù)記錄的索引項(xiàng)加鎖坛悉;對于鍵值在條件范圍內(nèi)但并不存在的記錄,叫做“間隙(GAP)”承绸。InnoDB也會對這個(gè)“間隙”加鎖裸影,這種鎖機(jī)制就是所謂的間隙鎖。
值得注意的是:間隙鎖只會在Repeatable read隔離級別下使用
例子:假如emp表中只有101條記錄军熏,其empid的值分別是1,2,...,100,101
Select * from emp where empid > 100 for update;
上面是一個(gè)范圍查詢轩猩,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大于101(這些記錄并不存在)的“間隙”加鎖荡澎。
InnoDB使用間隙鎖的目的:
- 為了防止幻讀:在一個(gè)事務(wù)未提交前均践,其他并發(fā)事務(wù)不能插入滿足其鎖定條件的任何記錄,也就是不允許出現(xiàn)幻讀摩幔。
五彤委、死鎖
并發(fā)的問題就少不了死鎖,在MySQL中同樣會存在死鎖的問題或衡。
但一般來說MySQL通過回滾幫我們解決了不少死鎖的問題了焦影,但死鎖是無法完全避免的,可以通過以下的經(jīng)驗(yàn)參考封断,來盡可能少遇到死鎖:
(1)以固定的順序訪問表和行斯辰。比如對兩個(gè)job批量更新的情形,簡單方法是對id列表先排序坡疼,后執(zhí)行椒涯,這樣就避免了交叉等待鎖的情形;將兩個(gè)事務(wù)的sql順序調(diào)整為一致,也能避免死鎖废岂。
(2)大事務(wù)拆小祖搓。大事務(wù)更傾向于死鎖,如果業(yè)務(wù)允許湖苞,將大事務(wù)拆小拯欧。
(3)在同一個(gè)事務(wù)中,盡可能做到一次鎖定所需要的所有資源财骨,減少死鎖概率镐作。
(4)降低隔離級別。如果業(yè)務(wù)允許隆箩,將隔離級別調(diào)低也是較好的選擇该贾,比如將隔離級別從RR調(diào)整為RC,可以避免掉很多因?yàn)間ap鎖造成的死鎖捌臊。
(5)為表添加合理的索引杨蛋。可以看到如果不走索引將會為表的每一行記錄添加上鎖理澎,死鎖的概率大大增大逞力。
六、總結(jié)
上面說了一大堆關(guān)于MySQL數(shù)據(jù)庫鎖的東西糠爬,現(xiàn)在來簡單總結(jié)一下寇荧。
表鎖其實(shí)我們程序員是很少關(guān)心它的:
- 在MyISAM存儲引擎中,當(dāng)執(zhí)行SQL語句的時(shí)候是自動加表鎖的执隧。
- 在InnoDB存儲引擎中揩抡,如果沒有使用索引,表鎖也是自動加的镀琉。
現(xiàn)在我們大多數(shù)使用MySQL都是使用InnoDB峦嗤,InnoDB支持行鎖:
- 共享鎖--讀鎖--S鎖
- 排它鎖--寫鎖--X鎖
在默認(rèn)的情況下,select是不加任何行鎖的~事務(wù)可以通過以下語句顯示給記錄集加共享鎖或排他鎖滚粟。
// 共享鎖(S)
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE寻仗。
// 排他鎖(X)
SELECT * FROM table_name WHERE ... FOR UPDATE刃泌。
InnoDB基于行鎖還實(shí)現(xiàn)了MVCC多版本并發(fā)控制凡壤,MVCC在隔離級別下的Read committed和Repeatable read下工作。MVCC能夠?qū)崿F(xiàn)讀寫不阻塞耙替!
InnoDB實(shí)現(xiàn)的Repeatable read隔離級別配合GAP間隙鎖已經(jīng)避免了幻讀亚侠!
- 樂觀鎖其實(shí)是一種思想,正如其名:認(rèn)為不會鎖定的情況下去更新數(shù)據(jù)俗扇,如果發(fā)現(xiàn)不對勁硝烂,才不更新(回滾)。在數(shù)據(jù)庫中往往添加一個(gè)version字段來實(shí)現(xiàn)铜幽。
- 悲觀鎖用的就是數(shù)據(jù)庫的行鎖滞谢,認(rèn)為數(shù)據(jù)庫會發(fā)生并發(fā)沖突串稀,直接上來就把數(shù)據(jù)鎖住,其他事務(wù)不能修改狮杨,直至提交了當(dāng)前事務(wù)