實(shí)戰(zhàn)3:如何使用樂(lè)觀鎖
1. 前言
在鎖一節(jié)中,我們從粒度
和管理
兩個(gè)角度來(lái)闡述了鎖。如果你還不熟悉鎖,請(qǐng)先閱讀該小節(jié),再來(lái)進(jìn)行本小節(jié)的學(xué)習(xí)。
本小節(jié)我們將繼續(xù)深挖鎖
,以開(kāi)發(fā)者和實(shí)戰(zhàn)的角度來(lái)談鎖。
2. 為什么需要鎖
2.1 什么是數(shù)據(jù)競(jìng)爭(zhēng)
在本節(jié)的開(kāi)頭,我們來(lái)談一談為什么開(kāi)發(fā)程序需要使用鎖?如果你有一點(diǎn)并發(fā)編程的基礎(chǔ),又或者對(duì)多線程有一點(diǎn)熟悉,那么你肯定知道答案,那就是數(shù)據(jù)競(jìng)爭(zhēng)
。
2.2 數(shù)據(jù)競(jìng)爭(zhēng)實(shí)例
我們舉一個(gè)生活的例子。在中學(xué)的時(shí)候,教室的前面會(huì)放上一塊小黑板,小黑板上會(huì)記載某一天上交作業(yè)或誰(shuí)誰(shuí)誰(shuí)打掃衛(wèi)生之類(lèi)的。那塊小黑板是所有同學(xué)都可使用的,只要你有事情要公布,就可以寫(xiě)在上面。
那么問(wèn)題來(lái)了,假設(shè)同學(xué)A
用小黑板寫(xiě)上明天要上交的作業(yè),此時(shí)同學(xué)B
也需要寫(xiě)上明天值日的同學(xué),對(duì)于A
和B
來(lái)說(shuō),他們之間存在競(jìng)爭(zhēng)關(guān)系,而小黑板就是競(jìng)爭(zhēng)點(diǎn)。
直觀上來(lái)說(shuō),如果A
比B
早到,那么A
就可以占有小黑板,換言之A
給小黑板加上了一把鎖,B
不能使用小黑板。A
寫(xiě)完了,把小黑板再次放到了教室前,相當(dāng)于釋放了鎖,此時(shí)B
才可書(shū)寫(xiě)小黑板,即B
拿到了鎖。
因此,鎖的出現(xiàn)是為了解決并發(fā)
中存在的數(shù)據(jù)競(jìng)爭(zhēng)
問(wèn)題。
3. 樂(lè)觀鎖和悲觀鎖
樂(lè)觀
與悲觀
是兩種不同的態(tài)度,從名字上看,二者就是以開(kāi)發(fā)者的態(tài)度作為邊界來(lái)分類(lèi)的。
樂(lè)觀鎖認(rèn)為,同一數(shù)據(jù)在并發(fā)條件下,發(fā)生沖突是小概率事件,因此我們不加鎖,而是加上版本號(hào)判斷修改是否成功。
悲觀鎖認(rèn)為,同一數(shù)據(jù)在并發(fā)條件下,沖突是大概率事件,因此我們必須先加鎖,不允許別人修改。
悲觀鎖和樂(lè)觀鎖其實(shí)是一種思想,主要取決于開(kāi)發(fā)者對(duì)待它的態(tài)度。在鎖這一小節(jié)中,里面談到的所有鎖宏觀上(可能實(shí)現(xiàn)的思想是樂(lè)觀鎖)來(lái)說(shuō)都是悲觀鎖,因此一旦加鎖,都會(huì)鎖定數(shù)據(jù),直到解鎖才會(huì)釋放。
3.1 樂(lè)觀鎖實(shí)施方案
樂(lè)觀鎖不全依賴(lài)于數(shù)據(jù)庫(kù),一般情況下我們都是在代碼層面上來(lái)完成它的,主流的設(shè)計(jì)思路是這樣的:
我們?cè)跀?shù)據(jù)表中添加一個(gè)字段version
,version 代表版本號(hào),字段類(lèi)型為整型。當(dāng)我們獲取數(shù)據(jù)時(shí),假設(shè)得到它的version
字段為n
,執(zhí)行完其它操作對(duì)該數(shù)據(jù)進(jìn)行更新時(shí),會(huì)執(zhí)行UPDATE ... SET version=n+1 WHERE version=n
。
如果在更新時(shí),數(shù)據(jù)已經(jīng)被別人更新過(guò)了,那么該數(shù)據(jù)的version
字段已經(jīng)不是n
了,那么此時(shí)修改就會(huì)失敗,反之修改就會(huì)成功。
可以看到,樂(lè)觀鎖就像它的名稱(chēng)一樣樂(lè)觀,適合數(shù)據(jù)讀多寫(xiě)少
的場(chǎng)景,因?yàn)閷?shí)際上并沒(méi)鎖住數(shù)據(jù),所以性能十分可觀;而悲觀鎖則與之相反,適合寫(xiě)多讀少的場(chǎng)景,盲目的排他性一定程度上會(huì)大幅影響性能。
4. 實(shí)踐
4.1 樂(lè)觀鎖數(shù)據(jù)表
樂(lè)觀鎖的使用十分廣泛,我們也推薦你在實(shí)際的開(kāi)發(fā)中使用樂(lè)觀鎖,接下來(lái),我們以一個(gè)例子來(lái)詳細(xì)的說(shuō)明一下樂(lè)觀鎖。
我們新建一個(gè)測(cè)試數(shù)據(jù)表 imooc_order :
DROP TABLE IF EXISTS imooc_order;
CREATE TABLE imooc_order
(
id int PRIMARY KEY,
price decimal(10,2),
-- version 字段作為樂(lè)觀鎖版本控制位
version int NOT NULL DEFAULT 0
);
INSERT INTO imooc_order(id,price,version)
VALUES (1,23.2,1);
注意: 我們已經(jīng)在表中添加了 version 字段
4.2 樂(lè)觀鎖實(shí)例
imooc_order
表存放了訂單信息(簡(jiǎn)略信息),而訂單的價(jià)格并非一成不變的,它可能會(huì)同時(shí)被多個(gè)人改變。
那么如何能夠安全地修改它的價(jià)格,且不會(huì)跟別人沖突了。
現(xiàn)在默認(rèn)有兩個(gè)人,甲
現(xiàn)在拿到了id
為1
的訂單,想要修改它的價(jià)格:
SELECT * FROM imooc_order WHERE id = 1;
在甲
拿到的同時(shí),乙
也同樣拿到了訂單數(shù)據(jù),且訂單此時(shí)的價(jià)格為23.2
,版本號(hào)為1
。
甲
決定修改訂單的價(jià)格為33.3
,于是他執(zhí)行了如下語(yǔ)句:
UPDATE imooc_order SET version = 1 + 1, price=33.3 WHERE id = 1 AND version = 1;
甲
執(zhí)行成功了,而此時(shí)乙
也需要修改價(jià)格,但是他并不知道價(jià)格已經(jīng)修改:
UPDATE imooc_order SET version = 1 + 1, price=22.1 WHERE id = 1 AND version = 1;
很明顯,乙
修改失敗了,因?yàn)樵谒薷膬r(jià)格之前,甲
以微弱的速度優(yōu)勢(shì)已經(jīng)修改了價(jià)格,且修改了 version
字段,此時(shí) version
等于2
。
而乙
提交 SQL 語(yǔ)句時(shí),Where 中明確的寫(xiě)到 version 等于 1。即使乙修改失敗,但是數(shù)據(jù)仍然是正確的,乙
完全可以在失敗的情況下重復(fù)獲取一次數(shù)據(jù)再修改。
如下圖所示:
4.3 樂(lè)觀鎖總結(jié)
可以看到,樂(lè)觀鎖雖然有缺陷,它會(huì)使更新失敗,因此必須重復(fù)獲取數(shù)據(jù)然后重試,但是它保證了數(shù)據(jù)的正確性和完整性。在讀多寫(xiě)少的場(chǎng)景下,樂(lè)觀鎖不會(huì)出現(xiàn)太多的重試,當(dāng)然如果出現(xiàn)了很多重試,證明場(chǎng)景已經(jīng)可能不是讀多寫(xiě)少了,可以嘗試換方案了。
樂(lè)觀鎖的實(shí)現(xiàn)也頗為簡(jiǎn)單,不需要任何第三方依賴(lài),你完全可以自己直接實(shí)現(xiàn),不過(guò)仍然有一些第三方框架提供了開(kāi)箱即用的樂(lè)觀鎖,你可以根據(jù)自己的使用語(yǔ)言和生態(tài)去查找相應(yīng)的樂(lè)觀鎖框架。
5. 小結(jié)
- 樂(lè)觀鎖和悲觀鎖同等重要,樂(lè)觀鎖是很多高并發(fā)場(chǎng)景下的基石。
- 大多數(shù)時(shí)候,程序使用的都是悲觀鎖,如常見(jiàn)的
自旋鎖
。 - 樂(lè)觀鎖與悲觀鎖都是一種思路,熟悉并掌握該思路,任何面試都攔不到你。