redis分布式锁,面试官请随便问,我都会
生活随笔
收集整理的這篇文章主要介紹了
redis分布式锁,面试官请随便问,我都会
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
文章有點長并且繞,先來個圖片緩沖下!
說到分布式,就必然涉及到分布式鎖的概念,如何保證不同機器不同線程的分布式鎖同步呢?互斥性,同一時刻,只能有一個客戶端持有鎖。 防止死鎖發生,如果持有鎖的客戶端崩潰沒有主動釋放鎖,也要保證鎖可以正常釋放及其他客戶端可以正常加鎖。 加鎖和釋放鎖必須是同一個客戶端。 容錯性,只有redis還有節點存活,就可以進行正常的加鎖解鎖操作。
Long?result?=?jedis.setnx(key,?uniqueId);
if?(1?==?result)?{
//如果該redis實例崩潰,那就無法設置過期時間了
jedis.expire(key,?expireTime);
}
}
在多線程并發環境下,任何非原子性的操作,都可能導致問題。這段代碼中,如果設置過期時間時,redis實例崩潰,就無法設置過期時間。如果客戶端沒有正確的釋放鎖,那么該鎖(永遠不會過期),就永遠不會被釋放。
long?expireTs?=?System.currentTimeMillis()?+?expireTime;
//?鎖不存在,當前線程加鎖成果
if?(jedis.setnx(key,?String.valueOf(expireTs))?==?1)?{
return?true;
}
String?value?=?jedis.get(key);
//如果當前鎖存在,且鎖已過期
if?(value?!=?null?&&?NumberUtils.toLong(value)?<?System.currentTimeMillis())?{
//鎖過期,設置新的過期時間
String?oldValue?=?jedis.getSet(key,?String.valueOf(expireTs));
if?(oldValue?!=?null?&&?oldValue.equals(value))?{
//?多線程并發下,只有一個線程會設置成功
//?設置成功的這個線程,key的舊值一定和設置之前的key的值一致
return?true;
}
}
//?其他情況,加鎖失敗
return?true;
}
乍看之下,沒有什么問題。但仔細分析,有如下問題:value設置為過期時間,就要求各個客戶端嚴格的時鐘同步,這就需要使用到同步時鐘。即使有同步時鐘,分布式的服務器一般來說時間肯定是存在少許誤差的。 鎖過期時,使用 jedis.getSet雖然可以保證只有一個線程設置成功,但是不能保證加鎖和解鎖為同一個客戶端,因為沒有標志鎖是哪個客戶端設置的嘛。
//不是自己加鎖的key,也會被釋放
jedis.del(key);
}
簡單粗暴,直接解鎖,但是不是自己加鎖的,也會被刪除,這好像有點太隨意了吧!
if?(uniqueId.equals(jedis.get(key)))?{
//?如果這時鎖過期自動釋放,又被其他線程加鎖,該線程就會釋放不屬于自己的鎖
jedis.del(key);
}
}
看起來很完美啊,但是如果你判斷的時候鎖是自己持有的,這時鎖超時自動釋放了。然后又被其他客戶端重新上鎖,然后當前線程執行到jedis.del(key),這樣這個線程不就刪除了其他線程上的鎖嘛,好像有點亂套了哦!命令必須保證互斥 設置的key必須要有過期時間,防止崩潰時鎖無法釋放 value使用唯一id標志每個客戶端,保證只有鎖的持有者才可以釋放鎖 加鎖直接使用set命令同時設置唯一id和過期時間;其中解鎖稍微復雜些,加鎖之后可以返回唯一id,標志此鎖是該客戶端鎖擁有;釋放鎖時要先判斷擁有者是否是自己,然后刪除,這個需要redis的lua腳本保證兩個命令的原子性執行。
下面是具體的加鎖和釋放鎖的代碼:@Slf4j
public?class?RedisDistributedLock?{
private?static?final?String?LOCK_SUCCESS?=?"OK";
private?static?final?Long?RELEASE_SUCCESS?=?1L;
private?static?final?String?SET_IF_NOT_EXIST?=?"NX";
private?static?final?String?SET_WITH_EXPIRE_TIME?=?"PX";
//?鎖的超時時間
private?static?int?EXPIRE_TIME?=?5?*?1000;
//?鎖等待時間
private?static?int?WAIT_TIME?=?1?*?1000;
private?Jedis?jedis;
private?String?key;
public?RedisDistributedLock(Jedis?jedis,?String?key)?{
this.jedis?=?jedis;
this.key?=?key;
}
//?不斷嘗試加鎖
public?String?lock()?{
try?{
//?超過等待時間,加鎖失敗
long?waitEnd?=?System.currentTimeMillis()?+?WAIT_TIME;
String?value?=?UUID.randomUUID().toString();
while?(System.currentTimeMillis()?<?waitEnd)?{
String?result?=?jedis.set(key,?value,?SET_IF_NOT_EXIST,?SET_WITH_EXPIRE_TIME,?EXPIRE_TIME);
if?(LOCK_SUCCESS.equals(result))?{
return?value;
}
try?{
Thread.sleep(10);
}?catch?(InterruptedException?e)?{
Thread.currentThread().interrupt();
}
}
}?catch?(Exception?ex)?{
log.error("lock?error",?ex);
}
return?null;
}
public?boolean?release(String?value)?{
if?(value?==?null)?{
return?false;
}
//?判斷key存在并且刪除key必須是一個原子操作
//?且誰擁有鎖,誰釋放
String?script?=?"if?redis.call('get',?KEYS[1])?==?ARGV[1]?then?return?redis.call('del',?KEYS[1])?else?return?0?end";
Object?result?=?new?Object();
try?{
result?=?jedis.eval(script,?Collections.singletonList(key),
Collections.singletonList(value));
if?(RELEASE_SUCCESS.equals(result))?{
log.info("release?lock?success,?value:{}",?value);
return?true;
}
}?catch?(Exception?e)?{
log.error("release?lock?error",?e);
}?finally?{
if?(jedis?!=?null)?{
jedis.close();
}
}
log.info("release?lock?failed,?value:{},?result:{}",?value,?result);
return?false;
}
}
單是一個redis的分布式鎖就有這么多道道,不知道你是否看明白了?留言討論下吧!
前言
現在的業務場景越來越復雜,使用的架構也就越來越復雜,分布式、高并發已經是業務要求的常態。像騰訊系的不少服務,還有CDN優化、異地多備份等處理。說到分布式,就必然涉及到分布式鎖的概念,如何保證不同機器不同線程的分布式鎖同步呢?
實現要點
正確的redis分布式鎖實現
錯誤加鎖方式
錯誤方式一
保證互斥和防止死鎖,首先想到的使用redis的setnx命令保證互斥,為了防止死鎖,鎖需要設置一個超時時間。public?static?void?wrongLock(Jedis?jedis,?String?key,?String?uniqueId,?int?expireTime)?{Long?result?=?jedis.setnx(key,?uniqueId);
if?(1?==?result)?{
//如果該redis實例崩潰,那就無法設置過期時間了
jedis.expire(key,?expireTime);
}
}
在多線程并發環境下,任何非原子性的操作,都可能導致問題。這段代碼中,如果設置過期時間時,redis實例崩潰,就無法設置過期時間。如果客戶端沒有正確的釋放鎖,那么該鎖(永遠不會過期),就永遠不會被釋放。
錯誤方式二
比較容易想到的就是設置值和超時時間為原子原子操作就可以解決問題。那使用setnx命令,將value設置為過期時間不就ok了嗎?public?static?boolean?wrongLock(Jedis?jedis,?String?key,?int?expireTime)?{long?expireTs?=?System.currentTimeMillis()?+?expireTime;
//?鎖不存在,當前線程加鎖成果
if?(jedis.setnx(key,?String.valueOf(expireTs))?==?1)?{
return?true;
}
String?value?=?jedis.get(key);
//如果當前鎖存在,且鎖已過期
if?(value?!=?null?&&?NumberUtils.toLong(value)?<?System.currentTimeMillis())?{
//鎖過期,設置新的過期時間
String?oldValue?=?jedis.getSet(key,?String.valueOf(expireTs));
if?(oldValue?!=?null?&&?oldValue.equals(value))?{
//?多線程并發下,只有一個線程會設置成功
//?設置成功的這個線程,key的舊值一定和設置之前的key的值一致
return?true;
}
}
//?其他情況,加鎖失敗
return?true;
}
乍看之下,沒有什么問題。但仔細分析,有如下問題:
錯誤解鎖方式
解鎖錯誤方式一
直接刪除keypublic?static?void?wrongReleaseLock(Jedis?jedis,?String?key)?{//不是自己加鎖的key,也會被釋放
jedis.del(key);
}
簡單粗暴,直接解鎖,但是不是自己加鎖的,也會被刪除,這好像有點太隨意了吧!
解鎖錯誤方式二
判斷自己是不是鎖的持有者,如果是,則只有持有者才可以釋放鎖。public?static?void?wrongReleaseLock(Jedis?jedis,?String?key,?String?uniqueId)?{if?(uniqueId.equals(jedis.get(key)))?{
//?如果這時鎖過期自動釋放,又被其他線程加鎖,該線程就會釋放不屬于自己的鎖
jedis.del(key);
}
}
看起來很完美啊,但是如果你判斷的時候鎖是自己持有的,這時鎖超時自動釋放了。然后又被其他客戶端重新上鎖,然后當前線程執行到jedis.del(key),這樣這個線程不就刪除了其他線程上的鎖嘛,好像有點亂套了哦!
正確加鎖釋放鎖方式
基本上避免了以上幾種錯誤方式之外,就是正確的方式了。要滿足以下幾個條件:下面是具體的加鎖和釋放鎖的代碼:@Slf4j
public?class?RedisDistributedLock?{
private?static?final?String?LOCK_SUCCESS?=?"OK";
private?static?final?Long?RELEASE_SUCCESS?=?1L;
private?static?final?String?SET_IF_NOT_EXIST?=?"NX";
private?static?final?String?SET_WITH_EXPIRE_TIME?=?"PX";
//?鎖的超時時間
private?static?int?EXPIRE_TIME?=?5?*?1000;
//?鎖等待時間
private?static?int?WAIT_TIME?=?1?*?1000;
private?Jedis?jedis;
private?String?key;
public?RedisDistributedLock(Jedis?jedis,?String?key)?{
this.jedis?=?jedis;
this.key?=?key;
}
//?不斷嘗試加鎖
public?String?lock()?{
try?{
//?超過等待時間,加鎖失敗
long?waitEnd?=?System.currentTimeMillis()?+?WAIT_TIME;
String?value?=?UUID.randomUUID().toString();
while?(System.currentTimeMillis()?<?waitEnd)?{
String?result?=?jedis.set(key,?value,?SET_IF_NOT_EXIST,?SET_WITH_EXPIRE_TIME,?EXPIRE_TIME);
if?(LOCK_SUCCESS.equals(result))?{
return?value;
}
try?{
Thread.sleep(10);
}?catch?(InterruptedException?e)?{
Thread.currentThread().interrupt();
}
}
}?catch?(Exception?ex)?{
log.error("lock?error",?ex);
}
return?null;
}
public?boolean?release(String?value)?{
if?(value?==?null)?{
return?false;
}
//?判斷key存在并且刪除key必須是一個原子操作
//?且誰擁有鎖,誰釋放
String?script?=?"if?redis.call('get',?KEYS[1])?==?ARGV[1]?then?return?redis.call('del',?KEYS[1])?else?return?0?end";
Object?result?=?new?Object();
try?{
result?=?jedis.eval(script,?Collections.singletonList(key),
Collections.singletonList(value));
if?(RELEASE_SUCCESS.equals(result))?{
log.info("release?lock?success,?value:{}",?value);
return?true;
}
}?catch?(Exception?e)?{
log.error("release?lock?error",?e);
}?finally?{
if?(jedis?!=?null)?{
jedis.close();
}
}
log.info("release?lock?failed,?value:{},?result:{}",?value,?result);
return?false;
}
}
單是一個redis的分布式鎖就有這么多道道,不知道你是否看明白了?留言討論下吧!
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的redis分布式锁,面试官请随便问,我都会的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NYOJ 35 表达式求值
- 下一篇: 2019届互联网校招本科薪酬清单