求你了,別在高并發(fā)場(chǎng)景中使用悲觀鎖了!


Hollis的新書限時(shí)折扣中,一本深入講解Java基礎(chǔ)的干貨筆記!
我們知道,樂觀鎖和悲觀鎖是并發(fā)控制主要采用的技術(shù)手段,通常用在數(shù)據(jù)庫管理中。
但是,樂觀鎖、悲觀鎖并不像行級(jí)鎖、共享鎖等概念一樣是真實(shí)存在的鎖。其實(shí)他們只是人們定義出來的概念,可以認(rèn)為是一種思想。 其實(shí)不僅僅是關(guān)系型數(shù)據(jù)庫系統(tǒng)中有樂觀鎖和悲觀鎖的概念,像memcache、hibernate、tair等都有類似的概念。
針對(duì)于不同的業(yè)務(wù)場(chǎng)景,應(yīng)該選用不同的并發(fā)控制方式。所以,不要把樂觀并發(fā)控制和悲觀并發(fā)控制狹義的理解為DBMS中的概念,更不要把他們和數(shù)據(jù)中提供的鎖機(jī)制(行鎖、表鎖、排他鎖、共享鎖)混為一談。其實(shí),在DBMS中,悲觀鎖正是利用數(shù)據(jù)庫本身提供的鎖機(jī)制來實(shí)現(xiàn)的。
網(wǎng)上有很多關(guān)于樂觀鎖和悲觀鎖的介紹,我之前也有文章(《深入理解樂觀鎖與悲觀鎖》)專門介紹過,這里為了方便大家理解,就簡(jiǎn)單做個(gè)總結(jié)。
悲觀鎖,正如其名,它指的是對(duì)數(shù)據(jù)被外界修改持悲觀態(tài)度,因此,在整個(gè)數(shù)據(jù)處理過程中,需要先將數(shù)據(jù)進(jìn)行鎖定,獲得鎖之后再進(jìn)行操作。
在MySQL中,可以使用排他鎖來實(shí)現(xiàn)悲觀鎖,主要就是用到select ... for update語法。
要使用悲觀鎖,需要關(guān)閉mysql數(shù)據(jù)庫的自動(dòng)提交屬性:set autocommit=0;
然后在事務(wù)中,通過如下語句對(duì)數(shù)據(jù)進(jìn)行加鎖:
select status from t_goods where id=1 for update以上,在對(duì)id = 1的記錄修改前,先通過for update的方式進(jìn)行加鎖,然后再進(jìn)行修改。這就是比較典型的悲觀鎖策略。
如果以上修改庫存的代碼發(fā)生并發(fā),同一時(shí)間只有一個(gè)線程可以開啟事務(wù)并獲得id=1的鎖,其它的事務(wù)必須等本次事務(wù)提交之后才能執(zhí)行。這樣我們可以保證當(dāng)前的數(shù)據(jù)不會(huì)被其它事務(wù)修改。
相對(duì)悲觀鎖而言,樂觀鎖假設(shè)認(rèn)為數(shù)據(jù)一般情況下不會(huì)造成沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時(shí)候,才會(huì)正式對(duì)數(shù)據(jù)的沖突與否進(jìn)行檢測(cè),如果發(fā)現(xiàn)沖突了,則讓返回用戶錯(cuò)誤的信息,讓用戶決定如何去做。
樂觀鎖的實(shí)現(xiàn)并不會(huì)使用數(shù)據(jù)庫提供的鎖機(jī)制。一般的實(shí)現(xiàn)樂觀鎖的方式就是記錄數(shù)據(jù)版本,如以下SQL:
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version}
以上,我們了解了樂觀鎖和悲觀鎖的思想以及實(shí)現(xiàn)了之后,討論一下他們的區(qū)別。
首先,在加鎖時(shí)間上有所不同,悲觀鎖是在事務(wù)剛開始的時(shí)候就加鎖,在拿到鎖之后再去進(jìn)行業(yè)務(wù)操作。而樂觀鎖是在更新的那一刻才會(huì)進(jìn)行并發(fā)控制,所以是先進(jìn)行的業(yè)務(wù)操作。
其次,悲觀鎖主要是借助數(shù)據(jù)庫的排他鎖實(shí)現(xiàn)的,而排他鎖本質(zhì)上是一種阻塞鎖。 如果并發(fā)量比較大并且沖突比較多的時(shí)候,會(huì)導(dǎo)致很多線程被鎖阻塞,導(dǎo)致請(qǐng)求的RT被拉長(zhǎng),并且會(huì)占用大量的數(shù)據(jù)庫鏈接。
相比之下,樂觀鎖不會(huì)造成阻塞,但是他帶來的問題就是如果并發(fā)的沖突比較高的話,那么就會(huì)有很多失敗的情況,需要業(yè)務(wù)代碼做好這種失敗的特殊處理。
第三點(diǎn),那就是樂觀鎖雖然叫鎖,但是他并沒有額外加鎖,它是通過CAS來實(shí)現(xiàn)的,所以他的效率比較高,而悲觀鎖需要利用數(shù)據(jù)庫的鎖機(jī)制進(jìn)行加鎖,這會(huì)帶來一定的額外消耗。
還有最后一點(diǎn),也是比較重要的一點(diǎn),那就是悲觀鎖因?yàn)樽隽思渔i的動(dòng)作,所以是會(huì)導(dǎo)致死鎖的。
我強(qiáng)烈建議大家,優(yōu)先使用樂觀鎖,尤其是并發(fā)比較高,并且沖突也比較多的場(chǎng)景。
因?yàn)槲覀兦懊嫣岬竭^,悲觀鎖會(huì)有額外的消耗、并且可能會(huì)帶來死鎖。但是這些都不是最重要的。
最重要的是,悲觀鎖本質(zhì)上是一種阻塞鎖,在并發(fā)比較高的情況下,會(huì)有很多個(gè)線程都被阻塞,而這些阻塞的線程是會(huì)占用數(shù)據(jù)庫鏈接的。所以這時(shí)候就會(huì)導(dǎo)致你的系統(tǒng)的并發(fā)度很低,還有就是這些阻塞的線程的響應(yīng)時(shí)長(zhǎng)也會(huì)被拉的很長(zhǎng),極度影響用戶體驗(yàn),也會(huì)多出來很多慢SQL。
額外提一句,在MySQL 8.0中,已經(jīng)支持了select ... for update nowait,可以把阻塞鎖變成非阻塞的??梢栽谀撤N程度上解決悲觀鎖的阻塞帶來的一些問題,但是加鎖的額外開銷和死鎖的問題也還是有的。
所以,高并發(fā)場(chǎng)景中,建議大家使用樂觀鎖,尤其是MySQL 5.x 的版本中,因?yàn)椴恢С?strong>nowait,一旦使用悲觀鎖,會(huì)大大降低你的系統(tǒng)的并發(fā)度。
完
我的新書《深入理解Java核心技術(shù)》已經(jīng)上市了,上市后一直蟬聯(lián)京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯(cuò)過哦~長(zhǎng)按二維碼即可購買~
長(zhǎng)按掃碼享受6折優(yōu)惠
往期推薦

知乎熱議:月薪 2~3W 的碼農(nóng),怎樣度過一天?

還在用 SimpleDateFormat 做時(shí)間格式化?小心項(xiàng)目崩掉!

入職一家新公司,如何快速熟悉代碼?
