日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當(dāng)前位置:首頁 > 單片機(jī) > 架構(gòu)師社區(qū)
[導(dǎo)讀]來自:冰河技術(shù)? ? ? 寫在前面 最近,很多小伙伴留言說,在學(xué)習(xí)高并發(fā)編程時,不太明白分布式鎖是用來解決什么問題的,還有不少小伙伴甚至連分布式鎖是什么都不太明白。明明在生產(chǎn)環(huán)境上使用了自己開發(fā)的分布式鎖,為什么還會出現(xiàn)問題呢?同樣的程序,加上分

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ><br></p>
  <p style=來自:冰河技術(shù)     

寫在前面

最近,很多小伙伴留言說,在學(xué)習(xí)高并發(fā)編程時,不太明白分布式鎖是用來解決什么問題的,還有不少小伙伴甚至連分布式鎖是什么都不太明白。明明在生產(chǎn)環(huán)境上使用了自己開發(fā)的分布式鎖,為什么還會出現(xiàn)問題呢?同樣的程序,加上分布式鎖后,性能差了幾個數(shù)量級!這又是為什么呢?今天,我們就來說說如何在高并發(fā)環(huán)境下實(shí)現(xiàn)分布式鎖,不是所有的鎖都是高并發(fā)的。


萬字長文,帶你深入解密高并發(fā)環(huán)境下的分布式鎖架構(gòu),不是所有的鎖都是分布式鎖?。?!

究竟什么樣的鎖才能更好的支持高并發(fā)場景呢?今天,我們就一起解密高并發(fā)環(huán)境下典型的分布式鎖架構(gòu),結(jié)合【高并發(fā)】專題下的其他文章,學(xué)以致用。

鎖用來解決什么問題呢?

在我們編寫的應(yīng)用程序或者高并發(fā)程序中,不知道大家有沒有想過一個問題,就是我們?yōu)槭裁葱枰腈i?鎖為我們解決了什么問題呢?

在很多業(yè)務(wù)場景下,我們編寫的應(yīng)用程序中會存在很多的 資源競爭 的問題。而我們在高并發(fā)程序中,引入鎖,就是為了解決這些資源競爭的問題。

電商超賣問題

這里,我們可以列舉一個簡單的業(yè)務(wù)場景。比如,在電子商務(wù)(商城)的業(yè)務(wù)場景中,提交訂單購買商品時,首先需要查詢相應(yīng)商品的庫存是否足夠,只有在商品庫存數(shù)量足夠的前提下,才能讓用戶成功的下單。下單時,我們需要在庫存數(shù)量中減去用戶下單的商品數(shù)量,并將庫存操作的結(jié)果數(shù)據(jù)更新到數(shù)據(jù)庫中。整個流程我們可以簡化成下圖所示。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
   <p style=很多小伙伴也留言說,讓我給出代碼,這樣能夠更好的學(xué)習(xí)和掌握相關(guān)的知識。好吧,這里,我也給出相應(yīng)的代碼片段吧。我們可以使用下面的代碼片段來表示用戶的下單操作,我這里將商品的庫存信息保存在了Redis中。

@RequestMapping("/submitOrder")
public String submitOrder(){
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    if(stock > 0){
        stock -= 1;
        stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
        logger.debug("庫存扣減成功,當(dāng)前庫存為:{}", stock);
    }else{
        logger.debug("庫存不足,扣減庫存失敗");
        throw new OrderException("庫存不足,扣減庫存失敗");
    }
    return "success";
}

注意:上述代碼片段比較簡單,只是為了方便大家理解,真正項(xiàng)目中的代碼就不能這么寫了。

上述的代碼看似是沒啥問題的,但是我們不能只從代碼表面上來觀察代碼的執(zhí)行順序。這是因?yàn)樵贘VM中代碼的執(zhí)行順序未必是按照我們書寫代碼的順序執(zhí)行的。即使在JVM中代碼是按照我們書寫的順序執(zhí)行,那我們對外提供的接口一旦暴露出去,就會有成千上萬的客戶端來訪問我們的接口。所以說,我們暴露出去的接口是會被并發(fā)訪問的。

試問,上面的代碼在高并發(fā)環(huán)境下是線程安全的嗎?答案肯定不是線程安全的,因?yàn)樯鲜隹蹨p庫存的操作會出現(xiàn)并行執(zhí)行的情況。

我們可以使用Apache JMeter來對上述接口進(jìn)行測試,這里,我使用Apache JMeter對上述接口進(jìn)行測試。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖!!

在Jmeter中,我將線程的并發(fā)度設(shè)置為3,接下來的配置如下所示。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖!!

以HTTP GET請求的方式來并發(fā)訪問提交訂單的接口。此時,運(yùn)行JMeter來訪問接口,命令行會打印出下面的日志信息。

庫存扣減成功,當(dāng)前庫存為:49
庫存扣減成功,當(dāng)前庫存為:49
庫存扣減成功,當(dāng)前庫存為:49

這里,我們明明請求了3次,也就是說,提交了3筆訂單,為什么扣減后的庫存都是一樣的呢?這種現(xiàn)象在電商領(lǐng)域有一個專業(yè)的名詞叫做  “超賣” 。

如果一個大型的高并發(fā)電商系統(tǒng),比如淘寶、天貓、京東等,出現(xiàn)了超賣現(xiàn)象,那損失就無法估量了!架構(gòu)設(shè)計(jì)和開發(fā)電商系統(tǒng)的人員估計(jì)就要通通下崗了。所以,作為技術(shù)人員,我們一定要嚴(yán)謹(jǐn)?shù)膶Υ夹g(shù),嚴(yán)格做好系統(tǒng)的每一個技術(shù)環(huán)節(jié)。

JVM中提供的鎖

JVM中提供的synchronized和Lock鎖,相信大家并不陌生了,很多小伙伴都會使用這些鎖,也能使用這些鎖來實(shí)現(xiàn)一些簡單的線程互斥功能。那么,作為立志要成為架構(gòu)師的你,是否了解過JVM鎖的底層原理呢?

JVM鎖原理

說到JVM鎖的原理,我們就不得不限說說Java中的對象頭了。

Java中的對象頭

每個Java對象都有對象頭。如果是?數(shù)組類型,則?2個字寬來存儲對象頭,如果是數(shù)組,則會?3個字寬來存儲對象頭。在32位處理器中,?個字寬是32位;在64位虛擬機(jī)中,?個字寬是64位。

