可靠性
首先,爲了確保分佈式鎖可用,咱們至少要確保鎖的實現同時知足如下四個條件:redis
- 互斥性。在任意時刻,只有一個客戶端能持有鎖。
- 不會發生死鎖。即便有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其餘客戶端能加鎖。
- 具備容錯性。只要大部分的Redis節點正常運行,客戶端就能夠加鎖和解鎖。
- 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端本身不能把別人加的鎖給解了。
上鎖:dom
ublic class RedisTool {分佈式
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";函數
/**
* 嘗試獲取分佈式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標識
* @param expireTime 超期時間
* @return 是否獲取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {spa
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);code
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;ip
}部署
}get
能夠看到,咱們加鎖就一行代碼:jedis.set(String key, String value, String nxxx, String expx, int time)
,這個set()方法一共有五個形參:io
-
第一個爲key,咱們使用key來當鎖,由於key是惟一的。
-
第二個爲value,咱們傳的是requestId,不少童鞋可能不明白,有key做爲鎖不就夠了嗎,爲何還要用到value?緣由就是咱們在上面講到可靠性時,分佈式鎖要知足第四個條件解鈴還須繫鈴人,經過給value賦值爲requestId,咱們就知道這把鎖是哪一個請求加的了,在解鎖的時候就能夠有依據。requestId能夠使用
UUID.randomUUID().toString()
方法生成。 -
第三個爲nxxx,這個參數咱們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,咱們進行set操做;若key已經存在,則不作任何操做;
-
第四個爲expx,這個參數咱們傳的是PX,意思是咱們要給這個key加一個過時的設置,具體時間由第五個參數決定。
-
第五個爲time,與第四個參數相呼應,表明key的過時時間。
總的來講,執行上面的set()方法就只會致使兩種結果:1. 當前沒有鎖(key不存在),那麼就進行加鎖操做,並對鎖設置個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不作任何操做。
心細的童鞋就會發現了,咱們的加鎖代碼知足咱們可靠性裏描述的三個條件。首先,set()加入了NX參數,能夠保證若是已有key存在,則函數不會調用成功,也就是隻有一個客戶端能持有鎖,知足互斥性。其次,因爲咱們對鎖設置了過時時間,即便鎖的持有者後續發生崩潰而沒有解鎖,鎖也會由於到了過時時間而自動解鎖(即key被刪除),不會發生死鎖。最後,由於咱們將value賦值爲requestId,表明加鎖的客戶端請求標識,那麼在客戶端在解鎖的時候就能夠進行校驗是不是同一個客戶端。因爲咱們只考慮Redis單機部署的場景,因此容錯性咱們暫不考慮。
解鎖:
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 釋放分佈式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @return 是否釋放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { 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代碼的功能是什麼呢?其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,若是相等則刪除鎖(解鎖)。
那麼爲何要使用Lua語言來實現呢?由於要確保上述操做是原子性的。關於非原子性會帶來什麼問題,
能夠閱讀【解鎖代碼-錯誤示例2】 。那麼爲何執行eval()方法能夠確保原子性,源於Redis的特性,下面是官網對eval命令的部分解釋: