聊聊樂(lè)觀鎖與悲觀鎖
悲觀鎖
在MySQL中,悲觀鎖依賴(lài)數(shù)據(jù)庫(kù)提供的鎖機(jī)制來(lái)實(shí)現(xiàn)。在InnoDB引擎中,使用悲觀鎖需要先關(guān)閉MySQL數(shù)據(jù)庫(kù)的自動(dòng)提交屬性,然后通過(guò)select ... for update來(lái)進(jìn)行加鎖。
在數(shù)據(jù)庫(kù)中,悲觀鎖的流程如下:
? 在對(duì)記錄進(jìn)行修改前,先嘗試為該記錄加上排他鎖(exclusive lock)。
? 如果加鎖失敗,說(shuō)明該記錄正在被修改,此時(shí)當(dāng)前查詢(xún)可能需要等待或拋出異常,具體響應(yīng)方式由開(kāi)發(fā)者根據(jù)實(shí)際需求決定。
? 如果成功加鎖,則可以對(duì)記錄進(jìn)行修改,事務(wù)完成后鎖將被釋放。
? 其間若有其他操作試圖對(duì)該記錄進(jìn)行修改或加排他鎖,則會(huì)等待當(dāng)前鎖的釋放或直接拋出異常。
我們以電商平臺(tái)下單過(guò)程中扣減庫(kù)存的需求為例,說(shuō)明如何使用悲觀鎖:
-- 0.開(kāi)始事務(wù)
begin;
-- 1.查詢(xún)出商品信息
SELECT stock FROM products WHERE product_id = 12345 FOR UPDATE;
-- 2.修改商品stock為2
update products set stock=2 where product_id = 12345;
-- 3.提交事務(wù)
commit;
在對(duì)id=1的記錄進(jìn)行修改前,先通過(guò)FOR UPDATE的方式加鎖,然后再進(jìn)行修改。這就是典型的悲觀鎖策略。
如果上述修改庫(kù)存的代碼發(fā)生并發(fā),同一時(shí)間只有一個(gè)線程可以開(kāi)啟事務(wù)并獲得id=1的鎖,其他事務(wù)必須等本次事務(wù)提交之后才能執(zhí)行。這樣,我們可以保證當(dāng)前的數(shù)據(jù)不會(huì)被其他事務(wù)修改。
上面提到,使用SELECT ... FOR UPDATE會(huì)將數(shù)據(jù)鎖住,不過(guò)我們需要注意一些鎖的級(jí)別。MySQL InnoDB默認(rèn)使用行級(jí)鎖。行級(jí)鎖都是基于索引的,如果一條SQL語(yǔ)句未使用索引,優(yōu)化器在選擇時(shí),若發(fā)現(xiàn)鎖表可能性能更好,有可能會(huì)直接鎖表。
上面這個(gè)點(diǎn)之前也有在文章提到過(guò):
日活3kw的實(shí)際庫(kù)存業(yè)務(wù)場(chǎng)景中的超賣(mài)到底怎么解決的
感興趣的可以參考閱讀一下,希望對(duì)你有所幫助
樂(lè)觀鎖
MySQL中的樂(lè)觀鎖主要通過(guò)CAS(Compare and Swap)的機(jī)制來(lái)實(shí)現(xiàn),通常使用版本號(hào)(version)來(lái)實(shí)現(xiàn)。
CAS是一種樂(lè)觀鎖技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能成功更新變量的值,而其他線程都失敗。失敗的線程并不會(huì)被掛起,而是被告知在此次競(jìng)爭(zhēng)中失敗,并可以再次嘗試。
以扣減庫(kù)存為例,通過(guò)樂(lè)觀鎖可以實(shí)現(xiàn)如下:
-- 查詢(xún)出商品信息,stock = 3
select stock from products product_id id= 1
-- 根據(jù)商品信息生成訂單
-- 修改商品stock為2
update products set stock=2 where id=1 and stock = 3;
以上,在更新之前,先查詢(xún)庫(kù)存表中當(dāng)前的庫(kù)存數(shù)(stock),然后在執(zhí)行更新時(shí),將庫(kù)存數(shù)作為修改條件。提交更新時(shí),對(duì)比數(shù)據(jù)庫(kù)表記錄的當(dāng)前庫(kù)存數(shù)與第一次查詢(xún)得到的庫(kù)存數(shù),若兩者相等,則執(zhí)行更新;否則,視為數(shù)據(jù)已過(guò)期。
題外話(huà)
悲觀鎖
在對(duì)數(shù)據(jù)庫(kù)中的數(shù)據(jù)進(jìn)行修改時(shí),為了避免同時(shí)被其他人修改,最好的方法是直接對(duì)該數(shù)據(jù)進(jìn)行加鎖以防止并發(fā)。這種在修改數(shù)據(jù)之前先鎖定再修改的方式被稱(chēng)為悲觀并發(fā)控制(又稱(chēng)“悲觀鎖”,Pessimistic Concurrency Control,縮寫(xiě)為“PCC”)。
悲觀鎖之所以被稱(chēng)為悲觀,是因?yàn)檫@是一種對(duì)數(shù)據(jù)的修改抱有悲觀態(tài)度的并發(fā)控制方式。一般來(lái)說(shuō),我們認(rèn)為數(shù)據(jù)被并發(fā)修改的概率較大,因此在修改之前先加鎖。
悲觀并發(fā)控制實(shí)際上是一種保守的策略,即“先取鎖再訪問(wèn)”,它為數(shù)據(jù)處理的安全性提供了保證。
圖片
在效率方面,處理加鎖機(jī)制會(huì)導(dǎo)致數(shù)據(jù)庫(kù)產(chǎn)生額外的開(kāi)銷(xiāo),增加了產(chǎn)生死鎖的風(fēng)險(xiǎn)。此外,悲觀鎖還可能降低并行性,因?yàn)槿绻粋€(gè)事務(wù)鎖定了某行數(shù)據(jù),其他事務(wù)就必須等待該事務(wù)完成才能處理該行數(shù)據(jù)。
樂(lè)觀鎖
樂(lè)觀鎖(Optimistic Locking)是相對(duì)悲觀鎖而言的。樂(lè)觀鎖假設(shè)數(shù)據(jù)在一般情況下不會(huì)發(fā)生沖突,因此在數(shù)據(jù)提交更新時(shí)才會(huì)實(shí)際檢查數(shù)據(jù)是否沖突。如果發(fā)現(xiàn)沖突,則會(huì)向用戶(hù)返回錯(cuò)誤信息,讓用戶(hù)決定如何處理。
與悲觀鎖相比,樂(lè)觀鎖在處理數(shù)據(jù)庫(kù)時(shí)并不會(huì)使用數(shù)據(jù)庫(kù)提供的鎖機(jī)制。一般來(lái)說(shuō),樂(lè)觀鎖的實(shí)現(xiàn)方式是通過(guò)記錄數(shù)據(jù)的版本信息。
圖片
樂(lè)觀并發(fā)控制相信事務(wù)之間的數(shù)據(jù)競(jìng)爭(zhēng)(data race)的概率較小,因此盡可能直接進(jìn)行操作,直到提交時(shí)才對(duì)數(shù)據(jù)進(jìn)行檢查和鎖定。這樣做不會(huì)產(chǎn)生任何鎖或死鎖。
如何選擇
在樂(lè)觀鎖與悲觀鎖的選擇上面,主要看下兩者的區(qū)別以及適用場(chǎng)景就可以了。
1. 樂(lè)觀鎖并未真正加鎖,效率高。適用于讀操作頻繁,寫(xiě)操作相對(duì)較少的場(chǎng)景。一旦鎖的粒度掌握不好,更新失敗的概率就會(huì)比較高,容易發(fā)生業(yè)務(wù)失敗。
2. 悲觀鎖依賴(lài)數(shù)據(jù)庫(kù)鎖,效率低。更新失敗的概率比較低。適用于寫(xiě)操作較為頻繁,且并發(fā)寫(xiě)入的概率較高的場(chǎng)景。
根據(jù)以上區(qū)別和場(chǎng)景特點(diǎn),可以針對(duì)具體業(yè)務(wù)需求選擇合適的并發(fā)控制策略。當(dāng)然最多的場(chǎng)景其實(shí)當(dāng)屬于高并發(fā)場(chǎng)景如何選擇。