對象頭的內(nèi)容如下表 。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
   <p style=Mark Work的格式如下所示。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

可以看到,當(dāng)對象狀態(tài)為偏向鎖時, Mark Word 存儲的是偏向的線程ID;當(dāng)狀態(tài)為輕量級鎖時, Mark Word 存儲的是指向線程棧中 Lock Record 的指針;當(dāng)狀態(tài)為重量級鎖時, Mark Word 為指向堆中的monitor對象的指針 。

有關(guān)Java對象頭的知識,參考《深入淺出Java多線程》。

JVM鎖原理

簡單點(diǎn)來說,JVM中鎖的原理如下。

在Java對象的對象頭上,有一個鎖的標(biāo)記,比如,第一個線程執(zhí)行程序時,檢查Java對象頭中的鎖標(biāo)記,發(fā)現(xiàn)Java對象頭中的鎖標(biāo)記為未加鎖狀態(tài),于是為Java對象進(jìn)行了加鎖操作,將對象頭中的鎖標(biāo)記設(shè)置為鎖定狀態(tài)。第二個線程執(zhí)行同樣的程序時,也會檢查Java對象頭中的鎖標(biāo)記,此時會發(fā)現(xiàn)Java對象頭中的鎖標(biāo)記的狀態(tài)為鎖定狀態(tài)。于是,第二個線程會進(jìn)入相應(yīng)的阻塞隊(duì)列中進(jìn)行等待。

這里有一個關(guān)鍵點(diǎn)就是Java對象頭中的鎖標(biāo)記如何實(shí)現(xiàn)。

JVM鎖的短板

JVM中提供的synchronized和Lock鎖都是JVM級別的,大家都知道,當(dāng)運(yùn)行一個Java程序時,會啟動一個JVM進(jìn)程來運(yùn)行我們的應(yīng)用程序。synchronized和Lock在JVM級別有效,也就是說,synchronized和Lock在同一Java進(jìn)程內(nèi)有效。如果我們開發(fā)的應(yīng)用程序是分布式的,那么只是使用synchronized和Lock來解決分布式場景下的高并發(fā)問題,就會顯得有點(diǎn)力不從心了。

synchronized和Lock支持JVM同一進(jìn)程內(nèi)部的線程互斥

synchronized和Lock在JVM級別能夠保證高并發(fā)程序的互斥,我們可以使用下圖來表示。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

但是,當(dāng)我們將應(yīng)用程序部署成分布式架構(gòu),或者將應(yīng)用程序在不同的JVM進(jìn)程中運(yùn)行時,synchronized和Lock就不能保證分布式架構(gòu)和多JVM進(jìn)程下應(yīng)用程序的互斥性了。

synchronized和Lock不能實(shí)現(xiàn)多JVM進(jìn)程之間的線程互斥

分布式架構(gòu)和多JVM進(jìn)程的本質(zhì)都是將應(yīng)用程序部署在不同的JVM實(shí)例中,也就是說,其本質(zhì)還是多JVM進(jìn)程。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

分布式鎖

我們在實(shí)現(xiàn)分布式鎖時,可以參照J(rèn)VM鎖實(shí)現(xiàn)的思想,JVM鎖在為對象加鎖時,通過改變Java對象的對象頭中的鎖的標(biāo)志位來實(shí)現(xiàn),也就是說,所有的線程都會訪問這個Java對象的對象頭中的鎖標(biāo)志位。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

我們同樣以這種思想來實(shí)現(xiàn)分布式鎖,當(dāng)我們將應(yīng)用程序進(jìn)行拆分并部署成分布式架構(gòu)時,所有應(yīng)用程序中的線程訪問共享變量時,都到同一個地方去檢查當(dāng)前程序的臨界區(qū)是否進(jìn)行了加鎖操作,而是否進(jìn)行了加鎖操作,我們在統(tǒng)一的地方使用相應(yīng)的狀態(tài)來進(jìn)行標(biāo)記。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

可以看到,在分布式鎖的實(shí)現(xiàn)思想上,與JVM鎖相差不大。而在實(shí)現(xiàn)分布式鎖中,保存加鎖狀態(tài)的服務(wù)可以使用MySQL、Redis和Zookeeper實(shí)現(xiàn)。

但是,在互聯(lián)網(wǎng)高并發(fā)環(huán)境中, 使用Redis實(shí)現(xiàn)分布式鎖的方案是使用的最多的。 接下來,我們就使用Redis來深入解密分布式鎖的架構(gòu)設(shè)計(jì)。

Redis如何實(shí)現(xiàn)分布式鎖

Redis命令

在Redis中,有一個不常使用的命令如下所示。

SETNX key value

這條命令的含義就是“SET if Not Exists”,即不存在的時候才會設(shè)置值。

只有在key不存在的情況下,將鍵key的值設(shè)置為value。如果key已經(jīng)存在,則SETNX命令不做任何操作。

這個命令的返回值如下。

  • 命令在設(shè)置成功時返回1。

  • 命令在設(shè)置失敗時返回0。

所以,我們在分布式高并發(fā)環(huán)境下,可以使用Redis的SETNX命令來實(shí)現(xiàn)分布式鎖。假設(shè)此時有線程A和線程B同時訪問臨界區(qū)代碼,假設(shè)線程A首先執(zhí)行了SETNX命令,并返回結(jié)果1,繼續(xù)向下執(zhí)行。而此時線程B再次執(zhí)行SETNX命令時,返回的結(jié)果為0,則線程B不能繼續(xù)向下執(zhí)行。只有當(dāng)線程A執(zhí)行DELETE命令將設(shè)置的鎖狀態(tài)刪除時,線程B才會成功執(zhí)行SETNX命令設(shè)置加鎖狀態(tài)后繼續(xù)向下執(zhí)行。

引入分布式鎖

了解了如何使用Redis中的命令實(shí)現(xiàn)分布式鎖后,我們就可以對下單接口進(jìn)行改造了,加入分布式鎖,如下所示。

/**
* 為了演示方便,我這里就簡單定義了一個常量作為商品的id
* 實(shí)際工作中,這個商品id是前端進(jìn)行下單操作傳遞過來的參數(shù)
*/

public static final String PRODUCT_ID = "100001";

