天天看點

Redis分布式鎖的正确實作,SET NX 并設定逾時時間錯誤示範使用單個執行個體正确實作

錯誤示範

之前看過很多redis實作分布式鎖基本都是在程式中使用時間戳進行加鎖逾時判斷,然而這種方法并不能在高并發情況避免誤删;以下是錯誤示範

錯誤示範

public boolean lock(String key, String value) {
        //如果key值不存在,則傳回 true,且設定 value
        if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }

        //擷取key的值,判斷是是否逾時
        String curVal = redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotEmpty(curVal) && Long.parseLong(curVal) < System.currentTimeMillis()) {
            //獲得之前的key值,同時設定目前的傳入的value。這個地方可能幾個線程同時過來,但是redis本身天然是單線程的,是以getAndSet方法還是會安全執行,
            //首先執行的線程,此時curVal當然和oldVal值相等,因為就是同一個值,之後該線程set了自己的value,後面的線程就取不到鎖了
            String oldVal = redisTemplate.opsForValue().getAndSet(key, value);
            if(StringUtils.isNotEmpty(oldVal) && oldVal.equals(curVal)) {
                return true;
            }
        }
        return false;
    }
           

  很明顯,在高并發情況下,會出現一個線程擷取鎖後被其他線程修改掉逾時時間。并且解鎖時也會出現一些問題;

redis官方給了正确的加鎖姿勢,快來看!!

使用單個執行個體正确實作

在嘗試克服上述單執行個體設定的限制之前,讓我們在這個簡單的情況下檢查如何正确地執行它,因為這實際上是一個可行的解決方案,在不時可以接受競争條件的應用程式中,并且因為鎖定到單個執行個體是我們将用于此處描述的分布式算法的基礎。

要獲得鎖定,可以采用以下方法:

SET resource_name my_random_value NX PX 30000           

java中使用jedis set 指令加鎖,即

// keys 加鎖key args:密鑰 解鎖時确認與加鎖着為同一對象,一般可用 uuid 
jedis.set(keys,args,"NX","PX",30000)
           

該指令僅在密鑰尚不存在時才設定密鑰(NX選項),到期時間為30000毫秒(PX選項)。密鑰設定為“我的随機值”值。此值必須在所有用戶端和所有鎖定請求中都是唯一的。

基本上使用随機值是為了以安全的方式釋放鎖,使用一個告訴Redis的腳本:隻有當密鑰存在并且密鑰中存儲的值正是我期望的那個時才删除密鑰。這是通過以下Lua腳本完成的:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
           

  在java中使用jedis eval指令執行腳本解鎖,即

String str = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";                         
 
// keys 即加鎖key ,args 即 密鑰
jedis.eval(str, Collections.singletonList(keys), Collections.singletonList(args))
           

這一點很重要,以避免删除由另一個用戶端建立的鎖。例如,用戶端可以擷取鎖定,在某些操作中被阻止的時間長于鎖定有效時間(密鑰将到期的時間),并且稍後移除已經由某個其他用戶端擷取的鎖定。僅使用DEL是不安全的,因為用戶端可能會删除另一個用戶端的鎖定。使用上面的腳本而不是每個鎖都使用随機字元串“簽名”,是以隻有在用戶端嘗試删除鎖定時,鎖定才會被删除。

這個随機字元串應該是什麼?我假設它是來自/ dev / urandom的20個位元組,但是你可以找到更便宜的方法來使它對你的任務足夠獨特。例如,安全選擇是使用/ dev / urandom對RC4進行種子處理,并從中生成僞随機流。一個更簡單的解決方案是使用unix時間和微秒分辨率的組合,将其與用戶端ID連接配接起來,它不是那麼安全,但可能在大多數環境中都可以完成任務。

我們用作關鍵時間的時間被稱為“鎖定有效時間”。它既是自動釋放時間,也是用戶端為了執行所需操作所需的時間,而另一個用戶端可能能夠再次擷取鎖定,而不會在技術上違反互斥保證,這僅限于給定的視窗從獲得鎖定的那一刻起的時間。

是以現在我們有了獲得和釋放鎖的好方法。系統推理由單個始終可用的執行個體組成的非分布式系統是安全的。讓我們将概念擴充到我們沒有這種保證的分布式系統。