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> php

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

  • 如返回0,代表該鎖已被其餘客戶端取得,這時咱們能夠先返回或進行重試等對方完成或等待鎖超時。python



解決死鎖 
上面的鎖定邏輯有一個問題:若是一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎麼解決?咱們能夠經過鎖的鍵對應的時間戳來判斷這種狀況是否發生了,若是當前的時間已經大於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代碼來描述使用分佈式鎖的全過程: redis

# 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 鎖等待超時,防止線程飢餓,永遠沒有入鎖執行代碼的機會    算法

/**
     * Acquire lock.
     * 
     * @param jedis
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException
     *             in case of thread interruption
     */
    public synchronized boolean acquire(Jedis jedis) throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); //鎖到期時間

            if (jedis.setnx(lockKey, expiresStr) == 1) {
                // lock acquired
                locked = true;
                return true;
            }

            String currentValueStr = jedis.get(lockKey); //redis裏的時間
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                //判斷是否爲空,不爲空的狀況下,若是被其餘線程設置了值,則第二個條件判斷是過不去的
                // lock is expired

                String oldValueStr = jedis.getSet(lockKey, expiresStr);
                //獲取上一個鎖到期時間,並設置如今的鎖到期時間,
                //只有一個線程才能獲取上一個線上的設置時間,由於jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    //如過這個時候,多個線程剛好都到了這裏,可是隻有一個線程的設置值和當前值相同,他纔有權利獲取鎖
                    // lock acquired
                    locked = true;
                    return true;
                }
            }
            timeout -= 100;
            Thread.sleep(100);
        }
        return false;
    }

 

通常用法 
其中不少繁瑣的邊緣代碼 
包括:異常處理,釋放資源等等        分佈式

JedisPool pool;
        JedisLock jedisLock = new JedisLock(pool.getResource(), lockKey, timeoutMsecs, expireMsecs);
        try {
            if (jedisLock.acquire()) { // 啓用鎖
                //執行業務邏輯
            } else {
                logger.info("The time wait for lock more than [{}] ms ", timeoutMsecs);
            }
        } catch (Throwable t) {
            // 分佈式鎖異常
            logger.warn(t.getMessage(), t);
        } finally {
            if (jedisLock != null) {
                try {
                    jedisLock.release();// 則解鎖
                } catch (Exception e) {
                }
            }
            if (jedis != null) {
                try {
                    pool.returnResource(jedis);// 還到鏈接池裏
                } catch (Exception e) {
                }
            }
        }

 

 

犀利用法 
用匿名類來實現,代碼很是簡潔 
至於SimpleLock的實現ide

 SimpleLock lock = new SimpleLock(key);
        lock.wrap(new Runnable() {
            @Override
            public void run() {
                //此處代碼是鎖上的
            }
        });
相關文章
相關標籤/搜索