針對業務場景的需要,合理的使用MySQL樂觀鎖與悲觀鎖

針對 MySQL的樂觀鎖與悲觀鎖的使用,基本都是按照業務場景針對性使用的。針對每個業務場景,對應的使用鎖。但是兩種鎖無非都是解決併發所產生的問題。下面我們來看看如何合理的使用樂觀鎖與悲觀鎖

何為悲觀鎖

悲觀鎖(Pessimistic Lock):就是很悲觀,每次去取數據的時候都認為別人會去修改,所以每次在取數據的時候都會給它上鎖,這樣別人想拿這個數據就會block直到它取到鎖。比如用在庫存增減問題上,利用悲觀鎖可以有效的防止減庫存問題。

簡單來講,悲觀鎖就是假定會發生併發衝突,屏蔽一切可能違反數據完整性的操作。悲觀併發控制實際上是 “先取鎖,再訪問” 的保守策略,為數據處理的安全提供了保證。

在效率上,處理加鎖的機制會讓數據庫產生額外的開銷,還會有死鎖的可能性。降低並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數據。


何為樂觀鎖

樂觀鎖(Optimistic Lock):就是很樂觀,每次去獲取數據時,都認為其他人不會修改它,因此不會鎖定,但是在提交更新時會判斷在此期間其他人是否有去更新此數據。 樂觀鎖適用於讀多寫少的應用場景,這樣可以提高吞吐量。

也就是說,樂觀鎖就是假設不會發生併發衝突,只在提交操作時檢查是否違反數據完整性。


悲觀鎖與樂觀鎖的區別

1 優缺點

兩種鎖各有優缺點,不可認為一種好於另一種,比如像樂觀鎖,適用於寫比較少的情況下,衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。 但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。

2 實現方式

悲觀鎖的實現方式:悲觀鎖的實現,依靠數據庫提供的鎖機制。在數據庫中,悲觀鎖的流程如下:1 在對數據修改前,嘗試增加排他鎖。2 加鎖失敗,意味著數據正在被修改,進行等待或者拋出異常。3 加鎖成功,對數據進行修改,提交事務,鎖釋放。4 如果我們加鎖成功,有其他線程對該數據進行操作或者加排他鎖的操作,只能等待或者拋出異常。

樂觀鎖的實現方式:1)version方式:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

sql實現代碼

<code>update table set n=n+1, version=version+1 where id=#{id} and version=#{version}; /<code>CAS(定義見後)操作方式:即compare and swap 或者 compare and set,涉及到三個操作數,數據所在的內存值,預期值,新值。當需要更新時,判斷當前內存值與之前取到的值是否相等,若相等,則用新值更新,若失敗則重試,一般情況下是一個自旋操作,即不斷的重試。


悲觀鎖與樂觀鎖的合理使用

本質上,MySQL的樂觀鎖與悲觀鎖主要都是用來解決併發的場景,避免丟失更新問題。

樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,數據發生衝突的可能性就會增大,為了保證數據的一致性,應用層需要不斷的重新獲取數據,這樣會增加大量的查詢操作,降低了系統的吞吐量。

悲觀鎖:比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。


用一個場景與代碼來詳細的介紹一下如何合理使用,假設有這麼一個商品秒殺和搶購的場景:在搶購場景中,一共只有100個商品,在最後一刻,已經消耗了99個商品,僅剩最後一個。這個時候,系統發來多個併發請求,這批請求讀取到的商品餘量都是99個,然後都通過了這一個餘量判斷,最終導致商品超發。也就是:導致了併發用戶B也“搶購成功”,多讓一個人獲得了商品。

1 用悲觀鎖的方案: 悲觀鎖,也就是在修改數據的時候,採用鎖定狀態,排斥外部請求的修改。遇到加鎖的狀態,就必須等待。

方案:使用MySQL的事務,鎖住操作的行

