1. 程式人生 > 實用技巧 >redis分散式鎖原理與實現

redis分散式鎖原理與實現

分散式鎖原理
分散式鎖,是控制分散式系統之間同步訪問共享資源的一種方式。在分散式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分散式鎖。

使用setnx、getset、expire、del這4個redis命令實現

setnx 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。 命令格式:SETNX key value;使用:只在鍵 key 不存在的情況下,將鍵 key 的值設定為 value 。若鍵 key 已經存在, 則 SETNX 命令不做任何動作。返回值:命令在設定成功時返回 1 ,設定失敗時返回 0 。

getset 命令格式:GETSET key value,將鍵 key 的值設為 value ,並返回鍵 key 在被設定之前的舊的value。返回值:如果鍵 key 沒有舊值, 也即是說, 鍵 key 在被設定之前並不存在, 那麼命令返回 nil 。當鍵 key 存在但不是字串型別時,命令返回一個錯誤。
expire 命令格式:EXPIRE key seconds,使用:為給定 key 設定生存時間,當 key 過期時(生存時間為 0 ),它會被自動刪除。返回值:設定成功返回 1 。 當 key 不存在或者不能為 key 設定生存時間時(比如在低於 2.1.3 版本的 Redis 中你嘗試更新 key 的生存時間),返回 0 。
del 命令格式:DEL key [key …],使用:刪除給定的一個或多個 key ,不存在的 key 會被忽略。返回值:被刪除 key 的數量。
redis 分散式鎖原理一
原理圖如下

過程分析:

A嘗試去獲取鎖lockkey,通過setnx(lockkey,currenttime+timeout)命令,對lockkey進行setnx,將value值設定為當前時間+鎖超時時間;
如果返回值為1,說明redis伺服器中還沒有lockkey,也就是沒有其他使用者擁有這個鎖,A就能獲取鎖成功;
在進行相關業務執行之前,先執行expire(lockkey),對lockkey設定有效期,防止死鎖。因為如果不設定有效期的話,lockkey將一直存在於redis中,其他使用者嘗試獲取鎖時,執行到setnx(lockkey,currenttime+timeout)時,將不能成功獲取到該鎖;

執行相關業務;
釋放鎖,A完成相關業務之後,要釋放擁有的鎖,也就是刪除redis中該鎖的內容,del(lockkey),接下來的使用者才能進行重新設定鎖新值。
程式碼實現:

