分布式鎖之Redis實(shí)現(xiàn)(acquire)

分布式鎖一般有三種實(shí)現(xiàn)方式:

    1. 基于數(shù)據(jù)庫(kù)的鎖;
    1. 基于Redis的分布式鎖;
    1. 基于ZooKeeper的分布式鎖。

本篇將介紹第二種方式,基于Redis實(shí)現(xiàn)分布式鎖。

使用分布式鎖要滿足的幾個(gè)條件

1.系統(tǒng)是一個(gè)分布式系統(tǒng)(關(guān)鍵是分布式,單機(jī)的可以使用ReentrantLock或者synchronized代碼塊來(lái)實(shí)現(xiàn))
2.共享資源(各個(gè)系統(tǒng)訪問(wèn)同一個(gè)資源,資源的載體可能是傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)或者NoSQL)
3.同步訪問(wèn)(即有很多個(gè)進(jìn)程同事訪問(wèn)同一個(gè)共享資源。沒(méi)有同步訪問(wèn),誰(shuí)管你資源競(jìng)爭(zhēng)不競(jìng)爭(zhēng))

使用命令介紹

SETNX

SETNX key val
當(dāng)且僅當(dāng)key不存在時(shí),set一個(gè)key為val的字符串,返回1;若key存在,則什么都不做,返回0。

expire

expire key timeout
為key設(shè)置一個(gè)超時(shí)時(shí)間,單位為second,超過(guò)這個(gè)時(shí)間鎖會(huì)自動(dòng)釋放,避免死鎖。

delete

delete key
刪除key

實(shí)現(xiàn)過(guò)程

簡(jiǎn)單版本

實(shí)現(xiàn)思路:SETNX命令只有當(dāng)key不存在時(shí)才能設(shè)值成功,返回值為1;key存在設(shè)值失敗,返回0。

public class testLock {

    public static void acquire(String lock){
        while(jedis.setnx(lock, "") == 0){}
    }

    public static void release(String lock){
        jedis.del(lock);
        jedis.close();
    }

}

在acquire方法內(nèi)部,循環(huán)設(shè)置某個(gè)key的值,直到設(shè)置成功。release方法中刪除這個(gè)key,代表釋放鎖。

存在的問(wèn)題

如果有多個(gè)客戶端競(jìng)爭(zhēng)同一個(gè)分布式鎖,如果三個(gè)客戶端中,有任意一個(gè)線程在調(diào)用acquire成功之后異常退出,沒(méi)有釋放鎖,另外兩個(gè)客戶端會(huì)死循環(huán)等待在SETNX命令上。

按照Redis文檔給出的一種解決方法,重新修改acquire方法:

public static void acquire(String lock){
   //1.先嘗試用setnx命令獲取鎖,key=lock,value=當(dāng)前時(shí)間+要持有鎖的時(shí)間hold_time
   while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time)) == 0){
       //2.如果獲取失敗,檢查lock對(duì)應(yīng)的值是否已超時(shí)
       String expireTime = jedis.get(lock);
       if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
           //3.如果已經(jīng)超時(shí)了,刪除lock,獲取鎖
               jedis.del(lock);
               jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time))
               break;
       }
   }
}

這樣就解決了死鎖的問(wèn)題,但是還有一個(gè)嚴(yán)重的問(wèn)題

C0操作超時(shí)了,但它還持有著鎖,C1和C2讀取lock.foo檢查時(shí)間戳,先后發(fā)現(xiàn)超時(shí)了。
C1 發(fā)送DEL lock.foo
C1 發(fā)送SETNX lock.foo 并且成功了。
C2 發(fā)送DEL lock.foo
C2 發(fā)送SETNX lock.foo 并且成功了。

這樣一來(lái),C1,C2都拿到了鎖!問(wèn)題大了!
針對(duì)這個(gè)問(wèn)題,
繼續(xù)修改acquire方法:

public static void acquire(String lock){
   //1.先嘗試用setnx命令獲取鎖,key=lock,value=當(dāng)前時(shí)間+要持有鎖的時(shí)間hold_time
   while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time)) == 0){
       //2.如果獲取失敗,檢查lock對(duì)應(yīng)的值是否已超時(shí)
       String expireTime = jedis.get(lock);
       if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
           //3.如果已經(jīng)超時(shí)了,使用getset命令,設(shè)置新的超時(shí)時(shí)間
           String oldExpire = jedis.getSet(lock, String.valueOf(System.currentTimeMillis() + hold_time));
           if(oldExpire != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
               //4.如果setget命令返回的值,依然是過(guò)期時(shí)間,認(rèn)為獲取鎖成功
               break;
           }
       }
   }
}

這樣就解決了上述的c1,c2的問(wèn)題,但這個(gè)版本依舊有兩個(gè)問(wèn)題沒(méi)有解決:

