分佈式鎖是在分佈式場景中,實現共享資源互斥訪問的一種方式。Java中synchronized或ReentrantLock只能保證在一個jvm中的最多隻有一個線程可以獲取資源的鎖,但是如果是分佈式場景,會有多個jvm中各自的線程都會競爭共享資源。這時synchronized或ReentrantLock就無能爲力,就需要使用分佈式鎖。
分佈式鎖的特點
- 互斥性:同一時間只能有一個線程獲取鎖
- 可重入性:一個線程獲取鎖之後,可以再次獲取鎖
- 鎖超時:支持鎖超時,防止死鎖
分佈式鎖的實現方式
- redis
- zookeeper
- 數據庫
本文使用第一種redis實現方式。
用Redis實現分佈式鎖
網上流傳的一種用redis實現分佈式鎖的方法,直接使用setnx和expire兩條命令,其實是錯誤,因爲這兩條命令的執行不是原子的,所以無法保證setnx加鎖後,expire設置鎖超時時間一定成功,這種情況就會出現死鎖。
一、使用set命令實現
從redis 2.6.12版本起,set命令增加了一種支持多個參數的格式
set key value [EX seconds] [PX miliseconds] [NX|XX]
EX: 以秒爲單位的過期時間
PX:以毫秒爲單位的過期時間
NX:if not exists的縮寫,表示當key不存在時,才設置值
XX:if exists的縮寫,表示當key存在時,才設置值
public class DistributedLock{
private static final String SET_IF_NOT_EXISTS = "NX";
private static final String EXPIRE = "EX";
private static final String LOCK_SUCCESS = "OK";
//返回true表示獲取鎖成功
public boolean lock(String key, String sessionId, int expiredSeconds){
return LOCK_SUCCESS.equals(jedis.set(key, sessionId, SET_IF_NOT_EXISTS, EXPIRE, expiredSeconds);
}
//返回true表示釋放成功
public boolean releaseLock(String key, String value){
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L));
}
}
這裏的key一般是資源的名稱,sessionId要求在分佈式場景中是唯一的,比如會話id,可以使用uuid實現。因爲在釋放鎖時會比較value的值,保證不會錯誤地釋放其他鎖。釋放鎖使用lua腳本,保證原子性。
這種方式適用於單節點redis的情況。如果是redis集羣,比如說A客戶端在Redis的master節點上拿到了鎖,但是這個加鎖的key還沒有同步到slave節點,master故障,發生故障轉移,一個slave節點升級爲master節點,B客戶端也可以獲取同個key的鎖,但客戶端A也已經拿到鎖了,這就導致多個客戶端都拿到鎖。這種情況需要其他方式處理。
二、使用lua腳本實現
使用lua腳本加鎖可以保證原子性,釋放鎖的方法與上面相同
public boolean getLockWithLua(String key, String uniqueId, int seconds) {
String luaScript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(key);
values.add(UniqueId);
values.add(String.valueOf(seconds));
Object result = jedis.eval(lua_scripts, keys, values);
//判斷是否成功
return result.equals(1L);
}
本文參考以下博文,感謝原作者