@RequestMapping("/submitOrder")
public String submitOrder(){
    //通過stringRedisTemplate來調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
    //實(shí)際上,value可以為任意的字符換
    Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
   //沒有拿到鎖,返回下單失敗
    if(!isLock){
        return "failure";
    }
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    if(stock > 0){
        stock -= 1;
        stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
        logger.debug("庫存扣減成功,當(dāng)前庫存為:{}", stock);
    }else{
        logger.debug("庫存不足,扣減庫存失敗");
        throw new OrderException("庫存不足,扣減庫存失敗");
    }
    //業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
    stringRedisTemplate.delete(PRODUCT_ID);
    return "success";
}

那么,在上述代碼中,我們加入了分布式鎖的操作,那上述代碼是否能夠在高并發(fā)場景下保證業(yè)務(wù)的原子性呢?答案是可以保證業(yè)務(wù)的原子性。但是,在實(shí)際場景中,上面實(shí)現(xiàn)分布式鎖的代碼是不可用的??!

假設(shè)當(dāng)線程A首先執(zhí)行stringRedisTemplate.opsForValue()的setIfAbsent()方法返回true,繼續(xù)向下執(zhí)行,正在執(zhí)行業(yè)務(wù)代碼時,拋出了異常,線程A直接退出了JVM。此時,stringRedisTemplate.delete(PRODUCT_ID);代碼還沒來得及執(zhí)行,之后所有的線程進(jìn)入提交訂單的方法時,調(diào)用stringRedisTemplate.opsForValue()的setIfAbsent()方法都會返回false。導(dǎo)致后續(xù)的所有下單操作都會失敗。這就是分布式場景下的死鎖問題。

所以,上述代碼中實(shí)現(xiàn)分布式鎖的方式在實(shí)際場景下是不可取的!!

引入try-finally代碼塊

說到這,相信小伙伴們都能夠想到,使用try-finall代碼塊啊,接下來,我們?yōu)橄聠谓涌诘姆椒由蟭ry-finally代碼塊。

/**
* 為了演示方便,我這里就簡單定義了一個常量作為商品的id
* 實(shí)際工作中,這個商品id是前端進(jìn)行下單操作傳遞過來的參數(shù)
*/

public static final String PRODUCT_ID = "100001";

@RequestMapping("/submitOrder")
public String submitOrder(){
    //通過stringRedisTemplate來調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
    //實(shí)際上,value可以為任意的字符換
    Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
   //沒有拿到鎖,返回下單失敗
    if(!isLock){
        return "failure";
    }
    try{
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            stock -= 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            logger.debug("庫存扣減成功,當(dāng)前庫存為:{}", stock);
        }else{
            logger.debug("庫存不足,扣減庫存失敗");
            throw new OrderException("庫存不足,扣減庫存失敗");
        }
    }finally{
         //業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
        stringRedisTemplate.delete(PRODUCT_ID);
    }
    return "success";
}

那么,上述代碼是否真正解決了死鎖的問題呢?我們在寫代碼時,不能只盯著代碼本身,覺得上述代碼沒啥問題了。實(shí)際上,生產(chǎn)環(huán)境是非常復(fù)雜的。如果線程在成功加鎖之后,執(zhí)行業(yè)務(wù)代碼時,還沒來得及執(zhí)行刪除鎖標(biāo)志的代碼,此時,服務(wù)器宕機(jī)了,程序并沒有優(yōu)雅的退出JVM。也會使得后續(xù)的線程進(jìn)入提交訂單的方法時,因無法成功的設(shè)置鎖標(biāo)志位而下單失敗。所以說,上述的代碼仍然存在問題。

引入Redis超時機(jī)制

在Redis中可以設(shè)置緩存的自動過期時間,我們可以將其引入到分布式鎖的實(shí)現(xiàn)中,如下代碼所示。

/**
* 為了演示方便,我這里就簡單定義了一個常量作為商品的id
* 實(shí)際工作中,這個商品id是前端進(jìn)行下單操作傳遞過來的參數(shù)
*/

public static final String PRODUCT_ID = "100001";

@RequestMapping("/submitOrder")
public String submitOrder(){
    //通過stringRedisTemplate來調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
    //實(shí)際上,value可以為任意的字符換
    Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
   //沒有拿到鎖,返回下單失敗
    if(!isLock){
        return "failure";
    }
    try{
        stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            stock -= 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            logger.debug("庫存扣減成功,當(dāng)前庫存為:{}", stock);
        }else{
            logger.debug("庫存不足,扣減庫存失敗");
            throw new OrderException("庫存不足,扣減庫存失敗");
        }
    }finally{
         //業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
        stringRedisTemplate.delete(PRODUCT_ID);
    }
    return "success";
}

在上述代碼中,我們加入了如下一行代碼來為Redis中的鎖標(biāo)志設(shè)置過期時間。

stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);

此時,我們設(shè)置的過期時間為30秒。

那么問題來了,這樣是否就真正的解決了問題呢?上述程序就真的沒有坑了嗎?答案是還是有坑的?。?/strong>

“坑位”分析

我們在下單操作的方法中為分布式鎖引入了超時機(jī)制,此時的代碼還是無法真正避免死鎖的問題,那“坑位”到底在哪里呢?試想,當(dāng)程序執(zhí)行完stringRedisTemplate.opsForValue().setIfAbsent()方法后,正要執(zhí)行stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS)代碼時,服務(wù)器宕機(jī)了,你還別說,生產(chǎn)壞境的情況非常復(fù)雜,就是這么巧,服務(wù)器就宕機(jī)了。此時,后續(xù)請求進(jìn)入提交訂單的方法時,都會因?yàn)闊o法成功設(shè)置鎖標(biāo)志而導(dǎo)致后續(xù)下單流程無法正常執(zhí)行。

既然我們找到了上述代碼的“坑位”,那我們?nèi)绾螌⑦@個”坑“填上?如何解決這個問題呢?別急,Redis已經(jīng)提供了這樣的功能。我們可以在向Redis中保存數(shù)據(jù)的時候,可以同時指定數(shù)據(jù)的超時時間。所以,我們可以將代碼改造成如下所示。

/**
* 為了演示方便,我這里就簡單定義了一個常量作為商品的id
* 實(shí)際工作中,這個商品id是前端進(jìn)行下單操作傳遞過來的參數(shù)
*/

public static final String PRODUCT_ID = "100001";

