1. 程式人生 > >分散式學習之五:redis分步式鎖

分散式學習之五:redis分步式鎖

前言

分散式鎖一般有三種實現方式:1. 資料庫樂觀鎖;2. 基於Redis的分散式鎖;3. 基於ZooKeeper的分散式鎖。本篇部落格將介紹第二種方式,基於Redis實現分散式鎖。雖然網上已經有各種介紹Redis分散式鎖實現的部落格,然而他們的實現卻有著各種各樣的問題,本篇部落格將詳細介紹如何正確地實現Redis分散式鎖。 需滿足如下條件: -互斥性。在任意時刻,只有一個客戶端能持有鎖。  - 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。  - 具有容錯性。只要大部分的Redis節點正常執行,客戶端就可以加鎖和解鎖。  - 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。

加鎖

正確方式:

首先我們要通過Maven引入Jedis開源元件,在pom.xml檔案加入下面的程式碼:

<dependency>     <groupId>redis.clients</groupId>     <artifactId>jedis</artifactId>     <version>2.9.0</version> </dependency>

注:如果在springBoot,可以使用springBoot自己封裝的redisTemplate.自行百度。

加鎖程式碼:

public 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 請求標識 (可通過UUID生成唯一標識)
     * @param expireTime 超期時間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

錯誤例項1:  比較常見的錯誤示例就是使用jedis.setnx()和jedis.expire()組合實現加鎖,程式碼如下:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在這裡程式突然崩潰,則無法設定過期時間,將發生死鎖
        jedis.expire(lockKey, expireTime);
    }
}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是給鎖加一個過期時間。乍一看好像和前面的set()方法結果一樣,然而由於這是兩條Redis命令,不具有原子性,如果程式在執行完setnx()之後突然崩潰,導致鎖沒有設定過期時間。那麼將會發生死鎖。網上之所以有人這樣實現,是因為低版本的jedis並不支援多引數的set()方法。

解鎖程式碼

正確方式:

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指令碼程式碼,第二行程式碼,我們將Lua程式碼傳到jedis.eval()方法裡,並使引數KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua程式碼交給Redis服務端執行。那麼這段Lua程式碼的功能是什麼呢?其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那麼為什麼要使用Lua語言來實現呢?因為要確保上述操作是原子性的。簡單來說,就是在eval命令執行Lua程式碼的時候,Lua程式碼將被當成一個命令去執行,並且直到eval命令執行完成,Redis才會執行其他命令。

錯誤示例1:

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

最常見的解鎖程式碼就是直接使用jedis.del()方法刪除鎖,這種不先判斷鎖的擁有者而直接解鎖的方式,會導致任何客戶端都可以隨時進行解鎖,即使這把鎖不是它的。

錯誤示例2:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {

    // 判斷加鎖與解鎖是不是同一個客戶端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此時,這把鎖突然不是這個客戶端的,則會誤解鎖
        jedis.del(lockKey);
    }
}

如程式碼註釋,問題在於如果呼叫jedis.del()方法的時候,這把鎖已經不屬於當前客戶端的時候會解除他人加的鎖。那麼是否真的有這種場景?答案是肯定的,比如客戶端A加鎖,一段時間之後客戶端A解鎖,在執行jedis.del()之前,鎖突然過期了,此時客戶端B嘗試加鎖成功,然後客戶端A再執行del()方法,則將客戶端B的鎖給解除了。  

springBoot中實現

首先需要引入redisson

        <!--redis-->         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-data-redis</artifactId>         </dependency>         <!--redis分散式鎖-->         <dependency>             <groupId>org.redisson</groupId>             <artifactId>redisson</artifactId>             <version>3.4.3</version>         </dependency>

生成Redisson的bean  支援單機,主從,哨兵,叢集等模式,具體方式請參考https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95,這裡只演示叢集環境。

 @Bean
    Redisson redissonSentinel() {
        Config config = new Config();
        config.useClusterServers()
                .setScanInterval(2000) // 叢集狀態掃描間隔時間,單位是毫秒
                //可以用"rediss://"來啟用SSL連線
                .addNodeAddress("redis://10.82.0.102:7000")
                .addNodeAddress("redis://10.82.0.102:7001")
                .addNodeAddress("redis://10.82.0.102:7002")
                .addNodeAddress("redis://10.82.0.102:7003")
                .addNodeAddress("redis://10.82.0.102:7004")
                .addNodeAddress("redis://10.82.0.102:7005");
        return (Redisson)Redisson.create(config);
    }

這裡只是簡單展示,配置更加詳細的,參考上面網站。

簡單使用實現:   

@Autowired
    Redisson redisson;


    RLock lock = redisson.getLock(key);
    lock.lock(60, TimeUnit.SECONDS); //設定60秒自動釋放鎖  (預設是30秒自動過期)

    //執行的業務程式碼

    lock.unlock(); //釋放鎖

關於Redisson 更加全面詳細鎖的情況,前往:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

就這樣通過redisson就實現redis分散式鎖,內部幫我們解決了上一篇提到的注意的地方。使用redisson更加體現一切皆物件,我們不需要知道內部如何實現,只需知道如何使用就行。當然作為一個積極進取的程式設計師還是要了解底層實現的。

原理簡介

無意看到一篇部落格,分析的很好  請參考:http://www.jianshu.com/p/de5a69622e49