1. 程式人生 > 其它 >四、Redis分散式鎖

四、Redis分散式鎖

技術標籤:Redisredis分散式鎖redisson死鎖

四、Redis分散式鎖

Java中的鎖我們通常以synchronized 、Lock來使用它,但是隻能保證在同一個JVM程序內中執行。如果在分散式叢集環境下呢?分散式鎖的實現有很多,比如基於資料庫樂觀鎖、Redis、zookeeper、memcached、系統檔案等。

1、命令列加鎖:SET lock_key random_value NX PX 5000 執行成功,則證明客戶端獲取到了鎖。

random_value:是客戶端生成的唯一的字串。

NX:代表只在鍵不存在時,才對鍵進行設定操作。

PX:5000 設定鍵的過期時間為5000毫秒。

EX:可以讓該 key 在超時之後自動刪除。

2、Jedis加鎖程式碼實現

(1)、Long setnx(final String key, final String value):該方法只會對不存在的key設值,返回1代表獲取鎖成功;對存在的key設值,會返回0代表獲取鎖失敗。value是客戶端鎖的唯一標識,不能重複,例:UUID或System.currentTimeMillis() (獲取鎖的時間)+鎖持有的時間。

命令格式:SETNX key value

注:執行setnx加了鎖,需要再次執行語句設定過期時間,如果在setnx之後宕機,所就不會釋放,就會產生死鎖。無法保證兩步操作的原子性,不可取

(2)、String getSet(final String key, final String value):返回redis中key對應的oldValue,然後再把key原來的值oldValue更新為新傳入的值value。

命令格式:GETSET key value

注:當某個客戶端鎖過期時間,多個客戶端開始爭搶鎖。雖然最後只有一個客戶端能成功鎖,但是獲取鎖失敗的客戶端呼叫getSet能覆蓋獲取鎖成功客戶端的value。當客戶端的value被覆蓋,會造成鎖不具有標識性,會造成客戶端沒有釋放鎖。該方式同樣不可取。

(3)、String set(final String key, final String value, final String nxxx, final String expx, final long time):

該方法合併普通的set()和expire()操作,保證NX和EX的原子性。不能把兩個命令(NX EX)分開執行,如果在 NX 之後程式出現問題就有可能產生死鎖。

private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX”;
private final String LOCK_PREFIX = "test:lock:key-";

public  boolean lock(String key, String value, long expireSeconds) {
    String key = LOCK_PREFIX + MD5.convertToMD5(key);
    String result = this.jedis.set(LOCK_PREFIX +key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireSeconds);
    if (LOCK_MSG.equals(result)){
        return true ;
    }else {
        return false ;
    }
}

Jedis(單機)、JedisCluster(叢集)

3、RedisTemplate加鎖程式碼實現

(1)、加鎖:

private final String LOCK_PREFIX = "test:lock:key-";
@Resource
private RedisTemplate<String, String> redisTemplate;

public boolean lock(String keyLock, String value, long expireSeconds){
    redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + keyLock, value, expireSeconds, TimeUnit.SECONDS);
}

(2)、解鎖:

//單個刪除
redisTemplate.delete(keyLock);
//批量刪除
List<String> keys;
redisTemplate.delete(keys);

(3)、指定key失效時間:redisTemplate.getExpire(key,TimeUnit.SECONDS);

(4)、根據key獲取過期時間:redisTemplate.expire(key, time, TimeUnit.SECONDS);

(5)、判斷key是否存在:redisTemplate.hasKey(key);

(6)、Jedis和RedisTemplate區別

Jedis是Redis官方推薦的面向Java的操作Redis的客戶端,而RedisTemplate是spring-data-redis.jar中對JedisApi的高度封裝。spring-data-redis.jar相對於Jedis來說可以方便地更換Redis的Java客戶端,比Jedis多了自動管理連線池的特性,方便與其他Spring框架進行搭配使用如SpringCache。原生Jedis效率優於RedisTemplate。

4、解鎖:將key鍵刪除,加鎖時需要傳遞一個引數,將該引數作為這個 key 的 value,這樣每次解鎖時判斷 value 是否相等從而知道這個鎖是不是該程序自己的。

//此方式無法保證get和del的原子性
if(value.equals(redisTemplate.opsForValue().get(keyLock))){
    redisTemplate.delete(keyLock);
}

解決方案:

(1)、使用lua指令碼

使用lua指令碼合併get()和del()操作,先判斷value是否相等,相等再執行del命令。lua指令碼能保證這裡兩個操作的原子性。

try {
    ...
} catch (Exception e) {
    ...
} finally {
    //獲取Jedis資源
    Jedis jedis = RedisUtils.getJedis();
    //定義lua指令碼
    String script = "if redis.call('get',KEYS[1]) == ARGV[1]" +
            "then" +
            "    return redis.call('del',KEYS[1])" +
            "else" +
            "    return 0" +
            "end";
    try {
        //對指定的key、value執行指令碼
        Object o = jedis.eval(script, Collections.singletonList(keyLock), Collections.singletonList(value));
        if ("1".equals(o.toString())) {
            System.out.println("del redis lock ok");
        }else {
            System.out.println("del redis lock error");
        }
    }finally {
        //關閉Jedis資源
        if (null != jedis) {
            jedis.close();
        }
    }
}

(2)、使用Redis事務

