redis分佈式鎖-SETNX實現

Redis有一系列的命令,特點是以NX結尾,NX是Not eXists的縮寫,如SETNX命令就應該理解爲:SET if Not eXists。這系列的命令非常有用,這裏講使用SETNX來實現分佈式鎖。 

用SETNX實現分佈式鎖 
利用SETNX非常簡單地實現分佈式鎖。例如:某客戶端要獲得一個名字foo的鎖,客戶端使用下面的命令進行獲取: 
SETNX lock.foo <current Unix time + lock timeout + 1>

  • 如返回1,則該客戶端獲得鎖,把lock.foo的鍵值設置爲時間值表示該鍵已被鎖定,該客戶端最後可以通過DEL lock.foo來釋放該鎖。

  • 如返回0,表明該鎖已被其他客戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖超時。

解決死鎖 
上面的鎖定邏輯有一個問題:如果一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎麼解決?我們可以通過鎖的鍵對應的時間戳來判斷這種情況是否發生了,如果當前的時間已經大於lock.foo的值,說明該鎖已失效,可以被重新使用。 

發生這種情況時,可不能簡單的通過DEL來刪除鎖,然後再SETNX一次,當多個客戶端檢測到鎖超時後都會嘗試去釋放它,這裏就可能出現一個競態條件,讓我們模擬一下這個場景: 

C0操作超時了,但它還持有着鎖,C1和C2讀取lock.foo檢查時間戳,先後發現超時了。 
C1 發送DEL lock.foo 
C1 發送SETNX lock.foo 並且成功了。 
C2 發送DEL lock.foo 
C2 發送SETNX lock.foo 並且成功了。 
這樣一來,C1,C2都拿到了鎖!問題大了! 

幸好這種問題是可以避免的,讓我們來看看C3這個客戶端是怎樣做的: 

C3發送SETNX lock.foo 想要獲得鎖,由於C0還持有鎖,所以Redis返回給C3一個0 
C3發送GET lock.foo 以檢查鎖是否超時了,如果沒超時,則等待或重試。 
反之,如果已超時,C3通過下面的操作來嘗試獲得鎖: 
GETSET lock.foo <current Unix time + lock timeout + 1> 
通過GETSET,C3拿到的時間戳如果仍然是超時的,那就說明,C3如願以償拿到鎖了。 
如果在C3之前,有個叫C4的客戶端比C3快一步執行了上面的操作,那麼C3拿到的時間戳是個未超時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,儘管C3沒拿到鎖,但它改寫了C4設置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。 

注意:爲了讓分佈式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作,因爲可能客戶端因爲某個耗時的操作而掛起,操作完的時候鎖因爲超時已經被別人獲得,這時就不必解鎖了。 

示例僞代碼 
根據上面的代碼,我寫了一小段Fake代碼來描述使用分佈式鎖的全過程: 
# get lock 
lock = 0 
while lock != 1: 
    timestamp = current Unix time + lock timeout + 1 
    lock = SETNX lock.foo timestamp 
    if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)): 
        break; 
    else: 
        sleep(10ms) 

# do your job 
do_job() 

# release 
if now() < GET lock.foo: 
    DEL lock.foo 
是的,要想這段邏輯可以重用,使用Python的你馬上就想到了Decorator,而用Java的你是不是也想到了那誰?AOP + annotation?行,怎樣舒服怎樣用吧,別重複代碼就行。 

java之jedis實現 
expireMsecs 鎖持有超時,防止線程在入鎖以後,無限的執行下去,讓鎖無法釋放 
timeoutMsecs 鎖等待超時,防止線程飢餓,永遠沒有入鎖執行代碼的機會

Java代碼下載地址    收藏代碼

  1. /** 

  2.  * Acquire lock. 

  3.  *  

  4.  * @param jedis 

  5.  * @return true if lock is acquired, false acquire timeouted 

  6.  * @throws InterruptedException 

  7.  *             in case of thread interruption 

  8.  */  

  9. public synchronized boolean acquire(Jedis jedis) throws InterruptedException {  

  10.     int timeout = timeoutMsecs;  

  11.     while (timeout >= 0) {  

  12.         long expires = System.currentTimeMillis() + expireMsecs + 1;  

  13.         String expiresStr = String.valueOf(expires); //鎖到期時間  

  14.   

  15.         if (jedis.setnx(lockKey, expiresStr) == 1) {  

  16.             // lock acquired  

  17.             locked = true;  

  18.             return true;  

  19.         }  

  20.   

  21.         String currentValueStr = jedis.get(lockKey); //redis裏的時間  

  22.         if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {  

  23.             //判斷是否爲空,不爲空的情況下,如果被其他線程設置了值,則第二個條件判斷是過不去的  

  24.             // lock is expired  

  25.   

  26.             String oldValueStr = jedis.getSet(lockKey, expiresStr);  

  27.             //獲取上一個鎖到期時間,並設置現在的鎖到期時間,  

  28.             //只有一個線程才能獲取上一個線上的設置時間,因爲jedis.getSet是同步的  

  29.             if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {  

  30.                 //如過這個時候,多個線程恰好都到了這裏,但是隻有一個線程的設置值和當前值相同,他纔有權利獲取鎖  

  31.                 // lock acquired  

  32.                 locked = true;  

  33.                 return true;  

  34.             }  

  35.         }  

  36.         timeout -= 100;  

  37.         Thread.sleep(100);  

  38.     }  

  39.     return false;  

  40. }  



一般用法 
其中很多繁瑣的邊緣代碼 
包括:異常處理,釋放資源等等

Java代碼  收藏代碼

  1. JedisPool pool;  

  2. JedisLock jedisLock = new JedisLock(pool.getResource(), lockKey, timeoutMsecs, expireMsecs);  

  3. try {  

  4.     if (jedisLock.acquire()) { // 啓用鎖  

  5.         //執行業務邏輯  

  6.     } else {  

  7.         logger.info("The time wait for lock more than [{}] ms ", timeoutMsecs);  

  8.     }  

  9. catch (Throwable t) {  

  10.     // 分佈式鎖異常  

  11.     logger.warn(t.getMessage(), t);  

  12. finally {  

  13.     if (jedisLock != null) {  

  14.         try {  

  15.             jedisLock.release();// 則解鎖  

  16.         } catch (Exception e) {  

  17.         }  

  18.     }  

  19.     if (jedis != null) {  

  20.         try {  

  21.             pool.returnResource(jedis);// 還到連接池裏  

  22.         } catch (Exception e) {  

  23.         }  

  24.     }  

  25. }  


犀利用法 
用匿名類來實現,代碼非常簡潔 
至於SimpleLock的實現,請在我附件中下載查看

Java代碼  收藏代碼

  1. SimpleLock lock = new SimpleLock(key);  

  2. lock.wrap(new Runnable() {  

  3.     @Override  

  4.     public void run() {  

  5.         //此處代碼是鎖上的  

  6.     }  

  7. });  



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章