分散式鎖之redis實現
對“鎖”大家肯定都不陌生,鎖是針對多執行緒情況下對資源訪問的控制,初學java時候,就知道synchronize和lock,synchronize是重量級鎖,lock是輕量級的鎖,巴拉巴拉。。。但是它們兩都是針對單個jvm來說的,現在稍微大點的網站都是多臺伺服器,通過nginx等負載均衡到多個伺服器節點,這樣的話,就得使用分散式鎖了,常見的分散式鎖有基於redis或者zookeeper,這裡討論下基於redis實現分散式鎖及其會遇到的問題。
1.0 setnx
根據redis提供的setnx命令,可以當做互斥鎖來使用,成功了會返回1,失敗返回0,然後做一些操作,最後刪除key,虛擬碼如下:
if(1 == setnx(key,value)){
dosomething;
del(key);
}
考慮的更完善點呢,可以加入“鎖超時”的情況,假如一個獲得鎖許可權的節點,因為一些其他因素導致遲遲沒有執行del(key)的操作,導致鎖無法釋放,會阻塞依賴該key的業務流程。虛擬碼如下:
if(1 == setnx(key,value)){
expire(key,timeOutSecond)
dosomething;
del(key);
}
加入了鎖超時的程式碼,看似是“很完善”了,但是,還是有一些問題的,setnx和expire不是原子性、del會誤刪
2.0 原子性和誤刪操作
2.1 操作的原子性
先說下原子性,在1.0中的程式碼,setnx和expire不是原子性,極端情況,第一行程式碼,setnx(key,value)執行完之後,再執行第二行expire的時候,節點GG思密達了,第二行程式碼指令就沒有抵達redis,那這樣的話,之前設定的key就永恆不朽了。
如何處理這個問題呢?redis 2.6.12及其以上版本,提供了原子性操作的api:
set(key,value,ex second,nx)
set四個引數的這種等於 setnx+expire兩行程式碼,還保證了原子性
這裡吐槽一下啊,redis不僅提供了setnx、還提供了setex,還還提供了psetex,一個set(key,value,ex|px second,nx)命令不就搞定了嗎,弄這麼多幹嘛呢?
2.2 誤刪key
再說下誤刪操作,還是1.0的虛擬碼,假如執行緒A獲得了鎖,超時時間是30秒,假如執行緒A在30秒內未執行完成,則鎖超時,鎖會被其他執行緒獲取;假如獲取到鎖的是執行緒B,執行緒B在執行程式碼塊的過程中,執行緒A終於執行完了並刪除了鎖,那這時就是誤刪了,這個時間執行緒A刪的鎖其實是執行緒B的鎖了。
如何處理這種誤刪操作呢?可以把當前執行緒的執行緒ID作為value,在刪除鎖之前比對下key的value是不是當前執行緒的執行緒ID,如果是,才會去刪除鎖。虛擬碼如下:
String currendThreadId = Thread.currendThread.getId();
if("ok".equals(set(key,currendThreadId,ex second,nx))){
doSomething;
if(currendThreadId.equals(get(key))){
del(key)
}
}
上面的程式碼,其實還是隱含一個問題--判斷鎖的值和刪除鎖,不是原子性,要達到效果,redis的現成api是沒有的,得需要使用redis指令碼了
String script_lua = "if (redis.call('get',KEYS[1]) == redis.call('get',ARGV[1])) then return redis.call('del',KEYS[1]) else return 0 end";
redisClient.eval(script_lua,Collections.singletoList(key),Collections.singletoList(currendThreadId));
這行程式碼的效果等同於在redis-cli中執行
2.3 存在併發可能性
雖然使用指令碼的方式可以有效防止誤刪的操作,但是還是會有併發問題--執行緒A和執行緒B同時都在操作,這顯然不合理,為解決這個問題,那只有限制B不能進入了。
這裡就需要引入一個“守護執行緒”的東西了,守護執行緒會線上程A即將過期的時候,去主動給執行緒A“續命”,當執行緒A執行完畢,再關掉守護執行緒;即使執行緒A所在的節點GG思密達了,因為執行緒A和守護執行緒是在一個節點的,守護執行緒也會GG的,鎖到期後,會自動釋放。
到這裡才算是把redis的分散式鎖比較全面的說完。