@RequestMapping("/submitOrder")
public String submitOrder(){
    //通過stringRedisTemplate來調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
    //實(shí)際上,value可以為任意的字符換
    Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe"30, TimeUnit.SECONDS);
   //沒有拿到鎖,返回下單失敗
    if(!isLock){
        return "failure";
    }
    try{
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            stock -= 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            logger.debug("庫存扣減成功,當(dāng)前庫存為:{}", stock);
        }else{
            logger.debug("庫存不足,扣減庫存失敗");
            throw new OrderException("庫存不足,扣減庫存失敗");
        }
    }finally{
         //業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
        stringRedisTemplate.delete(PRODUCT_ID);
    }
    return "success";
}

在上述代碼中,我們在向Redis中設(shè)置鎖標(biāo)志位的時候就設(shè)置了超時時間。此時,只要向Redis中成功設(shè)置了數(shù)據(jù),則即使我們的業(yè)務(wù)系統(tǒng)宕機(jī),Redis中的數(shù)據(jù)過期后,也會自動刪除。后續(xù)的線程進(jìn)入提交訂單的方法后,就會成功的設(shè)置鎖標(biāo)志位,并向下執(zhí)行正常的下單流程。

到此,上述的代碼基本上在功能角度解決了程序的死鎖問題,那么,上述程序真的就完美了嗎?哈哈,很多小伙伴肯定會說不完美!確實(shí),上面的代碼還不是完美的,那大家知道哪里不完美嗎?接下來,我們繼續(xù)分析。

在開發(fā)集成角度分析代碼

在我們開發(fā)公共的系統(tǒng)組件時,比如我們這里說的分布式鎖,我們肯定會抽取一些公共的類來完成相應(yīng)的功能來供系統(tǒng)使用。

這里,假設(shè)我們定義了一個RedisLock接口,如下所示。

public interface RedisLock{
    //加鎖操作
    boolean tryLock(String key, long timeout, TimeUnit unit);
    //解鎖操作
    void releaseLock(String key);
}

接下來,使用RedisLockImpl類實(shí)現(xiàn)RedisLock接口,提供具體的加鎖和解鎖實(shí)現(xiàn),如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        return stringRedisTemplate.opsForValue().setIfAbsent(key, "binghe", timeout, unit);
    }
    @Override
    public void releaseLock(String key){
        stringRedisTemplate.delete(key);
    }
}

在開發(fā)集成的角度來說,當(dāng)一個線程從上到下執(zhí)行時,首先對程序進(jìn)行加鎖操作,然后執(zhí)行業(yè)務(wù)代碼,執(zhí)行完成后,再進(jìn)行釋放鎖的操作。理論上,加鎖和釋放鎖時,操作的Redis Key都是一樣的。但是,如果其他開發(fā)人員在編寫代碼時,并沒有調(diào)用tryLock()方法,而是直接調(diào)用了releaseLock()方法,并且他調(diào)用releaseLock()方法傳遞的key與你調(diào)用tryLock()方法傳遞的key是一樣的。那此時就會出現(xiàn)問題了,他在編寫代碼時,硬生生的將你加的鎖釋放了?。?!

所以,上述代碼是不安全的,別人能夠隨隨便便的將你加的鎖刪除,這就是鎖的誤刪操作,這是非常危險(xiǎn)的,所以,上述的程序存在很嚴(yán)重的問題??!

那如何實(shí)現(xiàn)只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作呢? 繼續(xù)向下看。

如何實(shí)現(xiàn)加鎖和解鎖的歸一化?

什么是加鎖和解鎖的歸一化呢?簡單點(diǎn)來說,就是一個線程執(zhí)行了加鎖操作后,后續(xù)必須由這個線程執(zhí)行解鎖操作,加鎖和解鎖操作由同一個線程來完成。

為了解決只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作的問題,那么,我們就需要將加鎖和解鎖操作綁定到同一個線程中,那么,如何將加鎖操作和解鎖操作綁定到同一個線程呢?其實(shí)很簡單,相信很多小伙伴都想到了—— 使用ThreadLocal實(shí)現(xiàn) 。沒錯,使用ThreadLocal類確實(shí)能夠解決這個問題。

此時,我們將RedisLockImpl類的代碼修改成如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        String uuid = UUID.randomUUID().toString();
        threadLocal.set(uuid);
        return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,在執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
             stringRedisTemplate.delete(key);   
        }
    }
}

上述代碼的主要邏輯為:在對程序執(zhí)行嘗試加鎖操作時,首先生成一個uuid,將生成的uuid綁定到當(dāng)前線程,并將傳遞的key參數(shù)操作Redis中的key,生成的uuid作為Redis中的Value,保存到Redis中,同時設(shè)置超時時間。當(dāng)執(zhí)行解鎖操作時,首先,判斷當(dāng)前線程中綁定的uuid是否和Redis中存儲的uuid相等,只有二者相等時,才會執(zhí)行刪除鎖標(biāo)志位的操作。這就避免了一個線程對程序進(jìn)行了加鎖操作后,其他線程對這個鎖進(jìn)行了解鎖操作的問題。

繼續(xù)分析

我們將加鎖和解鎖的方法改成如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    private String lockUUID;
    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        String uuid = UUID.randomUUID().toString();
        threadLocal.set(uuid);
        lockUUID = uuid;
        return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,在執(zhí)行刪除鎖的操作
        if(lockUUID.equals(stringRedisTemplate.opsForValue().get(key))){
             stringRedisTemplate.delete(key);   
        }
    }
}

相信很多小伙伴都會看出上述代碼存在什么問題了?。]錯,那就是 線程安全的問題。

所以,這里,我們需要使用ThreadLocal來解決線程安全問題。

可重入性分析

在上面的代碼中,當(dāng)一個線程成功設(shè)置了鎖標(biāo)志位后,其他的線程再設(shè)置鎖標(biāo)志位時,就會返回失敗。還有一種場景就是在提交訂單的接口方法中,調(diào)用了服務(wù)A,服務(wù)A調(diào)用了服務(wù)B,而服務(wù)B的方法中存在對同一個商品的加鎖和解鎖操作。

所以,服務(wù)B成功設(shè)置鎖標(biāo)志位后,提交訂單的接口方法繼續(xù)執(zhí)行時,也不能成功設(shè)置鎖標(biāo)志位了。也就是說,目前實(shí)現(xiàn)的分布式鎖沒有可重入性。

