針對業務場景的需要,合理的使用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>
  1. CAS(定義見後)操作方式:即compare and swap 或者 compare and set,涉及到三個操作數,數據所在的內存值,預期值,新值。當需要更新時,判斷當前內存值與之前取到的值是否相等,若相等,則用新值更新,若失敗則重試,一般情況下是一個自旋操作,即不斷的重試。


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

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

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

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


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

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

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

方案:使用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>

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

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

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

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

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

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

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

我們用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 讀取頻繁時使用樂觀鎖,寫入頻繁時則使用悲觀鎖。還有一點:樂觀鎖不能解決髒讀的問題。


分享到:


相關文章: