redis setnx 過期時間_Redis加鎖的幾種實現

我們經常在工作中會碰到一些重復請求、並發等問題,而給資源加鎖是一種不錯的手段。我們今天就整理下使用redis
作為分布式鎖的幾種實現。
redis
可以用於幾個命令是:INCR
、SETNX
和SET
。
1. 使用INCR
加鎖
這種加鎖的思路是, key
不存在,那么 key
的值會先被初始化為 0 ,然后再執行 INCR
操作進行加一。然后其它用戶在執行 INCR
操作進行加一時,如果返回的數大於 1
,說明這個鎖正在被使用當中。
/*** 1、 客戶端A請求服務器獲取key的值為1表示獲取了鎖 2、 客戶端B也去請求服務器獲取key的值為2表示獲取鎖失敗 3、 客戶端A執行代碼完成,刪除鎖 4、 客戶端B在等待一段時間后在去請求的時候獲取key的值為1表示獲取鎖成功 5、 客戶端B執行代碼完成,刪除鎖 **/ $res = $redis->incr($key); // 自增1 $redis->expire($key, $ttl); // 設置鎖的有效期 if($res == 1){ // 獲取資源成功 }else{ // 資源被其他請求占用 }
2. 使用SETNX
加鎖
這種加鎖的思路是,如果 key
不存在,將 key
設置為 value
,如果 key
已存在,則 SETNX
不做任何動作。
/*** 1、 客戶端A請求服務器設置key的值,如果設置成功就表示加鎖成功 2、 客戶端B也去請求服務器設置key的值,如果返回失敗,那么就代表加鎖失敗 3、 客戶端A執行代碼完成,刪除鎖 4、 客戶端B在等待一段時間后在去請求設置key的值,設置成功 5、 客戶端B執行代碼完成,刪除鎖 **/ $res = $redis->setNX($key, $value); // 當key不存在時設置key=value $redis->expire($key, $ttl); // 設置鎖的有效期 if($res){ // 獲取資源成功 }else{ // 資源被其他請求占用 }
-
/
上面兩種方法都有一個問題,會發現,都需要設置 key
過期時間。那么為什么要設置key
過期時間呢?如果請求執行因為某些原因意外退出了,導致創建了鎖但是沒有刪除鎖,那么這個鎖將一直存在(redis不設置key的過期時間,默認是永久的),以至於一直處於加鎖狀態。於是乎我們需要給鎖加一個過期時間以防不測。
但是借助 Expire
來設置就不是原子性操作了。所以還可以通過redis
事務來確保原子性。那上面的代碼就要優化成:
// 第一種方式的加鎖 $redis->multi(); // 標記一個事務塊的開始 $res = $redis->incr($key); $redis->expire($key, $ttl); $redis->exec(); // 提交事務 if($res == 1){ // 獲取資源成功 }else{ // 資源被其他請求占用 } // 第二種方式的加鎖 $redis->multi(); // 標記一個事務塊的開始 $res = $redis->setNX($key, $value); $redis->expire($key, $ttl); $redis->exec(); // 提交事務 if($res){ // 獲取資源成功 }else{ // 資源被其他請求占用 }
上面代碼看起來是不是很繁瑣。好在redis
官方從版本 2.6.12 開始 SET
命令本身已經包含了設置過期時間的功能。
3. 使用SET
加鎖
/*** 1、 客戶端A請求服務器設置key的值,如果設置成功就表示加鎖成功 2、 客戶端B也去請求服務器設置key的值,如果返回失敗,那么就代表加鎖失敗 3、 客戶端A執行代碼完成,刪除鎖 4、 客戶端B在等待一段時間后在去請求設置key的值,設置成功 5、 客戶端B執行代碼完成,刪除鎖 **/ $res = $redis->set($key, $value, ['nx', 'ex' => $ttl]); //nx代表當key不存在時設置 ex代表設置過期時間 if($res){ // 獲取資源成功 }else{ // 資源被其他請求占用 }
4. 其他問題
雖然上面一步已經滿足了我們的需求,但是還是要考慮其它問題? - 1、 redis發現鎖失敗了要怎么辦?中斷請求還是循環請求? - 2、 循環請求的話,如果有一個獲取了鎖,其它的在去獲取鎖的時候,是不是容易發生搶鎖的可能? - 3、 鎖提前過期后,客戶端A還沒執行完,然后客戶端B獲取到了鎖,這時候客戶端A執行完了,會不會在刪鎖的時候把B的鎖給刪掉?
5. 解決辦法
- 針對問題1:使用循環請求,循環請求去獲取鎖
- 針對問題2:針對第二個問題,在循環請求獲取鎖的時候,加入睡眠功能,等待幾毫秒在執行循環
- 針對問題3:在加鎖的時候存入的key是隨機的。這樣的話,每次在刪除key的時候判斷下存入的key里的value和自己存的是否一樣
do { //針對問題1,使用循環 $timeout = 10; $roomid = 10001; $key = 'room_lock'; $value = 'room_'.$roomid; //分配一個隨機的值針對問題3 $isLock = $redis->set($key, $value, 'ex', $timeout, 'nx');//ex 秒 if ($isLock) { if ($redis->get($key) == $value) { //防止提前過期,誤刪其它請求創建的鎖 //執行內部代碼 $redos->del($key); continue;//執行成功刪除key並跳出循環 } } else { usleep(5000); //睡眠,降低搶鎖頻率,緩解redis壓力,針對問題2 } } while(!$isLock);
就這么多了,喜歡就點個贊吧~
參考:
- http://ukagaka.github.io/php/2017/09/21/redisLock.html