這里,就存在可重入性的問題了。我們希望設(shè)計(jì)的分布式鎖 具有可重入性 ,那什么是可重入性呢?簡單點(diǎn)來說,就是同一個線程,能夠多次獲取同一把鎖,并且能夠按照順序進(jìn)行解決操作。

其實(shí),在JDK 1.5之后提供的鎖很多都支持可重入性,比如synchronized和Lock。

如何實(shí)現(xiàn)可重入性呢?

映射到我們加鎖和解鎖方法時,我們?nèi)绾沃С滞粋€線程能夠多次獲取到鎖(設(shè)置鎖標(biāo)志位)呢?可以這樣簡單的設(shè)計(jì):如果當(dāng)前線程沒有綁定uuid,則生成uuid綁定到當(dāng)前線程,并且在Redis中設(shè)置鎖標(biāo)志位。如果當(dāng)前線程已經(jīng)綁定了uuid,則直接返回true,證明當(dāng)前線程之前已經(jīng)設(shè)置了鎖標(biāo)志位,也就是說已經(jīng)獲取到了鎖,直接返回true。

結(jié)合以上分析,我們將提交訂單的接口方法代碼改造成如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        Boolean isLocked = false;
        if(threadLocal.get() == null){
            String uuid = UUID.randomUUID().toString();
            threadLocal.set(uuid);
            isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
        }else{
            isLocked = true;   
        }
        return isLocked;
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,在執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
             stringRedisTemplate.delete(key);   
        }
    }
}

這樣寫看似沒有啥問題,但是大家細(xì)想一下,這樣寫就真的OK了嗎?

可重入性的問題分析

既然上面分布式鎖的可重入性是存在問題的,那我們就來分析下問題的根源在哪里!

假設(shè)我們提交訂單的方法中,首先使用RedisLock接口對代碼塊添加了分布式鎖,在加鎖后的代碼中調(diào)用了服務(wù)A,而服務(wù)A中也存在調(diào)用RedisLock接口的加鎖和解鎖操作。而多次調(diào)用RedisLock接口的加鎖操作時,只要之前的鎖沒有失效,則會直接返回true,表示成功獲取鎖。也就是說,無論調(diào)用加鎖操作多少次,最終只會成功加鎖一次。而執(zhí)行完服務(wù)A中的邏輯后,在服務(wù)A中調(diào)用RedisLock接口的解鎖方法,此時,會將當(dāng)前線程所有的加鎖操作獲得的鎖全部釋放掉。

我們可以使用下圖來簡單的表示這個過程。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
   <p style=那么問題來了,如何解決可重入性的問題呢?

解決可重入性問題

相信很多小伙伴都能夠想出使用計(jì)數(shù)器的方式來解決上面可重入性的問題,沒錯,就是使用計(jì)數(shù)器來解決。 整體流程如下所示。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
   <p style=那么,體現(xiàn)在程序代碼上是什么樣子呢?我們來修改RedisLockImpl類的代碼,如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        Boolean isLocked = false;
        if(threadLocal.get() == null){
            String uuid = UUID.randomUUID().toString();
            threadLocal.set(uuid);
            isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
        }else{
            isLocked = true;   
        }
        //加鎖成功后將計(jì)數(shù)器加1
        if(isLocked){
            Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
            threadLocalInteger.set(count++);
        }
        return isLocked;
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,再執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
            Integer count = threadLocalInteger.get();
            //計(jì)數(shù)器減為0時釋放鎖
            if(count == null || --count <= 0){
                 stringRedisTemplate.delete(key);      
            }
        }
    }
}

至此,我們基本上解決了分布式鎖的可重入性問題。

說到這里,我還要問大家一句,上面的解決問題的方案真的沒問題了嗎?

阻塞與非阻塞鎖

在提交訂單的方法中,當(dāng)獲取Redis分布式鎖失敗時,我們直接返回了failure來表示當(dāng)前請求下單的操作失敗了。試想,在高并發(fā)環(huán)境下,一旦某個請求獲得了分布式鎖,那么,在這個請求釋放鎖之前,其他的請求調(diào)用下單方法時,都會返回下單失敗的信息。在真實(shí)場景中,這是非常不友好的。我們可以將后續(xù)的請求進(jìn)行阻塞,直到當(dāng)前請求釋放鎖后,再喚醒阻塞的請求獲得分布式鎖來執(zhí)行方法。

所以,我們設(shè)計(jì)的分布式鎖需要支持 阻塞和非阻塞 的特性。

那么,如何實(shí)現(xiàn)阻塞呢?我們可以使用自旋來實(shí)現(xiàn),繼續(xù)修改RedisLockImpl的代碼如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        Boolean isLocked = false;
        if(threadLocal.get() == null){
            String uuid = UUID.randomUUID().toString();
            threadLocal.set(uuid);
            isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
            //如果獲取鎖失敗,則自旋獲取鎖,直到成功
            if(!isLocked){
                for(;;){
                    isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
                    if(isLocked){
                        break;
                    }
                }
            }
        }else{
            isLocked = true;   
        }
        //加鎖成功后將計(jì)數(shù)器加1
        if(isLocked){
            Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
            threadLocalInteger.set(count++);
        }
        return isLocked;
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,再執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
            Integer count = threadLocalInteger.get();
            //計(jì)數(shù)器減為0時釋放鎖
            if(count == null || --count <= 0){
                 stringRedisTemplate.delete(key);      
            }
        }
    }
}

在分布式鎖的設(shè)計(jì)中,阻塞鎖和非阻塞鎖 是非常重要的概念,大家一定要記住這個知識點(diǎn)。

鎖失效問題

盡管我們實(shí)現(xiàn)了分布式鎖的阻塞特性,但是還有一個問題是我們不得不考慮的。那就是 鎖失效 的問題。

當(dāng)程序執(zhí)行業(yè)務(wù)的時間超過了鎖的過期時間會發(fā)生什么呢? 想必很多小伙伴都能夠想到,那就是前面的請求沒執(zhí)行完,鎖過期失效了,后面的請求獲取到分布式鎖,繼續(xù)向下執(zhí)行了,程序無法做到真正的互斥,無法保證業(yè)務(wù)的原子性了。

那如何解決這個問題呢?答案就是:我們必須保證在業(yè)務(wù)代碼執(zhí)行完畢后,才能釋放分布式鎖。 方案是有了,那如何實(shí)現(xiàn)呢?

說白了,我們需要在業(yè)務(wù)代碼中,時不時的執(zhí)行下面的代碼來保證在業(yè)務(wù)代碼沒執(zhí)行完時,分布式鎖不會因超時而被釋放。

springRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);

這里,我們需要定義一個定時策略來執(zhí)行上面的代碼,需要注意的是:我們不能等到30秒后再執(zhí)行上述代碼,因?yàn)?0秒時,鎖已經(jīng)失效了。例如,我們可以每10秒執(zhí)行一次上面的代碼。

有些小伙伴說,直接在RedisLockImpl類中添加一個while(true)循環(huán)來解決這個問題,那我們就這樣修改下RedisLockImpl類的代碼,看看有沒有啥問題。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        Boolean isLocked = false;
        if(threadLocal.get() == null){
            String uuid = UUID.randomUUID().toString();
            threadLocal.set(uuid);
            isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
            //如果獲取鎖失敗,則自旋獲取鎖,直到成功
            if(!isLocked){
                for(;;){
                    isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
                    if(isLocked){
                        break;
                    }
                }
            }
            //定義更新鎖的過期時間
            while(true){
                Integer count = threadLocalInteger.get();
                //當(dāng)前鎖已經(jīng)被釋放,則退出循環(huán)
                if(count == 0 || count <= 0){
                    break;
                }
                springRedisTemplate.expire(key, 30, TimeUnit.SECONDS);
                try{
                    //每隔10秒執(zhí)行一次
                    Thread.sleep(10000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }else{
            isLocked = true;   
        }
        //加鎖成功后將計(jì)數(shù)器加1
        if(isLocked){
            Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
            threadLocalInteger.set(count++);
        }
        return isLocked;
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,再執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
            Integer count = threadLocalInteger.get();
            //計(jì)數(shù)器減為0時釋放鎖
            if(count == null || --count <= 0){
                 stringRedisTemplate.delete(key);      
            }
        }
    }
}

相信小伙伴們看了代碼就會發(fā)現(xiàn)哪里有問題了:更新鎖過期時間的代碼肯定不能這么去寫。因?yàn)檫@么寫會 導(dǎo)致當(dāng)前線程在更新鎖超時時間的while(true)循環(huán)中一直阻塞而無法返回結(jié)果。 所以,我們不能將當(dāng)前線程阻塞,需要異步執(zhí)行定時任務(wù)來更新鎖的過期時間。

此時,我們繼續(xù)修改RedisLockImpl類的代碼,將定時更新鎖超時的代碼放到一個單獨(dú)的線程中執(zhí)行,如下所示。

