應用場景
分布式系統中,面對高并發場景,又對數據一致性有一定要求的情況下,使用分布式鎖。例如商城中下單扣庫存這種情況。
解決方案
基于數據庫
例如:
select * from mall_spu where id=111 for update
例如:專門建一張表用來實現。例如以類名、方法名、數據ID作為唯一主鍵,org.leo.mall.order.OrderServer.addOrder.skuId.111,方法執行的時候,如果能插入成功,代表拿到鎖,如果報主鍵沖突,則拿鎖失敗。
基于Zookeeper
以類、方法、數據ID作為目錄,請求取順序節點&節點列表,如果自己的節點最小,說明拿鎖成功。而且還可以通過watch,在鎖釋放的時候重新拿鎖。因為是臨時鎖,所以主動釋放,或者session失效都可以釋放鎖,避免死鎖產生。
性能差點,因為Zookeeper的操作都在主節點上。
基于redis
本文主要講講應用的一些變革。
1、加鎖。
原來的做法是:
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加上超時時間。
問題在于:sennx和expire不是原子操作,萬一expire的時候崩了,這條命令永遠不過期了。
所以后來基于Redis的升級,有了下面正確的加鎖方法:
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;
}
其實就是用Redis提供的一條set命令,替代了前面的setnx、expire兩條命令,保證了原子性。
NX是指Key不存在就新增。PX是指設置超時時間。
requestId是為了后面解鎖用。
2、解鎖
解鎖看著最簡單,其實蠻復雜。
腦子里第一想法就是:
public static void releaseLock(String key){
RedisClient.del(key);
}
這個危險性在于任何人都可以解鎖!比如A請求加了鎖:spu_id_111。B請求也要對111進行操作,一看鎖被占了,直接del,然后自己拿鎖——雖然在程序開發上講,沒有哪個傻子會這么干!!
所以這才有了第二種做法:
A請求加鎖的時候,通過UUID、Random等方法生成隨機數requestId。
public static void releaseLock(String key,String requestId){
String result=RedisClient.get(key);//步驟1
if(result.equals(requestId)){//步驟2
//二者相等,說明加解鎖的請求是同一個
RedisClient.del(key);//步驟3
}
}
看似很嚴謹,但是問題出在哪呢?還是出在操作不是原子性上。
A請求執行步驟1、2完畢,還未執行步驟2時,鎖過期了,自動解鎖!這時B請求加鎖必然成功,而A請求繼續執行步驟3,把B請求的鎖給刪了。
正確的做法如下:
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通過eval命令執行LUA腳本是原子性的特性。
再然后就是使用Redisson實現了,這個適用于集群部署的Redis。
我在實際使用Redis分布式鎖的時候遇到過一種情況。使用分布式鎖后,要調用第三方接口,從而導致整個流程時間偏長,鎖過期的情況下還沒有執行完,當時的處理方式是加大了過期時間。
如果使用Redisson,因為有看門狗機制,就很好地解決了這個問題。看門狗會定時去檢查,如果請求實例還在則自動去延長超時時間。不過這帶來的問題一定是性能的下降,所以當時我們還是采用了粗暴的延長設置過期時間來解決此類問題。
Redisson也是個可重入鎖,因為鎖的內容除了key、實例ID之外還有數字Value,這樣一來同樣的實例多次拿鎖,Value+1,釋放鎖,Value-1即可。