轉自:https://my.oschina.net/u/1995545/blog/366381
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 鎖等待超時,防止線程饑餓,永遠沒有入鎖執行代碼的機會?? ?
/*** 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 acquiredlocked = 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 acquiredlocked = 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的實現
???????
SimpleLock lock = new SimpleLock(key);lock.wrap(new Runnable() {@Overridepublic void run() {//此處代碼是鎖上的 }});
?