public class RedisLockImpl implements RedisLock{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();

    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit){
        Boolean isLocked = false;
        if(threadLocal.get() == null){
            String uuid = UUID.randomUUID().toString();
            threadLocal.set(uuid);
            isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
            //如果獲取鎖失敗,則自旋獲取鎖,直到成功
            if(!isLocked){
                for(;;){
                    isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
                    if(isLocked){
                        break;
                    }
                }
            }
            //啟動新線程來執(zhí)行定時任務(wù),更新鎖過期時間
           new Thread(new Runnable{
                @Override
                public void run(){
                     //定義更新鎖的過期時間
                    while(true){
                        Integer count = threadLocalInteger.get();
                         //當(dāng)前鎖已經(jīng)被釋放,則退出循環(huán)
                        if(count == 0 || count <= 0){
                            break;
                        }
                        springRedisTemplate.expire(key, 30, TimeUnit.SECONDS);
                        try{
                            //每隔10秒執(zhí)行一次
                            Thread.sleep(10000);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }else{
            isLocked = true;   
        }
        //加鎖成功后將計(jì)數(shù)器加1
        if(isLocked){
            Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
            threadLocalInteger.set(count++);
        }
        return isLocked;
    }
    @Override
    public void releaseLock(String key){
        //當(dāng)前線程中綁定的key與Redis中的Key相同時,再執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
            Integer count = threadLocalInteger.get();
            //計(jì)數(shù)器減為0時釋放鎖
            if(count == null || --count <= 0){
                 stringRedisTemplate.delete(key);      
            }
        }
    }
}

上述解決分布式鎖失效的問題在分布式鎖領(lǐng)域有一個專業(yè)的術(shù)語叫做 “異步續(xù)命” 。需要注意的是:當(dāng)業(yè)務(wù)代碼執(zhí)行完畢后,我們需要停止更新鎖超時時間的線程。所以,這里,我在鎖超時的線程中寫了如下的代碼。

Integer count = threadLocalInteger.get();
//當(dāng)前鎖已經(jīng)被釋放,則退出循環(huán)
if(count == 0 || count <= 0){
    break;
}

表示鎖被釋放后,退出當(dāng)前循環(huán),進(jìn)而可以退出當(dāng)前線程。

實(shí)現(xiàn)分布式鎖的基本要求

結(jié)合上述的案例,我們可以得出實(shí)現(xiàn)分布式鎖的基本要求:

  • 支持互斥性

  • 支持鎖超時

  • 支持阻塞和非阻塞特性

  • 支持可重入性

  • 支持高可用

通用分布式解決方案

在互聯(lián)網(wǎng)行業(yè),分布式鎖是一個繞不開的話題,同時,也有很多通用的分布式鎖解決方案,其中,用的比較多的一種方案就是使用開源的Redisson框架來解決分布式鎖問題。

有關(guān)Redisson分布式鎖的使用方案大家可以參考《【高并發(fā)】你知道嗎?大家都在使用Redisson實(shí)現(xiàn)分布式鎖了!!

既然Redisson框架已經(jīng)很牛逼了,我們直接使用Redisson框架是否能夠100%的保證分布式鎖不出問題呢?答案是無法100%的保證。因?yàn)樵诜植际筋I(lǐng)域沒有哪一家公司或者架構(gòu)師能夠保證100%的不出問題,就連阿里這樣的大公司、阿里的首席架構(gòu)師這樣的技術(shù)大牛也不敢保證100%的不出問題。

在分布式領(lǐng)域,無法做到100%無故障,我們追求的是幾個9的目標(biāo),例如99.999%無故障。

CAP理論

在分布式領(lǐng)域,有一個非常重要的理論叫做CAP理論。

  • C:Consistency(一致性)

  • A:Availability(可用性)

  • P:Partition tolerance(分區(qū)容錯性)

在分布式領(lǐng)域中,是必須要保證分區(qū)容錯性的,也就是必須要保證“P”,所以,我們只能保證CP或者AP。

這里,我們可以使用Redis和Zookeeper來進(jìn)行簡單的對比,我們可以使用Redis實(shí)現(xiàn)AP架構(gòu)的分布式鎖,使用Zookeeper實(shí)現(xiàn)CP架構(gòu)的分布式鎖。

  • 基于Redis的AP架構(gòu)的分布式鎖模型

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
   <p style=在基于Redis實(shí)現(xiàn)的AP架構(gòu)的分布式鎖模型中,向Redis節(jié)點(diǎn)1寫入數(shù)據(jù)后,會立即返回結(jié)果,之后在Redis中會以異步的方式來同步數(shù)據(jù)。

  • 基于Zookeeper的CP架構(gòu)的分布式鎖模型

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
   <p style=在基于Zookeeper實(shí)現(xiàn)的CP架構(gòu)的分布式模型中,向節(jié)點(diǎn)1寫入數(shù)據(jù)后,會等待數(shù)據(jù)的同步結(jié)果,當(dāng)數(shù)據(jù)在大多數(shù)Zookeeper節(jié)點(diǎn)間同步成功后,才會返回結(jié)果數(shù)據(jù)。

當(dāng)我們使用基于Redis的AP架構(gòu)實(shí)現(xiàn)分布式鎖時,需要注意一個問題,這個問題可以使用下圖來表示。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

也就是Redis主從節(jié)點(diǎn)之間的數(shù)據(jù)同步失敗,假設(shè)線程向Master節(jié)點(diǎn)寫入了數(shù)據(jù),而Redis中Master節(jié)點(diǎn)向Slave節(jié)點(diǎn)同步數(shù)據(jù)失敗了。此時,另一個線程讀取的Slave節(jié)點(diǎn)中的數(shù)據(jù),發(fā)現(xiàn)沒有添加分布式鎖,此時就會出現(xiàn)問題了?。?!

所以,在設(shè)計(jì)分布式鎖方案時,也需要注意Redis節(jié)點(diǎn)之間的數(shù)據(jù)同步問題。

紅鎖的實(shí)現(xiàn)

在Redisson框架中,實(shí)現(xiàn)了紅鎖的機(jī)制,Redisson的RedissonRedLock對象實(shí)現(xiàn)了Redlock介紹的加鎖算法。該對象也可以用來將多個RLock對象關(guān)聯(lián)為一個紅鎖,每個RLock對象實(shí)例可以來自于不同的Redisson實(shí)例。當(dāng)紅鎖中超過半數(shù)的RLock加鎖成功后,才會認(rèn)為加鎖是成功的,這就提高了分布式鎖的高可用。

我們可以使用Redisson框架來實(shí)現(xiàn)紅鎖。

public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){
    RLock lock1 = redisson1.getLock("lock1");
    RLock lock2 = redisson2.getLock("lock2");
    RLock lock3 = redisson3.getLock("lock3");
    RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
    try {
        // 同時加鎖:lock1 lock2 lock3, 紅鎖在大部分節(jié)點(diǎn)上加鎖成功就算成功。
        lock.lock();
        // 嘗試加鎖,最多等待100秒,上鎖以后10秒自動解鎖
        boolean res = lock.tryLock(10010, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

其實(shí),在實(shí)際場景中,紅鎖是很少使用的。這是因?yàn)槭褂昧思t鎖后會影響高并發(fā)環(huán)境下的性能,使得程序的體驗(yàn)更差。所以,在實(shí)際場景中,我們一般都是要保證Redis集群的可靠性。同時,使用紅鎖后,當(dāng)加鎖成功的RLock個數(shù)不超過總數(shù)的一半時,會返回加鎖失敗,即使在業(yè)務(wù)層面任務(wù)加鎖成功了,但是紅鎖也會返回加鎖失敗的結(jié)果。另外,使用紅鎖時,需要提供多套Redis的主從部署架構(gòu),同時,這多套Redis主從架構(gòu)中的Master節(jié)點(diǎn)必須都是獨(dú)立的,相互之間沒有任何數(shù)據(jù)交互。

高并發(fā)“黑科技”與致勝奇招

假設(shè),我們就是使用Redis來實(shí)現(xiàn)分布式鎖,假設(shè)Redis的讀寫并發(fā)量在5萬左右。我們的商城業(yè)務(wù)需要支持的并發(fā)量在100萬左右。如果這100萬的并發(fā)全部打入Redis中,Redis很可能就會掛掉,那么,我們?nèi)绾谓鉀Q這個問題呢?接下來,我們就一起來探討這個問題。

在高并發(fā)的商城系統(tǒng)中,如果采用Redis緩存數(shù)據(jù),則Redis緩存的并發(fā)處理能力是關(guān)鍵,因?yàn)楹芏嗟那熬Y操作都需要訪問Redis。而異步削峰只是基本的操作,關(guān)鍵還是要保證Redis的并發(fā)處理能力。

解決這個問題的關(guān)鍵思想就是:分而治之,將商品庫存分開放。

暗度陳倉

我們在Redis中存儲商品的庫存數(shù)量時,可以將商品的庫存進(jìn)行“分割”存儲來提升Redis的讀寫并發(fā)量。

例如,原來的商品的id為10001,庫存為1000件,在Redis中的存儲為(10001, 1000),我們將原有的庫存分割為5份,則每份的庫存為200件,此時,我們在Redia中存儲的信息為(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

此時,我們將庫存進(jìn)行分割后,每個分割后的庫存使用商品id加上一個數(shù)字標(biāo)識來存儲,這樣,在對存儲商品庫存的每個Key進(jìn)行Hash運(yùn)算時,得出的Hash結(jié)果是不同的,這就說明,存儲商品庫存的Key有很大概率不在Redis的同一個槽位中,這就能夠提升Redis處理請求的性能和并發(fā)量。

分割庫存后,我們還需要在Redis中存儲一份商品id和分割庫存后的Key的映射關(guān)系,此時映射關(guān)系的Key為商品的id,也就是10001,Value為分割庫存后存儲庫存信息的Key,也就是10001_0,10001_1,10001_2,10001_3,10001_4。在Redis中我們可以使用List來存儲這些值。

在真正處理庫存信息時,我們可以先從Redis中查詢出商品對應(yīng)的分割庫存后的所有Key,同時使用AtomicLong來記錄當(dāng)前的請求數(shù)量,使用請求數(shù)量對從Redia中查詢出的商品對應(yīng)的分割庫存后的所有Key的長度進(jìn)行求模運(yùn)算,得出的結(jié)果為0,1,2,3,4。再在前面拼接上商品id就可以得出真正的庫存緩存的Key。此時,就可以根據(jù)這個Key直接到Redis中獲取相應(yīng)的庫存信息。

同時,我們可以將分隔的不同的庫存數(shù)據(jù)分別存儲到不同的Redis服務(wù)器中,進(jìn)一步提升Redis的并發(fā)量。

移花接木

在高并發(fā)業(yè)務(wù)場景中,我們可以直接使用Lua腳本庫(OpenResty)從負(fù)載均衡層直接訪問緩存。

這里,我們思考一個場景:如果在高并發(fā)業(yè)務(wù)場景中,商品被瞬間搶購一空。此時,用戶再發(fā)起請求時,如果系統(tǒng)由負(fù)載均衡層請求應(yīng)用層的各個服務(wù),再由應(yīng)用層的各個服務(wù)訪問緩存和數(shù)據(jù)庫,其實(shí),本質(zhì)上已經(jīng)沒有任何意義了,因?yàn)樯唐芬呀?jīng)賣完了,再通過系統(tǒng)的應(yīng)用層進(jìn)行層層校驗(yàn)已經(jīng)沒有太多意義了?。《鴳?yīng)用層的并發(fā)訪問量是以百為單位的,這又在一定程度上會降低系統(tǒng)的并發(fā)度。

