Redis:分布式鎖

Java多線程開發(fā)中鎖提供了原子性评雌、可見性。但是在分布式系統(tǒng)中螟加,一個進(jìn)程下的多個線程分布到一個集群中的多臺機(jī)器上徘溢,需要其他方式來保證原子性、可見性捆探。通過封裝Redis的SETNX命令然爆,可以實現(xiàn)分布式鎖,提供分布式環(huán)境下的原子性黍图。

測試代碼

測試代碼啟動三個名稱為test-1曾雕、test-2、test-3線程助被,線程內(nèi)部會對同一個靜態(tài)變量執(zhí)行一萬次++操作剖张,如果代碼正確,最終靜態(tài)變量的值應(yīng)該為3萬揩环。測試代碼如下:

public class LockTest {

    public static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        new Thread(new CountRunnable(countDownLatch, cyclicBarrier), "test-1").start();
        new Thread(new CountRunnable(countDownLatch, cyclicBarrier), "test-2").start();
        new Thread(new CountRunnable(countDownLatch, cyclicBarrier), "test-3").start();
        countDownLatch.await();
        System.out.println(LockTest.i);
    }

    static class CountRunnable implements Runnable{

        private CountDownLatch countDownLatch;
        private CyclicBarrier cyclicBarrier;

        public CountRunnable(CountDownLatch countDownLatch, CyclicBarrier cyclicBarrier){
            this.countDownLatch = countDownLatch;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            for(int j = 0; j < 10000; j++){
                LockTest.i++;
            }
            countDownLatch.countDown();
        }
    }
}

在不使用鎖的情況下搔弄,執(zhí)行三次輸出結(jié)果分別為:24404、21768丰滑、17539肯污。

簡單版本

SETNX命令只有當(dāng)key不存在時才能設(shè)值成功,返回值為1吨枉;key存在設(shè)值失敗蹦渣,返回0。根據(jù)命令特性貌亭,可以有以下實現(xiàn):

public class SimpleRedisLock {

    public static ThreadLocal<Jedis> holder = new ThreadLocal<>();

    public static JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "localhost");

    public static void acquire(String lock){
        Jedis jedis = jedisPool.getResource();
        while(jedis.setnx(lock, "") == 0){}
        holder.set(jedis);
    }

    public static void release(String lock){
        Jedis jedis = holder.get();
        jedis.del(lock);
        jedis.close();
    }

}

在acquire方法內(nèi)部柬唯,獲取jedis對象,循環(huán)設(shè)置某個key的值圃庭,直到設(shè)置成功锄奢。release方法中刪除這個key失晴,代表釋放鎖。修改LockTest代碼:

for(int j = 0; j < 10000; j++){
    SimpleRedisLock.acquire("lock");
    LockTest.i++;
    SimpleRedisLock.release("lock");
}

重新執(zhí)行測試代碼拘央,輸入值:30000涂屁。

簡單版本的問題

測試代碼中啟動了3個線程競爭同一個分布式鎖,如果三個線程中灰伟,有任意一個線程在調(diào)用SimpleRedisLock的acquire成功之后異常退出拆又,沒有釋放鎖,另外兩個線程會死循環(huán)等待在SETNX命令上栏账,簡單修改一下LockTest帖族,模擬test-1異常退出的情況:

@Override
public void run() {
    try {
        cyclicBarrier.await();
        for(int j = 0; j < 10000; j++){
            SimpleRedisLock.acquire("lock");
            if(Thread.currentThread().getName().equals("test-1")){
                throw new RuntimeException();
            }
            LockTest.i++;
            SimpleRedisLock.release("lock");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        countDownLatch.countDown();
    }
}

線程test-?1在獲取到分布式鎖之后,因為運(yùn)行時異常退出(也有可能是因為進(jìn)程挡爵、機(jī)器crash竖般,OOM等各種問題),沒有正確的釋放鎖茶鹃,導(dǎo)致線程test-2涣雕、test-3死循環(huán)執(zhí)行SETNX命令。

死鎖

解決死鎖問題

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

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

測試代碼執(zhí)行結(jié)果:

test-1異常退出情況

在test-1線程退出后挣郭,程序正常執(zhí)行,并得到了正確結(jié)果2萬男杈。但這個版本依舊有兩個問題沒有解決:

  1. test-1線程異常退出丈屹,test-2调俘、test-3線程同時執(zhí)行setnx失敗伶棒,獲取expireTime,發(fā)現(xiàn)已經(jīng)小于currentTime彩库,開始執(zhí)行g(shù)etset命令肤无。假設(shè)test-2先執(zhí)行了getset,獲取鎖成功骇钦。test-3線程在執(zhí)行g(shù)etset時宛渐,返回的是test-2設(shè)置的未超時的時間戳,是一個未超時的時間眯搭,獲取鎖失敗窥翩。功能上沒有問題,但test-2線程持有的鎖的有效期時間戳已經(jīng)被test-3修改了鳞仙。

  2. 如果test-2線程在持有鎖的期間寇蚊,因為網(wǎng)絡(luò)抖動等原因,操作(測試代碼中對應(yīng)++操作部分)還沒有完成棍好,但鎖已經(jīng)超時了仗岸。 如何確定是否要釋放鎖(即使客戶端記錄自己的超時時間戳也沒用允耿,問題1中已經(jīng)描述了時間戳被其他線程修改的情況)?在需要互斥訪問資源的場景扒怖,執(zhí)行時間超過鎖超時時間的情況下较锡,怎么解決多個節(jié)點同時訪問資源的情況(同時執(zhí)行++操作)?

解決問題

重新修改獲取鎖的代碼:

public class SimpleRedisLock {

    public static long hold_time = 3000;

    public static ThreadLocal<Jedis> holder = new ThreadLocal<>();

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

    public static JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "localhost");

    public static void acquire(String lock){
        Jedis jedis = jedisPool.getResource();
        //1.先嘗試用setnx命令獲取鎖,key為參數(shù)lock,值為當(dāng)前時間+要持有鎖的時間hold_time
        while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time)) == 0){
            //2.如果獲取失敗,先watch lock key
            jedis.watch(lock);
            //3.獲取當(dāng)前超時時間
            String expireTime = jedis.get(lock);
            if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                //4.如果超時時間小于當(dāng)前時間,開事務(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命令返回的值依然是過期時間,認(rèn)為獲取鎖成功(加了watch之后,這里返回的應(yīng)該一直是超時時間)
                        break;
                    }
                }
            }else{
                //如果key未超時,解除watch
                jedis.unwatch();
            }
        }
        //設(shè)置客戶端超時時間
        expireHolder.set(jedis.get(lock));
        holder.set(jedis);
    }

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

}  

新的acquire方法盗痒,通過watch蚂蕴、redis事務(wù),保證只有一個客戶端能執(zhí)行g(shù)etset积糯,并記錄了鎖超時時間掂墓,解決了問題一和問題二的前半部分。對于鎖超時導(dǎo)致的兩個客戶端同時訪問資源看成,只能靠業(yè)務(wù)代碼保證鎖超時時間內(nèi)可以完成處理(可以在release時檢查是否超時君编,如果超時回滾所有操作,但對不能回滾的川慌,例如++操作就比較麻煩)吃嘿,或者放棄死鎖容錯功能,需要看場景衡量梦重。

代碼 :SimpleRedisLock

擴(kuò)展

以上只是單點redis服務(wù)器情況下的分布式鎖兑燥。在redis master-slaver架構(gòu)下,如果master節(jié)點down機(jī)琴拧,由于redis主從復(fù)制是異步的降瞳,會有明顯的race-condition。Redis文檔中提供了一種解決方案:RedLock蚓胸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挣饥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子沛膳,更是在濱河造成了極大的恐慌扔枫,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锹安,死亡現(xiàn)場離奇詭異短荐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)叹哭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門忍宋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人风罩,你說我怎么就攤上這事糠排。” “怎么了泊交?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵乳讥,是天一觀的道長柱查。 經(jīng)常有香客問我,道長云石,這世上最難降的妖魔是什么唉工? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮汹忠,結(jié)果婚禮上淋硝,老公的妹妹穿的比我還像新娘。我一直安慰自己宽菜,他們只是感情好谣膳,可當(dāng)我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铅乡,像睡著了一般继谚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上阵幸,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天花履,我揣著相機(jī)與錄音,去河邊找鬼挚赊。 笑死诡壁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荠割。 我是一名探鬼主播妹卿,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔑鹦!你這毒婦竟也來了夺克?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤举反,失蹤者是張志新(化名)和其女友劉穎懊直,沒想到半個月后扒吁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體火鼻,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年雕崩,在試婚紗的時候發(fā)現(xiàn)自己被綠了魁索。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡盼铁,死狀恐怖粗蔚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饶火,我是刑警寧澤鹏控,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布致扯,位于F島的核電站,受9級特大地震影響当辐,放射性物質(zhì)發(fā)生泄漏抖僵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一缘揪、第九天 我趴在偏房一處隱蔽的房頂上張望耍群。 院中可真熱鬧,春花似錦找筝、人聲如沸蹈垢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曹抬。三九已至,卻和暖如春急鳄,著一層夾襖步出監(jiān)牢的瞬間沐祷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工攒岛, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留赖临,地道東北人。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓灾锯,卻偏偏與公主長得像拍柒,于是被迫代替她去往敵國和親楷兽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,047評論 2 355

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