處理高並發問題時,我們經常用 Redis 進行加鎖操作,目的是為了解決並發可能帶來的問題。做一個簡單的總結
常見的方案之一:setnx,其他線程必須拿到這個值,才能繼續往下執行,否則等待。該命令是原子操作,所以可以防止並發情況的發生。
while(!$redis->setnx('lock', '1')) { // 設置鎖 usleep(100000); } // 執行業務代碼 $redis->del('lock'); // 釋放鎖
但是該方案有個弊端,如果設置鎖后進程崩潰,那么該鎖永遠不會釋放。一般解決方法是在 setnx 的時候設置過期時間,則可以解決線程奔潰鎖無法釋放的問題。但如果設置鎖和設置鎖的過期時間不是原子操作,仍然不能防止並發的情況發生。好在 Redis 中的 set 命令提供了這些參數使用,NX 就和 setnx 一樣, EX 可以在 set 的時候加上過期時間。
$expire = 5; // 鎖的過期時間 while(!$redis->set('lock', '1', ['NX', 'EX'=>$expire])) { usleep(100000); } // 執行業務代碼 $redis->del('lock');
但是在這種情況下,當一個進程執行出現問題或執行時間超過 setnx 設置的過期時間,那么這個鎖就自動消失了,仍會有其他進程並發執行業務代碼,且不同進程的鎖相互覆蓋。所以這個方案也不能有效防止並發。
解決方法: watch,在 set 的時候設置 NX 與 EX,並且設置值為隨機數(唯一),當 A 進程設置鎖后,后續進程都無法設置鎖。A 進程業務邏輯完成后,對比隨機數是否一致,如果一致再刪除,如果在刪除過程中,發現 key 的值被修改,則刪除失敗。防止 A 進程超時后,鎖被后續進程獲取,這個時候如果 A 進程刪除鎖,就會把后面的鎖給刪了。
$expire = 5; $random = session_create_id(); while(!$redis->set('lock', $random, ['NX', 'EX'=>$expire])) { usleep(100000); } $redis->watch("lock"); // 監聽 lock 的值 // 執行業務代碼 $ret = $redis->multi(Redis::MULTI) ->del("lock") ->exec(); // multi 的作用在於若被 watch 的鍵被修改、刪除、覆蓋,那么 exec 就會執行失敗 if($ret) { $redis->del("lock"); // 釋放鎖 } else { // 代表 lock 的值已被其他進程修改 }