<code>fetch_assoc(); if($row['number']>0){ //生成訂單 $order_sn=build_order_no(); $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price) values('$order_sn','$user_id','$goods_id','$sku_id','$price')"; $order_rs=mysqli_query($conn,$sql); //庫存減少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'"; $store_rs=mysqli_query($conn,$sql); if($store_rs){ echo '庫存減少成功'; insertLog('庫存減少成功'); mysqli_query($conn,"COMMIT");//事務提交即解鎖 }else{ echo '庫存減少失敗'; insertLog('庫存減少失敗'); } }else{ echo '庫存不夠'; insertLog('庫存不夠'); mysqli_query($conn,"ROLLBACK"); }/<code>

上述的方案解決了線程安全的問題,但是,我們的場景是“高併發”。也就是說,會很多這樣的修改請求,每個請求都需要等待“鎖”,某些線程可能永遠都沒有機會搶到這個“鎖”,這種請求就會死在那裡。同時,這種請求會很多,瞬間增大系統的平均響應時間,結果是可用連接數被耗盡,系統陷入異常。

稍微修改一下上面的場景,我們直接將請求放入隊列中的,先進先出,這樣的話,我們就不會導致某些請求永遠獲取不到鎖。

全部請求採用“先進先出”的隊列方式來處理,解決了鎖的問題。但是新的問題來了,在高併發的場景下請求很多,可能一瞬間將隊列內存“撐爆”,然後系統又陷入到了異常狀態。

這個時候,我們就可以用樂觀鎖來解決相關問題了。上面也提到,樂觀鎖是相對於“悲觀鎖”採用更為寬鬆的加鎖機制,大都是採用帶版本號(Version)更新。

實現就是這個數據所有請求都有資格去修改,但會獲得一個該數據的版本號,只有版本號符合的才能更新成功,其他的返回搶購失敗。這樣的話,我們就不需要考慮隊列的問題,不過它會增大CPU的計算開銷。但是,綜合來說,這是一個比較好的解決方案。

我們用Redis中的watch來實現樂觀鎖,通過這個實現,保證數據的安全。

<code>connect('127.0.0.1', 6379); echo $mywatchkey = $redis->get("mywatchkey"); /* //插入搶購數據 if($mywatchkey>0){ $redis->watch("mywatchkey"); //啟動一個新的事務。 $redis->multi(); $redis->set("mywatchkey",$mywatchkey-1); $result = $redis->exec(); if($result) { $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time()); $watchkeylist = $redis->hGetAll("watchkeylist"); echo "搶購成功!


"; $re = $mywatchkey - 1; echo "剩餘數量:".$re."
"; echo "用戶列表:

"; print_r($watchkeylist); }else{ echo "手氣不好,再搶購!";exit; } }else{ // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12"); // $watchkeylist = $redis->hGetAll("watchkeylist"); echo "fail!
"; echo ".no result
"; echo "用戶列表:

"; //var_dump($watchkeylist); }*/ $rob_total = 100; //搶購數量 if($mywatchkey<=$rob_total){ $redis->watch("mywatchkey"); $redis->multi(); //在當前連接上啟動一個新的事務。 //插入搶購數據 $redis->set("mywatchkey",$mywatchkey+1); $rob_result = $redis->exec(); if($rob_result){ $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey); $mywatchlist = $redis->hGetAll("watchkeylist"); echo "搶購成功!
"; echo "剩餘數量:".($rob_total-$mywatchkey-1)."
"; echo "用戶列表:

"; var_dump($mywatchlist); }else{ $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao'); echo "手氣不好,再搶購!";exit; } } /<code>

總結

1 要記住鎖機制一定要在事務中才能生效,事務也就要基於MySQL InnoDB 引擎。

2 訪問量不大,不會造成壓力時使用悲觀鎖,面對高併發的情況下,我們應該使用樂觀鎖。

3 讀取頻繁時使用樂觀鎖,寫入頻繁時則使用悲觀鎖。還有一點:樂觀鎖不能解決髒讀的問題。