public void redis1() {
        log.info("關閉訂單定時任務啟動");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout", "5000"));
        //這個方法的缺陷在這裡,如果setnx成功後,鎖已經存到Redis裡面了,伺服器異常關閉重啟,將不會執行closeOrder,也就不會設定鎖的有效期,這樣的話鎖就不會釋放了,就會產生死鎖
        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
        if (setnxResult != null && setnxResult.intValue() == 1) {
            //如果返回值為1,代表設定成功,獲取鎖
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        } else {
            log.info("沒有獲得分散式鎖:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }
        log.info("關閉訂單定時任務結束");
    }
private void closeOrder(String lockName) {
        //對鎖設定有效期
        RedisShardedPoolUtil.expire(lockName, 5);//有效期為5秒,防止死鎖
        log.info("獲取鎖:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
        //執行業務
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "2"));
        iOrderService.closeOrder(hour);
        //執行完業務後,釋放鎖
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        log.info("釋放鎖:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
        log.info("=================================");
    }

缺陷:
如果A在setnx成功後,A成功獲取鎖了,也就是鎖已經存到Redis裡面了,此時伺服器異常關閉或是重啟,將不會執行closeOrder,也就不會設定鎖的有效期,這樣的話鎖就不會釋放了,就會產生死鎖。

解決方法:
關閉Tomcat有兩種方式,一種通過溫柔的執行shutdown關閉,一種通過kill殺死程序關閉

//通過溫柔的執行shutdown關閉時,以下的方法會在關閉前執行,即可以釋放鎖,而對於通過kill殺死程序關閉時,以下方法不會執行,即不會釋放鎖
   //這種方式釋放鎖的缺點在於,如果關閉的鎖過多,將造成關閉伺服器耗時過長
    @PreDestroy
    public void delLock() {
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
    }

redis 分散式鎖原理2(優化版)

為了解決原理1中會出現的死鎖問題,提出原理2雙重防死鎖,可以更好解決死鎖問題。
原理圖如下:

過程分析:

當A通過setnx(lockkey,currenttime+timeout)命令能成功設定lockkey時,即返回值為1,過程與原理1一致;
當A通過setnx(lockkey,currenttime+timeout)命令不能成功設定lockkey時,這是不能直接斷定獲取鎖失敗;因為我們在設定鎖時,設定了鎖的超時時間timeout,噹噹前時間大於redis中儲存鍵值為lockkey的value值時,可以認為上一任的擁有者對鎖的使用權已經失效了,A就可以強行擁有該鎖;具體判定過程如下;
A通過get(lockkey),獲取redis中的儲存鍵值為lockkey的value值,即獲取鎖的相對時間lockvalueA
lockvalueA!=null && currenttime>lockvalue,A通過當前的時間與鎖設定的時間做比較,如果當前時間已經大於鎖設定的時間臨界,即可以進一步判斷是否可以獲取鎖,否則說明該鎖還在被佔用,A就還不能獲取該鎖,結束,獲取鎖失敗;
步驟4返回結果為true後,通過getSet設定新的超時時間,並返回舊值lockvalueB,以作判斷,因為在分散式環境,在進入這裡時可能另外的程序獲取到鎖並對值進行了修改,只有舊值與返回的值一致才能說明中間未被其他程序獲取到這個鎖
lockvalueB == null || lockvalueA==lockvalueB,判斷:若果lockvalueB為null,說明該鎖已經被釋放了,此時該程序可以獲取鎖;舊值與返回的lockvalueB一致說明中間未被其他程序獲取該鎖,可以獲取鎖;否則不能獲取鎖,結束,獲取鎖失敗。
程式碼實現:

public void redis2() {
        log.info("關閉訂單定時任務啟動");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout", "5000"));
        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
        if (setnxResult != null && setnxResult.intValue() == 1) {
            //如果返回值為1,代表設定成功,獲取鎖
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        } else {
            //未獲取到鎖,繼續判斷,判斷時間戳,看是否可以重置並獲取到鎖
            String lockValueStr = RedisShardedPoolUtil.get(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            //通過當前的時間與鎖設定的時間做比較,如果當前時間已經大於鎖設定的時間臨界,即可以進一步判斷是否可以獲取鎖,否則說明該鎖還在被佔用,不能獲取該鎖
            if (lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)) {
                //通過getSet設定新的超時時間,並返回舊值,以作判斷,因為在分散式環境,在進入這裡時可能另外的程序獲取到鎖並對值進行了修改,只有舊值與返回的值一致才能說明中間未被其他程序獲取到這個鎖
                String getSetResult = RedisShardedPoolUtil.getSet(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
                //再次用當前時間戳getset。
                //返回給定的key的舊值,與舊值判斷,是否可以獲取鎖
                //當key沒有舊值時,即key不存在時,返回nil ->獲取鎖
                //這裡我們set了一個新的value值,獲取舊的值。
                //若果getSetResult為null,說明該鎖已經被釋放了,此時該程序可以獲取鎖;舊值與返回的getSetResult一致說明中間未被其他程序獲取該鎖,可以獲取鎖
                if (getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr, getSetResult))) {
                    //真正獲取到鎖
                    closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                } else {
                    log.info("沒有獲得分散式鎖:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }
            } else {
                log.info("沒有獲得分散式鎖:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
        }
        log.info("關閉訂單定時任務結束");
    }
    private void closeOrder(String lockName) {
        //對鎖設定有效期
        RedisShardedPoolUtil.expire(lockName, 5);//有效期為5秒,防止死鎖
        log.info("獲取鎖:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
        //執行業務
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "2"));
        iOrderService.closeOrder(hour);
        //執行完業務後,釋放鎖
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        log.info("釋放鎖:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
        log.info("=================================");
    }

優化點:

加入了超時時間判斷鎖是否超時了,及時A在成功設定了鎖之後,伺服器就立即出現宕機或是重啟,也不會出現死鎖問題;因為B在嘗試獲取鎖的時候,如果不能setnx成功,會去獲取redis中鎖的超時時間與當前的系統時間做比較,如果當前的系統時間已經大於鎖超時時間,說明A已經對鎖的使用權失效,B能繼續判斷能否獲取鎖,解決了redis分散式鎖的死鎖問題。