應(yīng)用場(chǎng)景
分布式系統(tǒng)中,面對(duì)高并發(fā)場(chǎng)景,又對(duì)數(shù)據(jù)一致性有一定要求的情況下,使用分布式鎖。例如商城中下單扣庫(kù)存這種情況。
解決方案
基于數(shù)據(jù)庫(kù)
例如:
select * from mall_spu where id=111 for update
例如:專(zhuān)門(mén)建一張表用來(lái)實(shí)現(xiàn)。例如以類(lèi)名、方法名、數(shù)據(jù)ID作為唯一主鍵,org.leo.mall.order.OrderServer.addOrder.skuId.111,方法執(zhí)行的時(shí)候,如果能插入成功,代表拿到鎖,如果報(bào)主鍵沖突,則拿鎖失敗。
基于Zookeeper
以類(lèi)、方法、數(shù)據(jù)ID作為目錄,請(qǐng)求取順序節(jié)點(diǎn)&節(jié)點(diǎn)列表,如果自己的節(jié)點(diǎn)最小,說(shuō)明拿鎖成功。而且還可以通過(guò)watch,在鎖釋放的時(shí)候重新拿鎖。因?yàn)槭桥R時(shí)鎖,所以主動(dòng)釋放,或者session失效都可以釋放鎖,避免死鎖產(chǎn)生。
性能差點(diǎn),因?yàn)閆ookeeper的操作都在主節(jié)點(diǎn)上。
基于redis
本文主要講講應(yīng)用的一些變革。
1、加鎖。
原來(lái)的做法是:
public static boolean getLock(String key,int expireTime){
Long result=RedisClient.setnx(key,"");
if(result!=1){return false}
RedisClient.expire(key,expireTime);
return true;
}
setnx加鎖,成功后用expire加上超時(shí)時(shí)間。
問(wèn)題在于:sennx和expire不是原子操作,萬(wàn)一expire的時(shí)候崩了,這條命令永遠(yuǎn)不過(guò)期了。
所以后來(lái)基于Redis的升級(jí),有了下面正確的加鎖方法:
public static boolean getLock(String key,String requestId,int expireTime){
String result=RedisClient.set(key,requestId,"NX","PK",expireTime);
if(result.equals("OK")){return true;}
return false;
}
其實(shí)就是用Redis提供的一條set命令,替代了前面的setnx、expire兩條命令,保證了原子性。
NX是指Key不存在就新增。PX是指設(shè)置超時(shí)時(shí)間。
requestId是為了后面解鎖用。
2、解鎖
解鎖看著最簡(jiǎn)單,其實(shí)蠻復(fù)雜。
腦子里第一想法就是:
public static void releaseLock(String key){
RedisClient.del(key);
}
這個(gè)危險(xiǎn)性在于任何人都可以解鎖!比如A請(qǐng)求加了鎖:spu_id_111。B請(qǐng)求也要對(duì)111進(jìn)行操作,一看鎖被占了,直接del,然后自己拿鎖——雖然在程序開(kāi)發(fā)上講,沒(méi)有哪個(gè)傻子會(huì)這么干!!
所以這才有了第二種做法:
A請(qǐng)求加鎖的時(shí)候,通過(guò)UUID、Random等方法生成隨機(jī)數(shù)requestId。
public static void releaseLock(String key,String requestId){
String result=RedisClient.get(key);//步驟1
if(result.equals(requestId)){//步驟2
//二者相等,說(shuō)明加解鎖的請(qǐng)求是同一個(gè)
RedisClient.del(key);//步驟3
}
}
看似很?chē)?yán)謹(jǐn),但是問(wèn)題出在哪呢?還是出在操作不是原子性上。
A請(qǐng)求執(zhí)行步驟1、2完畢,還未執(zhí)行步驟2時(shí),鎖過(guò)期了,自動(dòng)解鎖!這時(shí)B請(qǐng)求加鎖必然成功,而A請(qǐng)求繼續(xù)執(zhí)行步驟3,把B請(qǐng)求的鎖給刪了。
正確的做法如下:
public static boolean releaseLock(String key,String requestId){
String luaCommand="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
List<String> keyList=Lists.newArrayList(key);//這里我用的是Guava
List<String> valList=Lists.newArrayList(requestId);
Object result=RedisClient.eval(luaCommand,keyList,valList);
if(result.equals(1L)){return true;}
return false;
}
利用的就是Redis通過(guò)eval命令執(zhí)行LUA腳本是原子性的特性。
再然后就是使用Redisson實(shí)現(xiàn)了,這個(gè)適用于集群部署的Redis。
我在實(shí)際使用Redis分布式鎖的時(shí)候遇到過(guò)一種情況。使用分布式鎖后,要調(diào)用第三方接口,從而導(dǎo)致整個(gè)流程時(shí)間偏長(zhǎng),鎖過(guò)期的情況下還沒(méi)有執(zhí)行完,當(dāng)時(shí)的處理方式是加大了過(guò)期時(shí)間。
如果使用Redisson,因?yàn)橛锌撮T(mén)狗機(jī)制,就很好地解決了這個(gè)問(wèn)題。看門(mén)狗會(huì)定時(shí)去檢查,如果請(qǐng)求實(shí)例還在則自動(dòng)去延長(zhǎng)超時(shí)時(shí)間。不過(guò)這帶來(lái)的問(wèn)題一定是性能的下降,所以當(dāng)時(shí)我們還是采用了粗暴的延長(zhǎng)設(shè)置過(guò)期時(shí)間來(lái)解決此類(lèi)問(wèn)題。
Redisson也是個(gè)可重入鎖,因?yàn)殒i的內(nèi)容除了key、實(shí)例ID之外還有數(shù)字Value,這樣一來(lái)同樣的實(shí)例多次拿鎖,Value+1,釋放鎖,Value-1即可。