Redis分布式鎖的實現(xiàn)

分布式鎖

之前看程序員小灰的公眾號烙懦,通過漫畫的形式講解了分布式鎖的內(nèi)容器躏。

后來想到公司的項目里,也利用到了分布式鎖多矮,但是分布式鎖的具體代碼實現(xiàn)和在項目中的應(yīng)用并不是自己寫的缓淹,具體情況還不是太懂。尋思塔逃,決定利用這個熱度來看看公司的分布式鎖實現(xiàn)讯壶,向大牛們學(xué)習(xí)學(xué)習(xí)。

說到鎖患雏,在生活中鎖通常是用來鎖門的鹏溯,鎖車的。在java中淹仑,鎖是用來同步的丙挽,鎖是用來保證數(shù)據(jù)的一致性的肺孵。兩者雖不同,但最終都是以安全為結(jié)果來考慮的颜阐。

分布式鎖和我們java基礎(chǔ)中學(xué)習(xí)到的synchronized略有不同平窘,在synchronized中我們的鎖是個對象,如果一個線程拿到了該鎖凳怨,別的線程就只能等待了瑰艘。

分布式中的鎖,通常不會是一個對象肤舞,而是一個唯一的數(shù)據(jù)紫新,可以是商戶的訂單號,商戶的編碼李剖,或者是一條數(shù)據(jù)的唯一主鍵等等芒率。

synchronized主要對于單個應(yīng)用中,多線程的同步篙顺;

而分布式鎖對應(yīng)的是多個應(yīng)用偶芍,每個應(yīng)用中都可能會處理相同的數(shù)據(jù),所以需要對對個應(yīng)用的數(shù)據(jù)進(jìn)行同步德玫,保證數(shù)據(jù)的一致性匪蟀;

拋開具體代碼實現(xiàn),我認(rèn)為兩種鎖的最大不同宰僧。

分布式鎖的具體實現(xiàn)

1.redis

向redis中添加一個key材彪,添加的操作是原子性操作,key不存在才能添加成功撒桨;

2.zookeeper

具體實現(xiàn)查刻,還沒了解過,等有了解在做詳細(xì)解答凤类;

下面我們就來看看redis怎么實現(xiàn)分布式鎖;

redis實現(xiàn)分布式鎖

說的簡單些普气,redis來實現(xiàn)分布式鎖的原理就是將程序中一個唯一的key寫入redis中谜疤,當(dāng)有其他分布式應(yīng)用要訪問時候此key時,就去redis中讀取现诀,讀取到了則說明此數(shù)據(jù)正在被處理夷磕,讀取不到則說明可以進(jìn)行處理;

但是仔沿,想將分布式鎖處理的妥當(dāng)坐桩,還真不是一件輕松地事情,繼續(xù)往后看封锉。

在redis實現(xiàn)的分布式鎖中绵跷,我們需要強(qiáng)調(diào)以下幾點膘螟,只有保證了以下幾點,才可說是確保了鎖的實現(xiàn):

互斥碾局,在任何時刻荆残,對于同一條數(shù)據(jù),只有一臺應(yīng)用可以獲取到分布式鎖净当;

不能發(fā)生死鎖内斯,一臺服務(wù)器掛了,程序沒有執(zhí)行完像啼,但是redis中的鎖卻永久存在了俘闯,那么已加鎖未執(zhí)行完的數(shù)據(jù),就永遠(yuǎn)得不到處理了忽冻,直到人工發(fā)現(xiàn)真朗,或者監(jiān)控發(fā)現(xiàn);

高可用性甚颂,可以保證程序的正常加鎖蜜猾,正常解鎖;

加鎖解鎖必須由同一臺服務(wù)器進(jìn)行振诬,不能出現(xiàn)你加的鎖蹭睡,別人給你解鎖了。

再開始具體代碼之前赶么,我們需要來創(chuàng)建測試環(huán)境肩豁;首先是,在你的電腦中安裝redis辫呻,具體安裝如下:

下載清钥,解壓,編譯:

$ wget http://download.redis.io/releases/redis-4.0.10.tar.gz
$ tar xzf redis-4.0.10.tar.gz
$ cd redis-4.0.10
$ make

編譯成功后放闺,進(jìn)入到redis-4.0.10目錄中祟昭,再進(jìn)入到src目錄下:

執(zhí)行./redis-server命令,redis程序啟動怖侦;

新啟動一個新的窗口篡悟,進(jìn)入到redis-4.0.10/src目錄下執(zhí)行,./redis-cli命令進(jìn)入redis客戶端匾寝;

創(chuàng)建項目工程搬葬,添加java端redis依賴:

最新的2.9.0版本:

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

基本的環(huán)境創(chuàng)建完了,接下來我們就來編寫具體的代碼。

前面說了,redis分布式鎖實現(xiàn)就是像redis服務(wù)器中插入一個可做唯一條件的的key放吩。那么轧抗,我們來看看redis中Java的api抡锈;

我們使用的是redis在Java中的jedis框架疾忍。

在Jedis老版本中,多數(shù)實現(xiàn)使用的是setnx()方法和expire()方法實現(xiàn)企孩,而在jedis最新版本中使用的是set()方法锭碳;

例如:

public boolean setLock(Jedis jedis,String key,String val,int expireTime){
    if (jedis.setnx(key, val) == 1) {
        jedis.expire(key, expireTime / 1000);
        return true;
    }
    return false;
}

上面的例子中,你覺得會發(fā)生什么樣的問題勿璃?如果當(dāng)程序執(zhí)行完成jedis.setnx后擒抛,key和val被設(shè)置到了redis中,此時應(yīng)用程序異常补疑,過期時間還未設(shè)置歧沪,那么此key將永久保留在redis中,不會被刪除莲组。之所以這么實現(xiàn)诊胞,是因為當(dāng)時jedis框架并不支持多參數(shù)的setnx()方法,setnx指令本身不支持傳入超時時間。

public boolean setLock(Jedis jedis,String key,String val,int expireTime){
    String response = jedis.set(key, val, "NX", "PX",
            expireTime);
    return "OK".equals(response);
}

此方法為現(xiàn)在通用的實現(xiàn)锹杈;當(dāng)key不存在時撵孤,向redis中插入數(shù)據(jù),設(shè)置過期時間竭望,這就相當(dāng)于上鎖了邪码;這里面需要注意的一點就是,val的值咬清。我們將唯一的值當(dāng)做key闭专,那么val怎么搞?

在分布式系統(tǒng)環(huán)境下旧烧,val的值可以設(shè)置成該機(jī)器的唯一標(biāo)識影钉,例如時間+請求號。為什么這么說掘剪,當(dāng)一個服務(wù)器向redis加鎖時候平委,我們需要確定這個key是來自于哪臺服務(wù)器,在解鎖時需要校驗是不是解鎖的請求來自于同一個服務(wù)器夺谁;

set(final String key, final String value, final String nxxx,final String expx, final int time) 方法:

(1)key肆汹,我們使用key來當(dāng)鎖,key是唯一的予权。

(2)value,我們傳的是“時間+請求號”浪册,通過給value賦值我們在解鎖的時候就會傳遞同樣的數(shù)據(jù)進(jìn)行解鎖扫腺。不至于出現(xiàn)不同的服務(wù)器對key進(jìn)行解鎖。為什么說村象,不允許出現(xiàn)不同的服務(wù)器對一個key進(jìn)行解鎖笆环?我們后面講解攒至。

(3)nxxx,NX意思為SET IF NOT EXIST躁劣,即當(dāng)key不存在時迫吐,我們進(jìn)行set操作;若key已經(jīng)存在账忘,則不做任何操作志膀;

(4)expx,PX意思是給這個key加一個過期設(shè)置鳖擒,具體時間由第五個參數(shù)決定溉浙。

(5)time,代表key的過期時間蒋荚,單位毫秒戳稽。

說完了上鎖,接下來說說解鎖:

解鎖期升,就是將key刪除惊奇,你可能會覺得調(diào)用jedis刪除方法就行了唄,事實并不是如此播赁;

public void deleteLock(Jedis jedis, String key){
    jedis.del(key);
}

我們前面說了颂郎,在分布式環(huán)境中,哪臺服務(wù)器加的鎖行拢,在解鎖時候祖秒,還讓那臺服務(wù)器來解鎖。不能出現(xiàn)A服務(wù)器加鎖舟奠,而B服務(wù)器解鎖的情況竭缝;而上面的代碼就會出現(xiàn)這種情況。

當(dāng)A服務(wù)器將一個key設(shè)置超時時間為5秒鐘沼瘫,獲取到鎖執(zhí)行業(yè)務(wù)邏輯抬纸,但是呢,5秒鐘沒有執(zhí)行完耿戚,此時key由于到了過期時間而被刪除了湿故。正好B服務(wù)器進(jìn)行了獲取鎖操作,發(fā)現(xiàn)key沒有上鎖膜蛔,進(jìn)而加鎖開始執(zhí)行業(yè)務(wù)邏輯坛猪。過了1秒后,A服務(wù)器執(zhí)行完畢皂股,執(zhí)行釋放所操作墅茉,del(key),將B服務(wù)器上的鎖給刪除了。A就斤、B服務(wù)器對同一個可以執(zhí)行了一樣的操作悍募;

實現(xiàn)如下:

public void deleteLock(Jedis jedis, String key, String value){
    if (value.equals(jedis.get(key))) {
        jedis.del(lockKey);
    }
}

上面實現(xiàn),你覺得有問題嗎洋机?會不會出現(xiàn)A服務(wù)器加鎖坠宴,而B服務(wù)器解鎖的情況。

答案:是绷旗。

由于判斷和del()操作不是原子性的喜鼓,那么就會存在判斷后,讓其他服務(wù)器刪除的情況刁标;

例如:A服務(wù)器加鎖颠通,執(zhí)行業(yè)務(wù)邏輯,很快執(zhí)行完畢膀懈,進(jìn)行解鎖操作顿锰,解鎖判斷,OK启搂,準(zhǔn)備進(jìn)行del()操作硼控,此時CPU切換到執(zhí)行別的操作了,或者JVM虛擬機(jī)進(jìn)行垃圾回收操作胳赌。這時候牢撼,key到了過期時間,B服務(wù)器執(zhí)行獲取到鎖疑苫,執(zhí)行業(yè)務(wù)邏輯熏版,還沒執(zhí)行完成,A服務(wù)器復(fù)活捍掺,執(zhí)行del()操作撼短,刪除key;此時挺勿,A服務(wù)器上的鎖曲横,超時而被刪除,B服務(wù)器加鎖不瓶,A服務(wù)器將其刪除禾嫉;

終極大招,lua腳本實現(xiàn):

String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

jedis.eval(luaScript,Arrays.asList(key),Arrays.asList(value));

總體代碼結(jié)構(gòu)為:

 public void redisLock(Jedis jedis,String key,String val,int expireTime){
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    try{
        String response = jedis.set(key, val, "NX", "PX", expireTime);
        if(!"OK".equals(response)){
            return;
        }
        //.....執(zhí)行業(yè)務(wù)邏輯
    }catch (Exception e){

    }finally {
        jedis.eval(luaScript,Arrays.asList(key),Arrays.asList(val));
    }
}

通過lua腳本蚊丐,解決了 解鈴還須系鈴人 的問題熙参,但是并沒有解決由于A服務(wù)器執(zhí)行時間過長,導(dǎo)致鎖失效麦备,從而使得B服務(wù)器獲取到了鎖尊惰,對同一個key執(zhí)行了相同的邏輯讲竿。

筆者想到了兩種方式。首先弄屡,需要確認(rèn)的是,以上的情況發(fā)生概率很低鞋诗,如果你的系統(tǒng)并發(fā)量不大膀捷,業(yè)務(wù)邏輯不復(fù)雜的話,基本上很難遇到這個誤刪除的問題削彬,或者A全庸、B服務(wù)器都對同一個key執(zhí)行業(yè)務(wù)邏輯的問題。

第一個解決辦法融痛,我給他起名叫“懶政”壶笼,意思是重復(fù)執(zhí)行就重復(fù)執(zhí)行吧,不影響數(shù)據(jù)的一致性就行雁刷。但是覆劈,此解決辦法有個前提條件,不影響數(shù)據(jù)的最終一致性沛励。比如說责语,在加鎖的業(yè)務(wù)邏輯中有一個遠(yuǎn)程調(diào)用的接口,此接口不是冪等性的目派,你調(diào)用幾次坤候,此接口就接受幾次你的數(shù)據(jù),那么這種情況下就不能使用該方法企蹭。還有就是說白筹,在業(yè)務(wù)邏輯中有一個update數(shù)據(jù)庫更新操作,sql為 update set amount = amount - 10 where id = 1 and amount >=0谅摄,很常見的金額更新操作徒河,但是如果對id為1的數(shù)據(jù)連續(xù)執(zhí)行2次,那金額就不對了螟凭,這個是個大問題虚青,這種情況也不行。

第二個解決辦法是螺男,守護(hù)線程棒厘,當(dāng)A服務(wù)器設(shè)置的鎖要超時的時候,守護(hù)線程再對該鎖進(jìn)行續(xù)命下隧,加血奢人,延長存活時間。

守護(hù)線程例子:

public class ThreadTest implements Runnable{
    @Override
    public void run() {
        for (;;){
            System.out.println("111111111");
        }
    }
    public static void main(String[] agrs){
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        //thread.setDaemon(true);
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

當(dāng)設(shè)置setDaemon為true時淆院,當(dāng)main方法執(zhí)行結(jié)束后何乎,新啟動的線程也隨之結(jié)束,這就是守護(hù)線程,守護(hù)這main方法主線程支救,隨著main結(jié)束而結(jié)束抢野;

具體實現(xiàn)如下:

public void redisLockAndDaemonThread(final Jedis jedis, String key, String val, int expireTime){
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    try{
        String response = jedis.set(key, val, "NX", "PX", expireTime);
        if(!"OK".equals(response)){
            return;
        }
        //開啟守護(hù)線程:
        final int tmpExpireTime = expireTime;
        final String tmpKey = key;
        Thread thread = new Thread(new Runnable(){
            @Override
            public void run() {
                for(;;){
                    jedis.expire(tmpKey,tmpExpireTime);
                    try {
                        Thread.sleep(tmpExpireTime);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        //.....執(zhí)行業(yè)務(wù)邏輯
    }catch (Exception e){

    }finally {
        jedis.eval(luaScript,Arrays.asList(key),Arrays.asList(val));
    }
}

與之前的一樣,首先我們來對key進(jìn)行加鎖各墨,設(shè)置超時間指孤。獲取到鎖后,開啟守護(hù)線程贬堵,再對key進(jìn)行超時間設(shè)置恃轩,增加其壽命,接下來進(jìn)行睡眠黎做,如果在睡眠后還能繼續(xù)執(zhí)行叉跛,則說明此業(yè)務(wù)邏輯執(zhí)行還未結(jié)束,再次對key進(jìn)行壽命延長蒸殿。

如果業(yè)務(wù)線程結(jié)束筷厘,那么守護(hù)線程也隨之結(jié)束。

至此伟桅,如上便是redis實現(xiàn)分布式鎖的邏輯敞掘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市楣铁,隨后出現(xiàn)的幾起案子玖雁,更是在濱河造成了極大的恐慌,老刑警劉巖盖腕,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赫冬,死亡現(xiàn)場離奇詭異,居然都是意外死亡溃列,警方通過查閱死者的電腦和手機(jī)劲厌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來听隐,“玉大人补鼻,你說我怎么就攤上這事⊙湃危” “怎么了风范?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沪么。 經(jīng)常有香客問我硼婿,道長,這世上最難降的妖魔是什么禽车? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任寇漫,我火速辦了婚禮刊殉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘州胳。我一直安慰自己记焊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布陋葡。 她就那樣靜靜地躺著亚亲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腐缤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天肛响,我揣著相機(jī)與錄音岭粤,去河邊找鬼。 笑死特笋,一個胖子當(dāng)著我的面吹牛剃浇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播猎物,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼虎囚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蔫磨?” 一聲冷哼從身側(cè)響起淘讥,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎堤如,沒想到半個月后蒲列,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡搀罢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年蝗岖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榔至。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡抵赢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唧取,到底是詐尸還是另有隱情铅鲤,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布兵怯,位于F島的核電站彩匕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏媒区。R本人自食惡果不足惜驼仪,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一掸犬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绪爸,春花似錦湾碎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至递惋,卻和暖如春柔滔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背萍虽。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工睛廊, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杉编。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓超全,卻偏偏與公主長得像,于是被迫代替她去往敵國和親邓馒。 傳聞我的和親對象是個殘疾皇子嘶朱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 引題 比如在同一個節(jié)點上,兩個線程并發(fā)的操作A的賬戶光酣,都是取錢疏遏,如果不加鎖,A的賬戶可能會出現(xiàn)負(fù)數(shù)挂疆,正確的方式是對...
    阿康8182閱讀 4,793評論 0 75
  • 以下翻譯自redis官網(wǎng). 基于redis的分布式鎖 當(dāng)多個進(jìn)程需要以互斥的方式來操作共享數(shù)據(jù)的時候改览,分布式鎖在這...
    有惑閱讀 851評論 0 2
  • Java 并發(fā)包里面以及你給提供了很多方便的工具來幫助我們充分發(fā)揮單個節(jié)點上面的多線程并發(fā)處理能力,但是隨著并發(fā)量...
    ggr閱讀 735評論 1 1
  • 目前實現(xiàn)分布式鎖的方式主要有數(shù)據(jù)庫缤言、Redis和Zookeeper三種宝当,本文主要闡述利用Redis的相關(guān)命令來實現(xiàn)...
    Aldeo閱讀 2,067評論 0 6
  • 除夕夜的22:18跌穗,搶支付寶紅包的朋友們應(yīng)該都不陌生订晌,話說,大家搶了多少呢蚌吸? 我是1.68锈拨,個人覺得運氣還不錯。 ...
    蘇訴閱讀 277評論 5 3