while (true) {
    //監視key
    redisTemplate.watch(keyLock);
    if(value.equals(redisTemplate.opsForValue().get(keyLock))){
        //開啟事務支援
        redisTemplate.setEnableTransactionSupport(true);
        //開始事務
        redisTemplate.multi();
        redisTemplate.delete(keyLock);
        //執行命令,如果返回null,則事務執行失敗,key對應的value被別人修改過,需要重新執行。
        List<Object> list = redisTemplate.exec();
        if(list == null){
            continue;
        }
    }
    //解除監視
    redisTemplate.unwatch();
    break;
}

5、Redisson分散式鎖實現原理:

Redisson這個開源框架對Redis分散式鎖的實現原理,Jedis和Redisson都是Java中對Redis操作的封裝。Jedis只是簡單的封裝了Redis的API庫,可以看作是Redis客戶端,它的方法和Redis的命令很類似。Redisson不僅封裝了redis,還封裝了對更多資料結構的支援,以及鎖等功能,相比於Jedis更強大。但Jedis相比於Redisson更原生一些,更靈活。

//獲取Redisson物件
RLock lock = redisson.getLock(keyLock);
//加鎖
redissonLock.lock();
try {
 ...
} catch (Exception e) {
  ...
} finally {
    //判斷當前鎖仍在鎖定狀態 && 是當前執行緒所持有。
    //如果當前判斷不加可能會出現異常:attempt to unlock lock, not locked by current thread by node id: 0dsad...
    if (lock.isLocked() && lock.isHeldByCurrentThread()) {
        //解鎖
        lock.unlock();
    }
}

某個客戶端要加鎖,如果該客戶端面對的是一個redis cluster叢集,首先會根據hash節點只選擇一臺機器,然後傳送一段LOCK的lua指令碼到redis上,lua指令碼用來封裝複雜的業務邏輯,並保證這段複雜業務邏輯執行的原子性。

String LOCK = "if(redis.call('exists', KEYS[1]) == 0) then " +
        "redis.call('hset', KEYS[1], ARGV[2], 1); " +
        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
        "return nil; " +
        "end;" +
        "if(redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
        "return nil; " +
        "end;" +
        "return redis.call('pttl', KEYS[1]); ";

鎖的資料結構:

     lockKey:{
          "8743c9c0-0795-4907-87fd-6c719a6b4586:1": 1
     }

(1)、加鎖機制:客戶端1加鎖,因為沒有,所以執行第一個if

引數釋義:

KEYS[1]:要加鎖的key

ARGV[1]:鎖key的預設有效時間

ARGV[2]:加鎖的客戶端的ID,例:8743c9c0-0795-4907-87fd-6c719a6b4586:1

語句解析:

exists KEYS[1]:判斷要加的鎖是否存在

hset KEYS[1] ARGV[2] 1:執行加鎖操作

pexpire KEYS[1] ARGV[1]:設定有效時間

(2)、鎖互斥機制:客戶端2加鎖,因為存在客戶端1之前加了鎖,所以這個key的鎖已存在,則執行第二個if,鎖key的hash資料結構中,是否包含客戶端2的ID,不包含返回鎖key的剩餘生存時間,此時客戶端2會進入一個while迴圈,不停的嘗試加鎖。

(3)、可重入加鎖機制:重複執行lock.lock(),同一客戶端多次加鎖

第一個if判斷肯定不成立,“exists myLock”顯示鎖key已經存在。第二個if判斷會成立,因為myLock的hash資料結構中包含的那個ID,就是客戶端1的那個ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”。此時就會執行可重入加鎖的邏輯,會用:incrby lockKey

通過這個命令,對客戶端1的加鎖次數,累加1。此時myLock資料結構變為下面這樣:

     lockKey:{
          "8743c9c0-0795-4907-87fd-6c719a6b4586:1": 2
     }

(4)、釋放鎖機制:如果執行lock.unlock(),就可以釋放分散式鎖,其實就是每次都對lockKey資料結構中的那個加鎖次數減1。如果加鎖次數是0了,說明這個客戶端已經不再持有鎖了,此時就會用:"del lockKey"命令,從redis裡刪除這個key。然後,另外的客戶端2就可以嘗試完成加鎖了。

6、Redis分散式鎖的缺點:

如果你對某個redis master例項,寫入了lockKey這種鎖key的value,此時會非同步複製給對應的master slave例項。但是這個過程中一旦發生redis master宕機,主備切換,redis slave變為了redis master。就會導致,客戶端2來嘗試加鎖的時候,在新的redis master上完成了加鎖,而客戶端1也以為自己成功加了鎖。此時就會導致多個客戶端對一個分散式鎖完成了加鎖。這時系統在業務語義上一定會出現問題, 導致各種髒資料的產生 。所以這個就是redis cluster,或者是redis master-slave架構的主從非同步複製導致的redis分散式鎖的最大缺陷:在redis master例項宕機的時候,可能導致多個客戶端同時完成加鎖。

7、死鎖的四個必須要條件及解決方案

(1)、必要條件

①、互斥條件:某種資源一次只允許一個程序訪問,即該資源一旦分配給某個程序,其他程序就不能再訪問,直到該程序訪問結束。

②、佔有且等待條件:一個程序本身佔有資源(一種或多種),同時還有資源未得到滿足,正在等待其他程序釋放該資源。

③、不可剝奪條件:一個程序已經佔有了某項資源,在末使用完之前,其他程序不能強行剝奪該資源。

④、迴圈等待條件:存在一個程序鏈,使得每個程序都佔有下一個程序所需的至少一種資源。