使用Redis實現鎖(支持分佈式應用)(整理網絡資料)

使用Redis實現鎖(支持分佈式應用)

1.    簡介

使用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服務器

2.    背景

  在特殊業務邏輯中,須要保證莫一個操做同時只有一個線程在操做,保證數據一致性。防止數據被屢次改寫或產生多條重複數據。網絡

3.    思路

經過get 和set 命令實現

  這種方式很容易想到,就是當每次請求到來時經過get判斷這個鎖是否存在,若是不存在則set建立。這種方法有一個弊端,因爲get和set是兩次Redis請求,兩者之間有延時,在高併發的環境下,有可能在get檢測到鎖不存以後在set以前已經被其餘線程set,這時當前線程再set,這樣鎖就失效了。因此這種方法只能應對併發量不是很高的狀況併發

經過setnx 和 expire命令實現

  在訪問須要互斥訪問的資源時,經過setnx命令去設置一個lock 鍵,setnx的做用是判斷鎖是否存在,若是不存在則建立,返回成功,若是存在則返回失敗,服務器返回給客戶端,指示客戶端稍後重試。expire命令用於給該鎖設定一個過時時間,用於防止線程crash,致使鎖一直有效,從而致使死鎖。例如:設定鎖的有效期爲100秒,那麼即便線程奔潰,在100秒後鎖會自動失效。(實際上,這個地方也有問題,高併發下在執行expire命令時偶爾會失敗(Redis socket連接問題),失敗後這個lock就不會自動過時,值會會一直存在,出現死鎖致使後續的重試操做就永遠不會成功! 爲保證執行成功須要考慮失敗時屢次執行expiresocket

 

setnx lock "lock"分佈式

expire lock 100 //若是鎖定成功,則設置過時時間高併發

do work code //工做邏輯代碼

del lock //訪問互斥資源結束後,刪除鎖

 

經過setnx和 getset命令加 timespan+timeout (推薦)

如何解決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

4.    代碼

經過setnx 和 expire命令實現代碼

 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  

 

經過setnx和 getset命令加 timespan+timeout (推薦代碼)

 

 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     }
相關文章
相關標籤/搜索