Github源碼:
https://github.com/z521598/redis-lock
實現原理:
1.setnx
Redis的setnx指令(文檔參考),setnx意為SET if Not eXists,命令格式:setnx $key $value
如果此key不存在,則設置值為value,返回值為1;如果此key存在,則不設置,返回值為0。如下圖:
127.0.0.1:6379[1]> setnx key v (integer) 1 127.0.0.1:6379[1]> setnx key v2 (integer) 0
redis是單線程的,是線程安全的,setnx指令由於上述的特性能夠滿足高並發情況下的對於鎖的需求。
2.SpringData-redis
springData-redis是向Redis發送命令以及接受數據的高層次抽象的模版方法,簡單理解為“使用java向redis發送命令以及接受數據的客戶端”(文檔參考)
配置文件:https://github.com/z521598/redis-lock/blob/master/src/main/resources/applicationContext.xml
V1
說明:
最簡單的最粗糙的鎖實現,實現了2個方法。
方法1:獲取鎖,public UUID acquire(String lockKey, long acquireTimeoutInMillis, long lockExpiryInMillis)
參數說明:lockKey為鎖的key;acquireTimeoutInMillis為獲取鎖的等待時間,如果超過此時間就放棄鎖;lockExpiryInMillis為鎖的過期時間。
返回值:鎖對應的值,就是命令中“setnx key value”的value。
思路:
1.調用SpringData-Redis的setIfAbsent(lockKey, value)方法(就是命令行中的setnx),value為隨機的UUID。
2.如果返回true,則說明已經獲取的鎖,則繼續設置超時時間,返回設置的UUID。
如果返回false,則說明未獲取到鎖,則休眠100ms,並記錄總休眠的時間,如果lockExpiryInMillis大於等於記錄總休眠的時間,則說明未獲取到鎖,返回null。
方法2:釋放鎖,public void release(String lockKey, UUID uuid)
參數說明:lockKey為鎖的key;uuid為鎖的value。
思路:檢查鎖的value是否為uuid,如果相等,則釋放,如果不相等,則什么都不做;防止釋放了其他線程獲取的鎖。
明顯的缺點:
第一步,獲取鎖,第二步,然后設置超時時間。這是2步操作,不是原子性操作,如果第一步操作之后,程序崩潰了或者掉電了或者redis恰巧進行了主從切換等等原因,第二步無法正常執行,這樣這個鎖就永遠得不到釋放。
代碼:
public UUID acquire(String lockKey, long acquireTimeoutInMillis, long lockExpiryInMillis) throws InterruptedException { UUID uuid = UUID.randomUUID(); long timeout = 0L; while (timeout < acquireTimeoutInMillis) { if (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid.toString())) { redisTemplate.expire(lockKey, lockExpiryInMillis, TimeUnit.MILLISECONDS); return uuid; } TimeUnit.MILLISECONDS.sleep(DEFAULT_ACQUIRE_RESOLUTION_MILLIS); timeout += DEFAULT_ACQUIRE_RESOLUTION_MILLIS; } return null; }
V2
說明:
大體與V1相同,但是鎖的value是"過期的時間",如果獲取鎖的時候,發現過期時間小於now,則視為鎖已經過期。
缺點:(極少出現的情況)
當redis是主從形式的情況下,獲取鎖之后,master宕機,slave接管,但是這個時候“新master”還未同步鎖的key。在這個時候,其他線程去獲取鎖,發現無此key,則獲取了不應該獲取的鎖,這樣就會引起不安全的情況。
代碼:
鎖實現:https://github.com/z521598/redis-lock/tree/master/src/main/java/com/redis/lock/sdata/v2
V3
敬請期待