你真的知道樂觀鎖和悲觀鎖嗎?

你真的知道樂觀鎖和悲觀鎖嗎?

Java

一、什麼是樂觀鎖和悲觀鎖

樂觀鎖和悲觀鎖主要使用在併發的情況下,在多個事務中共同訪問同一個數據庫資源,為了避免因為同時訪問造成的數據操作錯誤而產生。這裡從兩方面說,第一指的是數據庫,第二是java.

數據庫中的樂觀鎖和悲觀鎖

樂觀鎖,主要強調的是每次取數據的時候,都認為別的線程或事務不會修改數據,所以不會對數據進行上鎖,只有在更新數據的時候才會判斷是否有線程對要操作的數據進行修改;

悲觀鎖,主要強調的是每次取數據的時候,都認為別的線程會修改數據,所以每次都會對數據上鎖,只有獲取鎖的情況下才有操作數據的機會;

從上面的描述中,可看出數據庫層面的樂觀鎖和悲觀鎖,主要是在集中在對sql的處理上,即反映在數據庫的能力上。

java中的樂觀鎖和悲觀鎖

對應於樂觀鎖和悲觀鎖的邏輯,即對資源的佔用情況,在java中同時有對應的表現,如,synchronized關鍵字、reentrantlock等,都是獨佔資源的情況,所以屬於悲觀鎖;CAS操作屬於樂觀鎖;

二、樂觀鎖和悲觀鎖的具體實現

這裡的具體實現以數據庫層面的為主,java中併發的情況下次在寫。

以一個具體的例子來說明樂觀鎖和悲觀鎖的具體使用。

有商品表(products),其表結構如下,

你真的知道樂觀鎖和悲觀鎖嗎?

樂觀鎖和悲觀鎖

此表只有兩列商品名稱(p_name)、商品數量(p_num),以商城秒殺系統減庫存場景來說明。假設,現在要對p_name為“手錶”的庫存進行更新,現在有多個線程都要進行減庫存操作。

1)、未使用鎖

首先,要從數據庫中查出當前的庫存p_num1,然後把庫存減去p_num2,即p_num_new=p_num1-p_num2;

select p_num from products where p_name ='手錶';
update products set p_num=p_num_new where p_name ='手錶';

上面的語句在單線程低併發下不會有問題,但在多線程高併發下,假如,兩個線程T1、T2讀到同一個p_num1,又同時去減庫存,那麼它們計算到的最新的庫存為,

p_num_t1為線程T1減的庫存數,p_num_t2為線程T2減的庫存數,

T1:p_num_new=p_num1-p_num_t1;

T2:p_num_new=p_num1-p_num_t2;

從上邊看出,T2的庫存數計算的是有問題的,應該是:p_num_new=p_num1-p_num_t1-p_num_t2

那麼上邊的數據就是錯誤。

2)、樂觀鎖

實現樂觀鎖有兩種方式,即version和CAS。下面以上面的例子一一說明

2.1、version,數據版本號,

需要在原有數據表結構中新增一列version,作為版本號,其數據類型可為bigint類型,其作用主要體現在更新過程中,代表數據被更新的次數,每成功更新一次加1;

其商品表(products)結構如下,

樂觀鎖和悲觀鎖

你真的知道樂觀鎖和悲觀鎖嗎?

樂觀鎖和悲觀鎖

首先,要從數據庫中查出當前的庫存p_num1和version(記為version_old),然後把庫存減去p_num2,即p_num_new=p_num1-p_num2;

select p_num,version from products where p_name ='手錶';
update products set p_num=p_num_new,,version=version+1 where p_name ='手錶' and
version=#{version_old};

上面sql中新增了version條件,即使用取出的version作為更新的條件。看在兩個線程下的過程,

假如,兩個線程T1、T2讀到同一個p_num1和version(記為version_old),又同時去減庫存,那麼它們計算到的最新的庫存為,

p_num_t1為線程T1減的庫存數,p_num_t2為線程T2減的庫存數,

T1:p_num_new=p_num1-p_num_t1;

T2:p_num_new=p_num1-p_num_t2;

再看T1和T2的更新過程,假如T1先執行,T1更新成功後的version值記為version_t1

update products set p_num=p_num_t1,,version=version+1 where p_name ='手錶' and 
version=#{version_old};

T2的更新為,

update products set p_num=p_num_t1,,version=version+1 where p_name ='手錶' and 
version=#{version_old};

T1更新成功後,T2還會成功嗎?肯定不會,因為此時的version執行了加1操作,變成了version_t1=version+1,而T2的條件中的version依舊為version,即version+1≠version,此時T2是不會執行成功,那麼這樣的話數據就不會造成混亂。

在執行更新操作的時候,只有現在數據的版本號,和讀出的版本號一致,才更新成功,否則更新失敗;

2.2、CAS操作

CAS中包含三個值,內存值V,現在值A,新值B,只有在保證V=A的情況下,才會把V更新為B;應用到數據庫方面,V代表的是首先讀出的值;

以上面的例子說明,

要從數據庫中查出當前的庫存p_num1,然後把庫存減去p_num2,即p_num_new=p_num1-p_num2;

select p_num,version from products where p_name ='手錶';
update products set p_num=p_num_new,,version=version+1 where p_name ='手錶' and
p_num=#{p_num1};

假如,兩個線程T1、T2讀到同一個p_num1,又同時去減庫存,那麼它們計算到的最新的庫存為,

p_num_t1為線程T1減的庫存數,p_num_t2為線程T2減的庫存數,

T1:p_num_new=p_num1-p_num_t1;

T2:p_num_new=p_num1-p_num_t2;

再看T1和T2的更新過程,假如T1先執行,T1更新成功後的p_num變成了p_num1-p_num_t1,

update products set p_num=p_num_new where p_name ='手錶' and p_num=#{p_num1}

T2再執行,

update products set p_num=p_num_new where p_name ='手錶' and p_num=#{p_num1}

此時數據中的p_num值已經變成了p_num1-p_num_t1,那麼where條件就不成立,那麼更新便不會成功。

通過在更新時比較現在的值和之前的值是否一致,來判斷是否可更新成功,其原理類似於version。但此種方式無法避免ABA的問題,即,如果p_num被更新過,且正好更新為了p_num1,使用CAS的方式是可以更新成功的,但最終的結果是一致的。

2)、悲觀鎖

即,對數據庫的所有操作均加上數據庫鎖,其實現主要依賴於數據層面,

Select * from products for update; 對查詢結果的每行均加排他鎖

悲觀鎖能夠防止丟失更新和不可重複讀這類問題,但是它非常影響併發性能,因此應該謹慎使用

三、總結

樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去鎖的開銷,加大系統的整體吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適,即對所有的操作加鎖控制。

有不當之處歡迎指正,謝謝!

加Java架構師進階交流群獲取Java工程化、高性能及分佈式、高性能、深入淺出。高架構。性能調優、Spring,MyBatis,Netty源碼分析和大數據等多個知識點高級進階乾貨的直播免費學習權限 都是大牛帶飛 讓你少走很多的彎路的 群.號是 338549832 對了 小白勿進 最好是有開發經驗

注:加群要求

1、具有工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。

2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。

4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。

5.阿里Java高級大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!


分享到:


相關文章: