為什么要用Redis
分布式環(huán)境考慮加鎖,可以想到如下方法
- 數(shù)據(jù)庫(kù)字段
- 基于Zookeeper管理機(jī)器
- 基于緩存,可以適用Redis
基于數(shù)據(jù)庫(kù)的方式個(gè)人感覺意義不大膜蛔,因?yàn)榇蠖鄶?shù)鎖說(shuō)需要保存的值非常少璧微,為此建庫(kù)建表意義不大仰猖,而且查詢速度還比較慢照皆。性能不佳。
而基于Zookeeper,可以對(duì)于每個(gè)客戶端對(duì)某個(gè)方法加鎖時(shí)典阵,在zookeeper上的與該方法對(duì)應(yīng)的指定節(jié)點(diǎn)的目錄下,生成一個(gè)唯一的瞬時(shí)有序節(jié)點(diǎn)镊逝。 判斷是否獲取鎖的方式很簡(jiǎn)單壮啊,只需要判斷有序節(jié)點(diǎn)中序號(hào)最小的一個(gè)。 當(dāng)釋放鎖的時(shí)候撑蒜,只需將這個(gè)瞬時(shí)節(jié)點(diǎn)刪除即可他巨。同時(shí)充坑,其可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無(wú)法釋放减江,而產(chǎn)生的死鎖問題染突。 問題是較為麻煩,而且效率沒有使用緩存高辈灼。
如果基于緩存呢?首先性能比較好讀取很快份企,而且像Redis都是已有部署好的集群可以直接使用。
實(shí)現(xiàn)
主要是使用SETNX()方法 全稱就是SET IF NOT EXIST
- 返回1 說(shuō)明在Redis中set了key巡莹,獲得鎖
- 返回0 說(shuō)明該key已經(jīng)被set司志,不能獲得鎖
看似很美好 直接一句話就可以實(shí)現(xiàn)了 但是其實(shí)存在死鎖的問題
死鎖問題
無(wú)論這個(gè)鎖是干什么用的 都要在使用后放開鎖 否則會(huì)讓其他競(jìng)爭(zhēng)者永久等待
對(duì)于這個(gè)問題一般都是考慮使用設(shè)置超時(shí)來(lái)實(shí)現(xiàn)的
錯(cuò)誤的處理
先來(lái)看幾個(gè)我親自犯過(guò)的錯(cuò)誤 一定認(rèn)真看一下 可能你第一次寫也是這樣考慮的 如果實(shí)在等不急可以先去偷看一下正確答案。
錯(cuò)誤A
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在這里程序突然崩潰降宅,則無(wú)法設(shè)置過(guò)期時(shí)間骂远,將發(fā)生死鎖
jedis.expire(lockKey, expireTime);
}
這是是第一次寫的時(shí)候出現(xiàn)的問題 先通過(guò)一條命令嘗試加鎖再設(shè)置過(guò)期時(shí)間,但是這里有個(gè)坑腰根,就是如果在嘗試加鎖完成以后程序崩了激才。GG這個(gè)鎖這輩子也釋放不了了,標(biāo)準(zhǔn)的死鎖额嘿。
錯(cuò)誤B
public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);
// 如果當(dāng)前鎖不存在瘸恼,返回加鎖成功
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}
// 如果鎖存在,獲取鎖的過(guò)期時(shí)間
String currentValueStr = jedis.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 鎖已過(guò)期册养,獲取上一個(gè)鎖的過(guò)期時(shí)間东帅,并設(shè)置現(xiàn)在鎖的過(guò)期時(shí)間
String oldValueStr = jedis.getSet(lockKey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考慮多線程并發(fā)的情況,只有一個(gè)線程的設(shè)置值和當(dāng)前值相同球拦,它才有權(quán)利加鎖
return true;
}
}
// 其他情況靠闭,一律返回加鎖失敗
return false;
}
這里看似很完美,通過(guò)對(duì)Value設(shè)置時(shí)間戳的方式防止之前的線程掛掉的情況坎炼,但是我們?cè)倏匆幌箩尫沛i的方法
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
// 判斷加鎖與解鎖是不是同一個(gè)客戶端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此時(shí)愧膀,這把鎖突然不是這個(gè)客戶端的,則會(huì)誤解鎖
jedis.del(lockKey);
}
}
設(shè)想一個(gè)情況点弯,線程A加鎖并設(shè)置過(guò)期時(shí)間扇调。突然線程A掛了這是線程B苦苦等到了過(guò)期時(shí)間成功拿到了鎖。正準(zhǔn)備爽一下的時(shí)候抢肛,突然A滿血復(fù)活了狼钮,可能會(huì)“正常”的釋放鎖捡絮。B就不能忍了熬芜,我等你這么長(zhǎng)時(shí)間好不容易拿到了鎖,你回來(lái)直接給我釋放了福稳。
A加鎖 - A死亡 - 超時(shí) - B加鎖 - A復(fù)活 - A釋放鎖(這時(shí)B還在執(zhí)行)
說(shuō)了這么多涎拉,都感覺Redis是不是不適合做分布式鎖啊!那我們來(lái)看一下正確答案鼓拧。
正確答案
這里我也是學(xué)習(xí)了別人的代碼半火,需要使用Lua腳本。
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
第二行代碼季俩,我們將Lua代碼傳到j(luò)edis.eval()方法里钮糖,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId酌住。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行店归。
那么這段Lua代碼的功能是什么呢?其實(shí)很簡(jiǎn)單酪我,首先獲取鎖對(duì)應(yīng)的value值消痛,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)都哭。那么為什么要使用Lua語(yǔ)言來(lái)實(shí)現(xiàn)呢秩伞?因?yàn)橐_保上述操作是原子性的。
因?yàn)椋篹val命令執(zhí)行Lua代碼的時(shí)候质涛,Lua代碼將被當(dāng)成一個(gè)命令去執(zhí)行稠歉,并且直到eval命令執(zhí)行完成,Redis才會(huì)執(zhí)行其他命令汇陆。保證了其原子性怒炸。
最后
其實(shí)Redis本身實(shí)現(xiàn)的分布式鎖的確存在各種問題。有人認(rèn)為它并不安全
但是對(duì)于Redis是多機(jī)部署的毡代,那么可以嘗試使用Redisson實(shí)現(xiàn)分布式鎖阅羹,這是Redis官方提供的Java組件,這里有一篇網(wǎng)易技術(shù)的博客可以看一下.