解鎖Redis分佈式鎖的正確實現姿勢

一、什麼是分佈式鎖?
分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分佈式鎖。
在這裏插入圖片描述
二、常見的分佈式鎖有哪些實現方式?

  1. 數據庫樂觀鎖或者悲觀鎖實現的分佈式鎖;
  2. 基於Redis的分佈式鎖;
  3. 基於ZooKeeper的分佈式鎖。
    本次將主要介紹平常開發中用的最多,性能也是最好基於Redis實現的分佈式鎖。

三、redis分佈式鎖的特點
(1)redis有很高的性能;
(2)redis對此支持的命令較好,實現起來比較方便

四、使用分佈式鎖的時候主要用到的命令介紹
(1)SETNX
SETNX key val:當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不做,返回0。
(2)expire
expire key timeout:當key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。
(3)delete
delete key:刪除key

五、使用Redis實現分佈式鎖的實現思想
(1)獲取鎖的時候,使用setnx加鎖,並使用expire命令給鎖加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。
(2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
(3)釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

六、使用Redis實現分佈式鎖的實現原理
Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的連接並不存在競爭關係,基於此,Redis中可以使用SETNX命令實現分佈式鎖。

七、解鎖實現redis分佈式鎖的兩種姿勢
第一勢 使用setNx實現:
1、引入jar包

org.springframework.boot spring-boot-starter-data-redis ```

2、上代碼

```java
/**
 * setNxByExpire
 * 獲取分佈式鎖
 * @param lockKey 鎖
* @param requestId 請求標識
* @param expTime 超時時間
* @return 是否獲取成功
*/
public boolean setNxByExpire(final String lockKey, final Serializable requestId, final long expTime) {
    boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {

        @Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {

            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
RedisSerializer keySerializer = redisTemplate.getKeySerializer();
Object object = redisConnection.execute("set", keySerializer.serialize(lockKey),
valueSerializer.serialize(requestId),
SafeEncoder.encode("NX"),
SafeEncoder.encode("EX"),
redis.clients.jedis.Protocol.toByteArray(expTime));
            return object != null;
}
    });
    return result;
}

可以看到,我們加鎖就一行代碼:setNxByExpire(final String lockKey, final Serializable requestId, final long expTime),這個set()方法一共有三個形參:

第一個爲key,我們使用key來當鎖,因爲key是唯一的。

第二個爲value,我們傳的是requestId,很多童鞋可能不明白,有key作爲鎖不就夠了嗎,爲什麼還要用到value?原因就是我們在上面講到可靠性時,分佈式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值爲requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用UUID.randomUUID().toString()方法生成。

第三個代表key的過期時間。

總的來說,執行上面的set()方法就只會導致兩種結果:
當前沒有鎖(key不存在),那麼就進行加鎖操作,並對鎖設置個有效期,同時value表示加鎖的客戶端。
已有鎖存在,不做任何操作。

第二勢 使用Lua腳本實現:

/**
 * 釋放分佈式鎖
 * @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()方法可以確保原子性,源於Redis的特性,下面是官網對eval命令的部分解釋:簡單來說,就是在eval命令執行Lua代碼的時候,Lua代碼將被當成一個命令去執行,並且直到eval命令執行完成,Redis纔會執行其他命令。
程序祕籍
專注java職場經驗,技術分享
程序員接私活,編程培訓
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章