Redis鎖
Redis為單進程單線程模式,採用隊列模式將並發訪問變成串列訪問,且多用戶端對Redis的串連並不存在競爭關係。
SETNX命令(SET if Not eXists)文法:
SETNX key value
若給定的 key 已經存在,則 SETNX 不做任何動作,並返回0。
安全性:保證互斥,在任何時候,只有一個用戶端可以持有鎖
無死結:即使當前持有鎖的用戶端崩潰或者從叢集中被分開了,其它用戶端最終總是能夠獲得鎖。
容錯性:只要大部分的 Redis 節點線上,那麼用戶端就能夠擷取和釋放鎖。 加鎖:
if (conn.setnx(“lock","1").equals(1L)) { return true; } return false;
解鎖
conn.del(“lock”);
存在問題: 用戶端崩潰或其他原因導致解鎖失敗時,其他用戶端將再也無法擷取到鎖,造成死結。
SET resource_name my_random_value NX PX 30000
命令中的NX表示如果Key不存在就添加,存在則直接返回。
PX表示以毫秒為單位設定key的到期時間,這裡是30000ms。
設定到期時間是防止獲得鎖的用戶端突然崩潰掉或其他異常情況,導致Redis中的對象鎖一直無法釋放,造成死結。Key的值需要在所有請求鎖服務的用戶端中,確保是個唯一值。 這是為了保證拿到鎖的用戶端能安全釋放鎖,防止這個鎖對象被其他用戶端刪除。 分布式鎖
舉例: A用戶端拿到對象鎖,但在因為一些原因被阻塞導致無法及時釋放鎖。 因為到期時間已到,Redis中的鎖對象被刪除。
B用戶端請求擷取鎖成功。
C用戶端請求擷取鎖成功。
這時B、C都拿到了鎖,因此分布式鎖失效。 加鎖:
public static String lock(String lockName, long lockTimeout) { String identifier = UUID.randomUUID().toString();if (redis.setnx(lockName, identifier).equals(1L)) { redis.pexpire(lockName, lockTimeout); } else if (redis.ttl(lockName).equals(-1L)){ redis.pexpire(lockName, lockTimeout); } return null;}
解鎖:
public static void unlock(String lockName, String identifier) { if (identifier.equals(redis.get(lockName))) { redis.del(lockName);}}
加鎖 參數含義: KEYS[1] :需要加鎖的key,這裡需要是字串類型。
ARGV[1] :鎖的逾時時間,防止死結
ARGV[2] :鎖的唯一標識,也就是剛才介紹的 id(UUID.randomUUID()) + “:” + threadId
// 檢查是否key已經被佔用,如果沒有則設定逾時時間和唯一標識,初始化value=1
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; // 如果鎖重入,需要判斷鎖的key field 都一致情況下 value 加一 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]);//鎖重入重新設定逾時時間 return nil; end; // 返回剩餘的到期時間 return redis.call('pttl', KEYS[1]);
解鎖
參數:
– KEYS[1] :需要加鎖的key,這裡需要是字串類型。
– KEYS[2] :redis訊息的ChannelName,一個分布式鎖對應唯一的一個
channelName:“redisson_lock__channel__{” + getName() + “}”
– ARGV[1] :reids訊息體,這裡只需要一個位元組的標記就可以,主要標記redis的key已經解鎖,再結合redis的Subscribe,能喚醒其他訂閱解鎖訊息的用戶端線程申請鎖。
– ARGV[2] :鎖的逾時時間,防止死結 – ARGV[3] :鎖的唯一標識,也就是剛才介紹的 id(UUID.randomUUID()) + “:” + threadId
// 如果key已經不存在,說明已經被解鎖,直接發布(publish)redis訊息
if (redis.call('exists', KEYS[1]) == 0) redis.call('publish', KEYS[2], ARGV[1]); return 1; end; // key和field不匹配,說明當前用戶端線程沒有持有鎖,不能主動解鎖。 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end;
// 將value減1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); // 如果counter>0說明鎖在重入,不能刪除key if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else // 刪除key並且publish 解鎖訊息 redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
高效能 加鎖、解鎖部分使用LUA封裝,確保原子性。
使用PUB/SUB訊息機制,減少申請鎖時的等待時間 安全 鎖帶有逾時時間、鎖的標識唯一,防止死結 鎖設計為可重新進入,避免死結
可重新進入鎖指的是在一個線程中可以多次擷取同一把鎖,如Java中的 ReentrantLock和 synchronized關鍵字
public void get() { lock.lock(); set(); lock.unlock(); } public void set() { lock.lock();lock.unlock(); }