使用Redis指令setnx、expire、getset等操做實現互斥資源的訪問java
本文內容來着網絡整理,參考:linux
http://www.linuxidc.com/Linux/2014-12/110958.htmredis
http://www.jeffkit.info/2011/07/1000/算法
http://blog.csdn.net/java2000_wl/article/details/8740911服務器
在特殊業務邏輯中,須要保證莫一個操做同時只有一個線程在操做,保證數據一致性。防止數據被屢次改寫或產生多條重複數據。網絡
這種方式很容易想到,就是當每次請求到來時經過get判斷這個鎖是否存在,若是不存在則set建立。這種方法有一個弊端,因爲get和set是兩次Redis請求,兩者之間有延時,在高併發的環境下,有可能在get檢測到鎖不存以後在set以前已經被其餘線程set,這時當前線程再set,這樣鎖就失效了。因此這種方法只能應對併發量不是很高的狀況併發
在訪問須要互斥訪問的資源時,經過setnx命令去設置一個lock 鍵,setnx的做用是判斷鎖是否存在,若是不存在則建立,返回成功,若是存在則返回失敗,服務器返回給客戶端,指示客戶端稍後重試。expire命令用於給該鎖設定一個過時時間,用於防止線程crash,致使鎖一直有效,從而致使死鎖。例如:設定鎖的有效期爲100秒,那麼即便線程奔潰,在100秒後鎖會自動失效。(實際上,這個地方也有問題,高併發下在執行expire命令時偶爾會失敗(Redis socket連接問題),失敗後這個lock就不會自動過時,值會會一直存在,出現死鎖致使後續的重試操做就永遠不會成功! 爲保證執行成功須要考慮失敗時屢次執行expire)socket
setnx lock "lock"分佈式
expire lock 100 //若是鎖定成功,則設置過時時間高併發
do work code //工做邏輯代碼
del lock //訪問互斥資源結束後,刪除鎖
如何解決setnx + expire 的死鎖問題?能夠經過鎖的鍵對應的時間戳來判斷這種狀況是否發生了,若是當前的時間已經大於lock的值,說明該鎖已失效,能夠被從新使用。
發生這種狀況時,可不能簡單的經過DEL來刪除鎖,而後再SETNX一次,當多個客戶端檢測到鎖超時後都會嘗試去釋放它,這裏就可能出現一個競態條件,讓咱們模擬一下這個場景:
C0操做超時了,但它還持有着鎖,C1和C2讀取lock檢查時間戳,前後發現超時了。
C1 發送DEL lock
C1 發送SETNX lock而且成功了。
C2 發送DEL lock
C2 發送SETNX lock而且成功了。
這樣一來,C1,C2都拿到了鎖!問題大了!
幸虧這種問題是能夠避免的,讓咱們來看看C3這個客戶端是怎樣作的:
C3發送SETNX lock想要得到鎖,因爲C0還持有鎖,因此Redis返回給C3一個0
C3發送GET lock以檢查鎖是否超時了,若是沒超時,則等待或重試。
反之,若是已超時,C3經過下面的操做來嘗試得到鎖:
GETSET lock <current Unix time + lock timeout + 1>
經過GETSET,C3拿到的時間戳若是仍然是超時的,那就說明,C3如願以償拿到鎖了。
若是在C3以前,有個叫C4的客戶端比C3快一步執行了上面的操做,那麼C3拿到的時間戳是個未超時的值,這時,C3沒有如期得到鎖,須要再次等待或重試。留意一下,儘管C3沒拿到鎖,但它改寫了C4設置的鎖的超時值,不過這一點很是微小的偏差帶來的影響能夠忽略不計。
注意:爲了讓分佈式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖以前應該再檢查一次本身的鎖是否已經超時,再去作DEL操做,由於可能客戶端由於某個耗時的操做而掛起,操做完的時候鎖由於超時已經被別人得到,這時就沒必要解鎖了
附僞代碼:
# 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
1 public boolean tryLock(String key, int timeout, int expiretime, int sleeptime) throws Exception { 2 3 4 5 Jedis redis = jedisPool.getResource(); 6 7 try { 8 9 long nano = System.nanoTime(); 10 11 do { 12 13 Long i = redis.setnx(key, "key"); 14 15 jedisPool.returnResource(redis); 16 17 if (i == 1) { 18 19 redis.expire(key, expiretime); 20 21 return Boolean.TRUE; 22 23 } 24 25 if (timeout == 0) { 26 27 break; 28 29 } 30 31 Thread.sleep(sleeptime); 32 33 } while ((System.nanoTime() - nano) < TimeUnit.SECONDS.toNanos(timeout)); 34 35 return Boolean.FALSE; 36 37 } catch (RuntimeException | InterruptedException e) { 38 39 if (redis != null) { 40 41 jedisPool.returnBrokenResource(redis); 42 43 } 44 45 throw e; 46 47 } 48 49 } 50 51
1 public boolean tryLock(String key, int timeout, int expiretime, int sleeptime) throws Exception { 2 3 4 5 Jedis redis = jedisPool.getResource(); 6 7 try { 8 9 long nano = System.nanoTime(); 10 11 12 13 do { 14 15 long timestamp = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiretime) + 1; 16 17 Long i = redis.setnx(key, String.valueOf(timestamp)); 18 19 jedisPool.returnResource(redis); 20 21 if (i == 1) { 22 23 return Boolean.TRUE; 24 25 } 26 27 String lockVal = getString(key); 28 29 if (StringUtils.isBlank(lockVal) || !StringUtils.isNumeric(lockVal)) { 30 31 lockVal = "0"; 32 33 } 34 35 if (System.currentTimeMillis() > Long.valueOf(lockVal)) { 36 37 lockVal = getAndset(key, String.valueOf(timestamp)); 38 39 if (StringUtils.isBlank(lockVal) || !StringUtils.isNumeric(lockVal)) { 40 41 lockVal = "0"; 42 43 } 44 45 if (System.currentTimeMillis() > Long.valueOf(lockVal)) { 46 47 return Boolean.TRUE; 48 49 } 50 51 } 52 53 if (timeout == 0) { 54 55 break; 56 57 } 58 59 Thread.sleep(sleeptime); 60 61 } while ((System.nanoTime() - nano) < TimeUnit.SECONDS.toNanos(timeout)); 62 63 return Boolean.FALSE; 64 65 } catch (RuntimeException | InterruptedException e) { 66 67 if (redis != null) { 68 69 jedisPool.returnBrokenResource(redis); 70 71 } 72 73 throw e; 74 75 } 76 77 }