1.有效期時(shí)間戳覆蓋問(wèn)題:持有鎖的客戶端1異常退出,其余多個(gè)客戶端同時(shí)執(zhí)行setnx失敗,獲取expireTime,發(fā)現(xiàn)已經(jīng)小于currentTime,開始執(zhí)行g(shù)etset命令。假設(shè)客戶端2先執(zhí)行了getset,獲取鎖成功??蛻舳?在執(zhí)行g(shù)etset時(shí),返回的是客戶端2設(shè)置的未超時(shí)的時(shí)間戳,是一個(gè)未超時(shí)的時(shí)間,獲取鎖失敗??雌饋?lái)沒(méi)有問(wèn)題,但客戶端2持有的鎖的有效期時(shí)間戳已經(jīng)被客戶端3修改了。

2.超時(shí)問(wèn)題:如果客戶端2在持有鎖的期間,由于操作還沒(méi)有完成,但鎖已經(jīng)超時(shí)了。這時(shí)其它客戶端會(huì)拿到鎖,和超時(shí)的客戶端一起訪問(wèn)redis,不滿足互斥條件。

解決問(wèn)題
public class SimpleRedisLock {

    public static long hold_time = 3000;

    public static ThreadLocal<String> expireHolder = new ThreadLocal<>();

    public static void acquire(String lock){
        //1.先嘗試用setnx命令獲取鎖,key為參數(shù)lock,值為當(dāng)前時(shí)間+要持有鎖的時(shí)間hold_time
        while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time)) == 0){
            //2.如果獲取失敗,先watch lock key
            jedis.watch(lock);
            //3.獲取當(dāng)前超時(shí)時(shí)間
            String expireTime = jedis.get(lock);
            if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                //4.如果超時(shí)時(shí)間小于當(dāng)前時(shí)間,開事務(wù)準(zhǔn)備更新lock值
                Transaction transaction = jedis.multi();
                Response<String> response = transaction.getSet(lock, String.valueOf(System.currentTimeMillis() + hold_time));
                //5.步驟2設(shè)置了watch,如果lock的值被其他線程修改,不是執(zhí)行事務(wù)中的命令
                if(transaction.exec() != null){
                    String oldExpire = response.get();
                    if(oldExpire != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                        //6.如果setget命令返回的值依然是過(guò)期時(shí)間,認(rèn)為獲取鎖成功(加了watch之后,這里返回的應(yīng)該一直是超時(shí)時(shí)間)
                        break;
                    }
                }
            }else{
                //如果key未超時(shí),解除watch
                jedis.unwatch();
            }
        }
        //設(shè)置客戶端超時(shí)時(shí)間
        expireHolder.set(jedis.get(lock));
    }

    public static void release(String lock){
        //比較客戶端超時(shí)時(shí)間與lock值,判斷是否還由自己持有鎖
        if(jedis.get(lock).equals(expireHolder.get())){
            jedis.del(lock);
        }
        jedis.close();
    }

}

新的acquire方法,通過(guò)watch、redis事務(wù),保證只有一個(gè)客戶端能執(zhí)行g(shù)etset,并記錄了鎖超時(shí)時(shí)間,解決了問(wèn)題一的麻煩。
對(duì)于鎖超時(shí)導(dǎo)致的兩個(gè)客戶端同時(shí)訪問(wèn)資源,要么靠業(yè)務(wù)代碼保證鎖超時(shí)時(shí)間內(nèi)可以完成處理;要么在release時(shí)檢查是否超時(shí),如果超時(shí)回滾所有操作,但對(duì)不能回滾的,例如++操作就比較麻煩,或者放棄死鎖容錯(cuò)功能。
Redis分布式鎖的獲取鎖的問(wèn)題就到這里了,具體怎么使用還要看實(shí)際業(yè)務(wù)場(chǎng)景。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目前實(shí)現(xiàn)分布式鎖的方式主要有數(shù)據(jù)庫(kù)、Redis和Zookeeper三種,本文主要闡述利用Redis的相關(guān)命令來(lái)實(shí)現(xiàn)...
    Aldeo閱讀 2,166評(píng)論 0 6
  • Java多線程開發(fā)中鎖提供了原子性、可見性。但是在分布式系統(tǒng)中,一個(gè)進(jìn)程下的多個(gè)線程分布到一個(gè)集群中的多臺(tái)機(jī)器上,...
    yingzong閱讀 1,786評(píng)論 1 5
  • 守望/大漠 輕柔的吉他聲低低的 漫過(guò)秋日的涼爽 女兒的房間里 劃過(guò)來(lái)輕柔的吟唱 陽(yáng)光靜靜的 落在綠蘿新生的芽上 一...
    大漠qxy閱讀 180評(píng)論 0 3
  • ?。。。?!考線代的時(shí)候居然學(xué)號(hào)沒(méi)涂。
    明天999999閱讀 275評(píng)論 0 0

友情鏈接更多精彩內(nèi)容