一種redis命令搞定基於redis的分散式鎖

銘銘erom發表於2017-09-12

基於redis的分散式鎖專案肯定經常用到,主要是為了避免重複處理,或者由於併發帶來的髒資料或者錯誤的處理。

使用鎖就必須注意一下幾點:
1、互斥,同一時間不能有多個client能獲取到鎖
2、不能發生死鎖,不能因為有鎖的client因為崩潰或者解鎖命令請求失敗導致無法釋放鎖
3、自己只能解自己上的鎖,不能刪除別人的鎖

下面介紹一下程式碼為什麼這麼寫
1、為什麼不用setnx和expire兩個命令來實現加鎖,因為如果再執行setnx後加過期時間崩潰了,就無法解鎖。
2、為什麼需要使用隨機字串來做lockvalue,主要是為了防止不同client的鎖都是一樣的,防止client誤刪
3、為什麼還需要Lua指令碼程式碼來實現刪除鎖,縱然我們在刪除之前比對了鎖,但是get和del之間不是原子性的,所以也防止刪除別人的鎖,用lua指令碼程式碼實現原子操作

下面是一個不侵佔業務的通過redis分散式鎖來執行的方法程式碼,當然這裡只是參考,一種解決問題的方案,至於業務實現怎麼寫,大家可以隨便發揮。

/**
 * 分散式鎖
 *
 * @param r             繼承於Redis
 * @param lockKey       分散式鎖的key
 * @param voidMethod    獲得到鎖後需要執行的方法
 * @param expireSeconds 鎖定時間 單位/s(需要評估方法執行時間)
 * @param retrySeconds  重試間隔 單位/s
 * @throws ServiceException
 */
public <R extends Redis> void lock(R r, String lockKey, VoidMethodInterface voidMethod, int expireSeconds, int retrySeconds) throws ServiceException {
    long begin = 0L;
    long retryMills = retrySeconds * 1000;
    //獲取隨機字串,避免lockValue相同
    String lockValue = UUID.randomUUID().toString();
    while (begin <= retryMills) {
        String response = r.set(lockKey, lockValue, "nx", "ex", expireSeconds);
        if (response != null) {
            logger.debug("lock method exec begin");
            try {
                voidMethod.exec();
            } catch (Exception e) {
                logger.error("method exec failed ,msg:%s", e.getMessage());
            } finally {
                String value = r.get(lockKey);
                //當快取裡還有該key對應的值時,才去刪除鎖,避免執行時間過長導致鎖被釋放
                if (!StringUtils.isEmpty(value) && lockValue.equals(value)) {
                    // 避免若在此時,這把鎖突然不是這個客戶端的,則會誤解鎖
                    String script = "if redis.call(`get`, KEYS[1]) == ARGV[1] then return redis.call(`del`, KEYS[1]) else return 0 end";
                    r.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
                }
            }
            logger.debug("lock method exec success");
            return;
        } else {
            long waitMills = random.nextInt(WAIT_INTERVAL_MIN_MILLS, WAIT_INTERVAL_MAX_MILLS);
            try {
                Thread.sleep(waitMills);
            } catch (InterruptedException ex) {
                throw new UnexpectedStateException(ex);
            }
            begin = begin + waitMills;
            logger.debug("等待獲取鎖,當前等待時間:%sms", begin);
        }
    }
    logger.error("等待獲取鎖超時");
}


相關文章