為了解決這個問題,此時,我們可以在系統(tǒng)的負(fù)載均衡層取出用戶發(fā)送請求時攜帶的用戶id,商品id和活動id等信息,直接通過Lua腳本等技術(shù)來訪問緩存中的庫存信息。如果商品的庫存小于或者等于0,則直接返回用戶商品已售完的提示信息,而不用再經(jīng)過應(yīng)用層的層層校驗(yàn)了。

寫在最后

最后,附上并發(fā)編程需要掌握的核心技能知識圖,祝大家在學(xué)習(xí)并發(fā)編程時,少走彎路。

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p>
  </section>
  <section style=

特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖?。? ></p><pre ng-bind-html=

長按訂閱更多精彩▼

【高并發(fā)】高并發(fā)分布式鎖架構(gòu)解密,不是所有的鎖都是分布式鎖??!

如有收獲,點(diǎn)個在看,誠摯感謝

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

LED驅(qū)動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關(guān)鍵字: 驅(qū)動電源

在工業(yè)自動化蓬勃發(fā)展的當(dāng)下,工業(yè)電機(jī)作為核心動力設(shè)備,其驅(qū)動電源的性能直接關(guān)系到整個系統(tǒng)的穩(wěn)定性和可靠性。其中,反電動勢抑制與過流保護(hù)是驅(qū)動電源設(shè)計(jì)中至關(guān)重要的兩個環(huán)節(jié),集成化方案的設(shè)計(jì)成為提升電機(jī)驅(qū)動性能的關(guān)鍵。

關(guān)鍵字: 工業(yè)電機(jī) 驅(qū)動電源

LED 驅(qū)動電源作為 LED 照明系統(tǒng)的 “心臟”,其穩(wěn)定性直接決定了整個照明設(shè)備的使用壽命。然而,在實(shí)際應(yīng)用中,LED 驅(qū)動電源易損壞的問題卻十分常見,不僅增加了維護(hù)成本,還影響了用戶體驗(yàn)。要解決這一問題,需從設(shè)計(jì)、生...

關(guān)鍵字: 驅(qū)動電源 照明系統(tǒng) 散熱

根據(jù)LED驅(qū)動電源的公式,電感內(nèi)電流波動大小和電感值成反比,輸出紋波和輸出電容值成反比。所以加大電感值和輸出電容值可以減小紋波。

關(guān)鍵字: LED 設(shè)計(jì) 驅(qū)動電源

電動汽車(EV)作為新能源汽車的重要代表,正逐漸成為全球汽車產(chǎn)業(yè)的重要發(fā)展方向。電動汽車的核心技術(shù)之一是電機(jī)驅(qū)動控制系統(tǒng),而絕緣柵雙極型晶體管(IGBT)作為電機(jī)驅(qū)動系統(tǒng)中的關(guān)鍵元件,其性能直接影響到電動汽車的動力性能和...

關(guān)鍵字: 電動汽車 新能源 驅(qū)動電源

在現(xiàn)代城市建設(shè)中,街道及停車場照明作為基礎(chǔ)設(shè)施的重要組成部分,其質(zhì)量和效率直接關(guān)系到城市的公共安全、居民生活質(zhì)量和能源利用效率。隨著科技的進(jìn)步,高亮度白光發(fā)光二極管(LED)因其獨(dú)特的優(yōu)勢逐漸取代傳統(tǒng)光源,成為大功率區(qū)域...

關(guān)鍵字: 發(fā)光二極管 驅(qū)動電源 LED

LED通用照明設(shè)計(jì)工程師會遇到許多挑戰(zhàn),如功率密度、功率因數(shù)校正(PFC)、空間受限和可靠性等。

關(guān)鍵字: LED 驅(qū)動電源 功率因數(shù)校正

在LED照明技術(shù)日益普及的今天,LED驅(qū)動電源的電磁干擾(EMI)問題成為了一個不可忽視的挑戰(zhàn)。電磁干擾不僅會影響LED燈具的正常工作,還可能對周圍電子設(shè)備造成不利影響,甚至引發(fā)系統(tǒng)故障。因此,采取有效的硬件措施來解決L...

關(guān)鍵字: LED照明技術(shù) 電磁干擾 驅(qū)動電源

開關(guān)電源具有效率高的特性,而且開關(guān)電源的變壓器體積比串聯(lián)穩(wěn)壓型電源的要小得多,電源電路比較整潔,整機(jī)重量也有所下降,所以,現(xiàn)在的LED驅(qū)動電源

關(guān)鍵字: LED 驅(qū)動電源 開關(guān)電源

LED驅(qū)動電源是把電源供應(yīng)轉(zhuǎn)換為特定的電壓電流以驅(qū)動LED發(fā)光的電壓轉(zhuǎn)換器,通常情況下:LED驅(qū)動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關(guān)鍵字: LED 隧道燈 驅(qū)動電源
關(guān)閉