成人午夜视频全免费观看高清-秋霞福利视频一区二区三区-国产精品久久久久电影小说-亚洲不卡区三一区三区一区

Redis如何實(shí)現(xiàn)高并發(fā)分布式鎖?

眾所周知,分布式鎖在微服務(wù)架構(gòu)中是重頭戲,尤其是在互聯(lián)網(wǎng)公司,基本上企業(yè)內(nèi)部都會(huì)有自己的一套分布式鎖開(kāi)發(fā)框架。本文主要介紹使用redis如何構(gòu)建高并發(fā)分布式鎖。

成都創(chuàng)新互聯(lián)從2013年創(chuàng)立,是專(zhuān)業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站建設(shè)、網(wǎng)站制作網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元保康做網(wǎng)站,已為上家服務(wù),為??蹈鞯仄髽I(yè)和個(gè)人服務(wù),聯(lián)系電話:18982081108

假設(shè) 存在一個(gè)SpringBoot的控制器,其扣減庫(kù)存的業(yè)務(wù)邏輯如下:

@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {

    // 將庫(kù)存取出來(lái)
    int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

    // 判斷庫(kù)存夠不夠減
    if (stock > 0) {
        // 將庫(kù)存回寫(xiě)到redis
        int tmp = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", tmp.toString());
        logger.info("庫(kù)存扣減成功");
    } else {
        logger.info("庫(kù)存扣減失敗");
    }

    return "finished.";
}

不難看出,在應(yīng)用服務(wù)器運(yùn)行這段代碼的時(shí)候就會(huì)有線程安全性問(wèn)題。因?yàn)槎鄠€(gè)線程同時(shí)去修改Redis服務(wù)中的數(shù)據(jù)。因此考慮給這段代碼加上一把鎖:

@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
    synchronized (this) {
        int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        // 判斷庫(kù)存夠不夠減
        if (stock > 0) {
            // 將庫(kù)存回寫(xiě)到redis
            int tmp = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", tmp.toString());
            logger.info("庫(kù)存扣減成功");
        } else {
            logger.info("庫(kù)存扣減失敗");
        }
    }
    return "finished.";
}

這樣一來(lái),當(dāng)多個(gè)HTTP請(qǐng)求來(lái)請(qǐng)求數(shù)據(jù)的時(shí)候,多個(gè)線程去修改同一數(shù)據(jù)會(huì)有JVM本地鎖來(lái)進(jìn)行合理的資源限制。雖然這樣解決了線程安全性問(wèn)題,但是這僅僅是JVM級(jí)別的鎖,在分布式的環(huán)境下,由于像這樣的Web應(yīng)用隨時(shí)會(huì)進(jìn)行動(dòng)態(tài)擴(kuò)容,因此當(dāng)多個(gè)應(yīng)用的時(shí)候,同樣會(huì)有線程安全性問(wèn)題,當(dāng)上面這段代碼遇到類(lèi)似下面的架構(gòu)時(shí)還是會(huì)有各種各樣的問(wèn)題:

Redis如何實(shí)現(xiàn)高并發(fā)分布式鎖?

對(duì)于上述的情況,我們可以使用redis api提供的setnx方法解決:

@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {

    // 嘗試獲取鎖
    Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World");

    // 判斷是否獲得鎖
    if (!flag) { return "error"; }

    int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

    // 判斷庫(kù)存夠不夠減
    if (stock > 0) {
        // 將庫(kù)存回寫(xiě)到redis
        int tmp = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", tmp.toString());
        logger.info("庫(kù)存扣減成功");
    } else {
        logger.info("庫(kù)存扣減失敗");
    }

    // 刪除鎖
    stringRedisTemplate.delete("Hello");

    return "finished.";
}

setnx key value是將key的值設(shè)置為value,當(dāng)且僅當(dāng)key不存在的時(shí)候。如果設(shè)置成功就返回1,否則就返回0。

這樣的話,首先嘗試獲取鎖,然后當(dāng)業(yè)務(wù)執(zhí)行完成的時(shí)候再刪除鎖。但是還是有問(wèn)題的,當(dāng)獲取鎖的時(shí)候拋出異?;蛘邩I(yè)務(wù)執(zhí)行拋出異常怎么辦,所以加入異常處理邏輯:

@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
    try {
        // 嘗試獲取鎖
        Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World");

        // 判斷是否獲得鎖
        if (!flag) { return "error"; }

        int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        // 判斷庫(kù)存夠不夠減
        if (stock > 0) {
            // 將庫(kù)存回寫(xiě)到redis
            int tmp = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", tmp.toString());
            logger.info("庫(kù)存扣減成功");
        } else {
            logger.info("庫(kù)存扣減失敗");
        }
    } finally {
        // 刪除鎖
        stringRedisTemplate.delete("Hello");
    }
    return "finished.";
}

經(jīng)過(guò)這樣的修改,看起來(lái)沒(méi)什么問(wèn)題了。但是當(dāng)程序獲得鎖并且開(kāi)始執(zhí)行業(yè)務(wù)邏輯的時(shí)候,突然程序掛掉了或者被一些粗暴的運(yùn)維工程師給kill,在finally中刪除鎖的邏輯就會(huì)得不到執(zhí)行,因此就會(huì)產(chǎn)生死鎖。對(duì)于這種情況,我們可以給這個(gè)鎖設(shè)置一個(gè)超時(shí)時(shí)間:

@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
    try {
        // 嘗試獲取鎖
        Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World");

        // 設(shè)置超時(shí)時(shí)間, 根據(jù)業(yè)務(wù)場(chǎng)景估計(jì)超時(shí)時(shí)長(zhǎng)
        stringRedisTmplate.expire("Hello", 10, TimeUnit.SECONDS);

        // 判斷是否獲得鎖
        if (!flag) { return "error"; }

        int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        // 判斷庫(kù)存夠不夠減
        if (stock > 0) {
            // 將庫(kù)存回寫(xiě)到redis
            int tmp = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", tmp.toString());
            logger.info("庫(kù)存扣減成功");
        } else {
            logger.info("庫(kù)存扣減失敗");
        }
    } finally {
        // 刪除鎖
        stringRedisTemplate.delete("Hello");
    }
    return "finished.";
}

如果程序這么來(lái)寫(xiě),相對(duì)來(lái)說(shuō)安全一些了,但是還是存在問(wèn)題。試想一下,當(dāng)獲取鎖成功時(shí),正想給這把鎖設(shè)置超時(shí)的時(shí)候,程序掛掉了,還是會(huì)出現(xiàn)死鎖的,因此在redis較高的版本中提供的setIfAbsent方法中可以同時(shí)設(shè)置鎖的超時(shí)時(shí)間。

 Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World", 10, TimeUnit.SECONDS);

這樣一來(lái),嘗試獲取鎖和設(shè)置鎖的超時(shí)時(shí)間就具備原子性了。實(shí)際上經(jīng)過(guò)我們這一番改造,這在小型企業(yè)已經(jīng)沒(méi)有太大的問(wèn)題, 因?yàn)橄襁@種代碼每天也就執(zhí)行幾百次,并不算做高并發(fā)的場(chǎng)景。當(dāng)這樣的代碼被暴露在超高并發(fā)場(chǎng)景下的時(shí)候,還是會(huì)存在各種各樣的問(wèn)題。試想一個(gè)場(chǎng)景,當(dāng)一個(gè)HTTP請(qǐng)求請(qǐng)求到控制器的時(shí)候,應(yīng)用獲取到鎖了,超時(shí)時(shí)間也設(shè)置成功了,但是應(yīng)用的業(yè)務(wù)邏輯超過(guò)了超時(shí)時(shí)間,我們這里的超時(shí)時(shí)間設(shè)置的是10秒,當(dāng)應(yīng)用的業(yè)務(wù)邏輯執(zhí)行15秒的時(shí)候,鎖就被redis服務(wù)刪除了。假設(shè)恰好此時(shí)又有一個(gè)HTTP請(qǐng)求來(lái)請(qǐng)求控制器,此時(shí)應(yīng)用服務(wù)器會(huì)再啟動(dòng)一個(gè)線程來(lái)獲取鎖,而且還獲取成功了,但是這次的HTTP請(qǐng)求對(duì)應(yīng)的業(yè)務(wù)邏輯還沒(méi)有執(zhí)行完。新來(lái)的TTTP請(qǐng)求也在執(zhí)行,由于新來(lái)的HTTP請(qǐng)求也在執(zhí)行,因?yàn)殒i超時(shí)后被刪除,新的HTTP請(qǐng)求也成功獲取鎖了。當(dāng)原來(lái)的HTTP請(qǐng)求對(duì)應(yīng)的業(yè)務(wù)邏輯執(zhí)行完成以后,嘗試刪除鎖,這樣正好刪除的是新來(lái)的HTTP請(qǐng)求對(duì)應(yīng)的鎖。這個(gè)時(shí)候redis中又沒(méi)有鎖了,這樣第三個(gè)HTTP請(qǐng)求又會(huì)獲得鎖,所以情況就不妙了。

為了解決上面的問(wèn)題,我們可以將代碼優(yōu)化為下面的樣子:

@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
    String clientUuid = UUID.randomUUID().toString();
    try {
        // 嘗試獲取鎖,設(shè)置超時(shí)時(shí)間, 根據(jù)業(yè)務(wù)場(chǎng)景估計(jì)超時(shí)時(shí)長(zhǎng)
        Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", clientUuid, 10, TimeUnit.SECONDS);

        // 判斷是否獲得鎖
        if (!flag) { return "error"; }

        int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        // 判斷庫(kù)存夠不夠減
        if (stock > 0) {
            // 將庫(kù)存回寫(xiě)到redis
            int tmp = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", tmp.toString());
            logger.info("庫(kù)存扣減成功");
        } else {
            logger.info("庫(kù)存扣減失敗");
        }
    } finally {
        // 刪除鎖的時(shí)候判斷是不是自己的鎖
        if (clientUuid.equals(stringRedisTemplate.opsForValue().get("Hello"))) {
            stringRedisTemplate.delete("Hello");   
        }
    }
    return "finished.";
}

但是由于程序的不可預(yù)知性,誰(shuí)也不能保證極端情況下,同時(shí)會(huì)有多個(gè)線程同時(shí)執(zhí)行這段業(yè)務(wù)邏輯。我們可以在當(dāng)執(zhí)行業(yè)務(wù)邏輯的時(shí)候同時(shí)開(kāi)一個(gè)定時(shí)器線程,每隔幾秒就重新將這把鎖設(shè)置為10秒,也就是給這把鎖進(jìn)行“續(xù)命”。這樣就用擔(dān)心業(yè)務(wù)邏輯到底執(zhí)行多長(zhǎng)時(shí)間了。但是這樣程序的復(fù)雜性就會(huì)增加,每個(gè)業(yè)務(wù)邏輯都要寫(xiě)好多的代碼,因此這里推薦在分布式環(huán)境下使用redisson。因此我們使用redisson實(shí)現(xiàn)分支線程的代碼:

  • 引入依賴(lài):
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.6.5</version>
</dependency>
  • 初始化Redisson的客戶端配置:
@Bean
public Redisson redisson () {
    Config cfg = new Config();
    cfg.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
    return (Redisson) Redisson.create(cfg);
}
  • 在程序中注入Redisson客戶端:
@Autowired
private Redisson redisson;
  • 對(duì)應(yīng)的業(yè)務(wù)邏輯:
@Autowired
private StringRedisTemplate stringRedisTemplate;

@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
    // 獲取鎖對(duì)象
    RLock lock = redisson.getLock("Hello");
    try {
        // 嘗試加鎖, 默認(rèn)30秒, 自動(dòng)后臺(tái)開(kāi)一個(gè)線程實(shí)現(xiàn)鎖的續(xù)命
        lock.tryLock();

        int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        // 判斷庫(kù)存夠不夠減
        if (stock > 0) {
            // 將庫(kù)存回寫(xiě)到redis
            int tmp = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", tmp.toString());
            logger.info("庫(kù)存扣減成功");
        } else {
            logger.info("庫(kù)存扣減失敗");
        }
    } finally {
        // 釋放鎖
        lock.unlock();
    }
    return "finished.";
}

Redisson分布式鎖的實(shí)現(xiàn)原理如下:

Redis如何實(shí)現(xiàn)高并發(fā)分布式鎖?

但是這個(gè)架構(gòu)還是存在問(wèn)題的,因?yàn)閞edis服務(wù)器是主從的架構(gòu),當(dāng)在master節(jié)點(diǎn)設(shè)置鎖之后,slave節(jié)點(diǎn)會(huì)立刻同步。但是如果剛在master節(jié)點(diǎn)設(shè)置上了鎖,slave節(jié)點(diǎn)還沒(méi)來(lái)得及設(shè)置,master節(jié)點(diǎn)就掛掉了。還是會(huì)產(chǎn)生上同樣的問(wèn)題,新的線程獲得鎖。

因此使用redis構(gòu)建高并發(fā)的分布式鎖,僅適合單機(jī)架構(gòu),當(dāng)使用主從架構(gòu)的redis時(shí)還是會(huì)出現(xiàn)線程安全性問(wèn)題。

本文名稱(chēng):Redis如何實(shí)現(xiàn)高并發(fā)分布式鎖?
文章位置:http://jinyejixie.com/article10/pggcgo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供軟件開(kāi)發(fā)、域名注冊(cè)用戶體驗(yàn)、網(wǎng)站內(nèi)鏈定制開(kāi)發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

搜索引擎優(yōu)化
锡林浩特市| 湘潭县| 会昌县| 双辽市| 腾冲县| 吉首市| 民县| 沛县| 沛县| 通河县| 灯塔市| 宁波市| 麦盖提县| 永胜县| 锡林浩特市| 徐汇区| 康定县| 威宁| 张家界市| 垦利县| 资中县| 苗栗市| 陵川县| 潢川县| 八宿县| 略阳县| 浮山县| 三门峡市| 阿尔山市| 成武县| 新化县| 株洲县| 耒阳市| 偃师市| 定安县| 潮州市| 庄浪县| 柞水县| 武穴市| 荥阳市| 台南市|