欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Redis高并發(fā)超賣問題解決方案圖文詳解

 更新時間:2024年02月17日 11:04:17   作者:山河亦問安  
Redis是一種基于內(nèi)存的數(shù)據(jù)存儲系統(tǒng),被廣泛用于解決高并發(fā)問題,下面這篇文章主要給大家介紹了關(guān)于Redis高并發(fā)超賣問題解決方案的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下

1. Redis高并發(fā)超賣問題解決方案

在高并發(fā)的秒殺搶購場景中,常常會面臨一個稱為“超賣”(Over-Selling)的問題。超賣指的是同一件商品被售出的數(shù)量超過了實際庫存數(shù)量,導(dǎo)致庫存出現(xiàn)負(fù)數(shù)。這是由于多個用戶同時發(fā)起搶購請求,而系統(tǒng)未能有效地控制庫存的并發(fā)訪問。

下面進(jìn)行一個秒殺購買某個商品的接口模擬,代碼如下:

@RestController
public class MyController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/buy/{id}")
    public String buy(@PathVariable("id") Long id){
        String key="product_" + id;
        int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
        if(count>0){
            stringRedisTemplate.opsForValue().set(key, String.valueOf(--count));
            System.out.println(key+"商品購買成功,剩余庫存"+count);
            return "success";
        }
        System.out.println(key+"商品庫存不足");
        return "error";
    }
}

上面的代碼在高并發(fā)環(huán)境下容易出現(xiàn)超賣問題,使用JMeter進(jìn)行壓測,如下圖:

 進(jìn)行壓測獲得的日志如下圖,存在并發(fā)安全問題。

 要解決上面的問題,我們一開始想到的是synchronized加鎖,但是在 Redis 的高并發(fā)環(huán)境下,使用 Java 中的 synchronized關(guān)鍵字來解決超賣問題是行不通的,原因如下:

  • 分布式環(huán)境下無效: synchronized是 Java 中的關(guān)鍵字,用于在單個 JVM 中保護(hù)共享資源。在分布式環(huán)境下,多個服務(wù)實例之間無法通過synchronized來同步,因為各個實例之間無法直接共享 JVM 中的鎖。

  • 性能問題: synchronized會導(dǎo)致性能問題,尤其在高并發(fā)的情況下,爭奪鎖可能會成為瓶頸。

對于 Redis 高并發(fā)環(huán)境下的超賣問題,更合適的解決方案通常是使用 Redis 提供的分布式鎖(如基于 Redis 的分布式鎖實現(xiàn))。這可以確保在分布式環(huán)境中的原子性和可靠性。

基于Redis的分布式鎖,我們可以基于Redis中的Setnx(命令在指定的 key 不存在時,為 key 設(shè)置指定的值),更改代碼如下:

@RestController
public class MyController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/buy/{id}")
    public String buy(@PathVariable("id") Long id){
        String lock="product_lock_"+id;
        String key="product_" + id;
        Boolean lock1 = stringRedisTemplate.opsForValue().setIfAbsent(lock, "lock");
        String message="error";
        if(!lock1){
            System.out.println("業(yè)務(wù)繁忙稍后再試");
            return "業(yè)務(wù)繁忙稍后再試";
        }
        //try catch 設(shè)計是為了防止在執(zhí)行業(yè)務(wù)的時候出現(xiàn)異常導(dǎo)致redis鎖一直無法釋放
        try {
                int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
                if (count > 0) {
                    stringRedisTemplate.opsForValue().set(key, String.valueOf(--count));
                    System.out.println(key + "商品購買成功,剩余庫存" + count);
                    message="success";
                }
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            stringRedisTemplate.delete(lock);
        }
        if(message.equals("error"))
        System.out.println(key+"商品庫存不足");
        return message;
    }
}

然后使用JMeter壓測,在10s內(nèi)陸續(xù)發(fā)送500個請求,日志如下圖,由圖可以看出基本解決超賣問題。

1.1 高并發(fā)場景超賣bug解析

系統(tǒng)在達(dá)到finally塊之前崩潰宕機(jī),鎖可能會一直存在于Redis中。這可能會導(dǎo)致其他進(jìn)程或線程無法在未來獲取該鎖,從而導(dǎo)致資源被鎖定,后續(xù)嘗試訪問該資源的操作可能被阻塞。因此在redis中給定 key設(shè)置過期時間。代碼如下:

@RestController
public class MyController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/buy/{id}")
    public String buy(@PathVariable("id") Long id){
        String lock="product_lock_"+id;
        String key="product_" + id;
        Boolean lock1 = stringRedisTemplate.opsForValue().setIfAbsent(lock, "lock",10, TimeUnit.SECONDS); //保證原子性
//        Boolean lock2 = stringRedisTemplate.opsForValue().setIfAbsent(lock, "lock");
//        stringRedisTemplate.expire(lock,10,TimeUnit.SECONDS); //此時宕機(jī)依舊會出現(xiàn)redis鎖無法釋放,應(yīng)設(shè)置為原子操作
        String message="error";
        if(!lock1){
            System.out.println("業(yè)務(wù)繁忙稍后再試");
            return "業(yè)務(wù)繁忙稍后再試";
        }
        //try catch 設(shè)計是為了防止在執(zhí)行業(yè)務(wù)的時候出現(xiàn)異常導(dǎo)致redis鎖一直無法釋放
        try {
                int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
                if (count > 0) {
                    stringRedisTemplate.opsForValue().set(key, String.valueOf(--count));
                    System.out.println(key + "商品購買成功,剩余庫存" + count);
                    message="success";
                }
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            stringRedisTemplate.delete(lock);
        }
        if(message.equals("error"))
        System.out.println(key+"商品庫存不足");
        return message;
    }
}

在高并發(fā)場景下,還存在一個問題,即業(yè)務(wù)執(zhí)行時間過長可能導(dǎo)致 Redis 鎖提前釋放,并且誤刪除其他線程或進(jìn)程持有的鎖。這可能發(fā)生在以下情況:

  • 線程A獲取鎖并開始執(zhí)行業(yè)務(wù)邏輯。
  • 由于高并發(fā),其他線程B、C等也嘗試獲取相同資源的鎖。
  • 由于鎖的過期時間設(shè)置為10秒,線程A的業(yè)務(wù)邏輯執(zhí)行時間超過10秒,導(dǎo)致其鎖被 Redis 自動釋放。
  • 線程B在10秒內(nèi)獲取到了之前由線程A持有的鎖,并開始執(zhí)行業(yè)務(wù)邏輯。
  • 線程A在業(yè)務(wù)邏輯執(zhí)行完成后,嘗試刪除自己的鎖,但由于已經(jīng)被線程B持有,線程A實際上刪除的是線程B的鎖。

修改代碼如下:

@RestController
public class MyController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/buy/{id}")
    public String buy(@PathVariable("id") Long id){
        String lock="product_lock_"+id;
        String key="product_" + id;
        String clientId=UUID.randomUUID().toString();
        Boolean lock1 = stringRedisTemplate.opsForValue().setIfAbsent(lock, clientId,10, TimeUnit.SECONDS); //保證原子性
//        Boolean lock2 = stringRedisTemplate.opsForValue().setIfAbsent(lock, "lock");
//        stringRedisTemplate.expire(lock,10,TimeUnit.SECONDS); //此時宕機(jī)依舊會出現(xiàn)redis鎖無法釋放,應(yīng)設(shè)置為原子操作
        String message="error";
        if(!lock1){
            System.out.println("業(yè)務(wù)繁忙稍后再試");
            return "業(yè)務(wù)繁忙稍后再試";
        }
        //try catch 設(shè)計是為了防止在執(zhí)行業(yè)務(wù)的時候出現(xiàn)異常導(dǎo)致redis鎖一直無法釋放
        try {
                int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
                if (count > 0) {
                    stringRedisTemplate.opsForValue().set(key, String.valueOf(--count));
                    System.out.println(key + "商品購買成功,剩余庫存" + count);
                    message="success";
                }
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            if (stringRedisTemplate.opsForValue().get(lock).equals(clientId))//在這里如果有別的業(yè)務(wù)代碼并且耗時較長, stringRedisTemplate.delete(lock)之前還是有可能超過過期時間出現(xiàn)問題
                stringRedisTemplate.delete(lock);
        }
        if(message.equals("error"))
        System.out.println(key+"商品庫存不足");
        return message;
    }
}

上面的代碼在高并發(fā)場景下仍然存在概率很低的問題,所以就有了redisson分布式鎖。

1.2 Redisson

Redisson 是一個用于 Java 的 Redis 客戶端,它提供了豐富的功能,包括分布式鎖。Redisson 的分布式鎖實現(xiàn)了基于 Redis 的分布式鎖,具有簡單易用、可靠性高的特點。

以下是 Redisson 分布式鎖的一些重要特性和用法:

  • 可重入鎖: Redisson 的分布式鎖是可重入的,同一線程可以多次獲取同一把鎖,而不會出現(xiàn)死鎖。

  • 公平鎖: Redisson 支持公平鎖,即按照獲取鎖的順序依次獲取,避免了某些線程一直獲取不到鎖的情況。

  • 鎖超時: 可以為分布式鎖設(shè)置過期時間,確保即使在某些情況下鎖沒有被顯式釋放,也能在一定時間后自動釋放。

  • 異步鎖: Redisson 提供了異步的分布式鎖,通過異步 API 可以在不阻塞線程的情況下獲取和釋放鎖。

  • 監(jiān)控鎖狀態(tài): Redisson 允許監(jiān)控鎖的狀態(tài),包括鎖是否被某個線程持有,鎖的過期時間等。

導(dǎo)入依賴

       <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.23.5</version>
        </dependency>

application.yaml 配置:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: 1000ms

RedissonConfig配置:

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;


    /**
     * RedissonClient,單機(jī)模式
     */
    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
    }
}

利用Redisson分布式鎖解決超賣問題,修改代碼如下:

加鎖 lock.lock()

阻塞等待,默認(rèn)等待,加鎖的默認(rèn)時間都是30s,鎖的自動續(xù)期,如果業(yè)務(wù)時間長,運行期間會自動給鎖續(xù)上新的30s,不用擔(dān)心業(yè)務(wù)時間長導(dǎo)致鎖自動過期被刪除,加鎖的業(yè)務(wù)只要運行完成,就不會給當(dāng)前鎖續(xù)期,即使不手動解鎖,鎖默認(rèn)在30s后自動刪除。

加鎖 lock.lock(10,TimeUnit.SECONDS)

鎖到期后,不會自動續(xù)期,如果傳遞了鎖的超時時間,就發(fā)送給redis執(zhí)行腳本,進(jìn)行占鎖,默認(rèn)超時就是我們指定的時間。如果未指定鎖的超時時間,只要占鎖成功,就會啟動一個定時任務(wù)【重新給鎖設(shè)置過期時間,新的過期時間就是看門狗的默認(rèn)時間】,每隔10s就會自動進(jìn)行續(xù)期。

@RestController
public class MyController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redisson;

    @RequestMapping("/buy/{id}")
    public String buy(@PathVariable("id") Long id){
        String message="error";
        String lock_key="product_lock_"+id;
        String key="product_" + id;
        RLock lock = redisson.getLock(lock_key);
        //try catch 設(shè)計是為了防止在執(zhí)行業(yè)務(wù)的時候出現(xiàn)異常導(dǎo)致redis鎖一直無法釋放
        try {
                lock.lock();
                int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
                if (count > 0) {
                    stringRedisTemplate.opsForValue().set(key, String.valueOf(--count));
                    System.out.println(key + "商品購買成功,剩余庫存" + count);
                    message="success";
                }
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        if(message.equals("error"))
        System.out.println(key+"商品庫存不足");
        return message;
    }
}

總結(jié) 

到此這篇關(guān)于Redis高并發(fā)超賣問題解決方案的文章就介紹到這了,更多相關(guān)Redis高并發(fā)超賣問題解決內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis緩存lettuce更換為Jedis的實現(xiàn)步驟

    Redis緩存lettuce更換為Jedis的實現(xiàn)步驟

    在springboot中引入spring-boot-starter-data-redis依賴時,默認(rèn)使用的是lettuce,如果不想使用lettuce而是使用Jedis連接池,本文主要介紹了Redis緩存lettuce更換為Jedis的實現(xiàn)步驟,感興趣的可以了解一下
    2024-08-08
  • redis中使用redis-dump導(dǎo)出、導(dǎo)入、還原數(shù)據(jù)實例

    redis中使用redis-dump導(dǎo)出、導(dǎo)入、還原數(shù)據(jù)實例

    這篇文章主要介紹了redis中使用redis-dump導(dǎo)出、導(dǎo)入、還原數(shù)據(jù)實例,本文直接給出操作命令,并給出注釋加以說明,需要的朋友可以參考下
    2014-11-11
  • Redis分布式鎖升級版RedLock及SpringBoot實現(xiàn)方法

    Redis分布式鎖升級版RedLock及SpringBoot實現(xiàn)方法

    這篇文章主要介紹了Redis分布式鎖升級版RedLock及SpringBoot實現(xiàn),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-02-02
  • redis緩存穿透解決方法

    redis緩存穿透解決方法

    在本篇文章里小編給大家分享了關(guān)于redis緩存穿透的解決方法以及相關(guān)實例內(nèi)容,需要的朋友們學(xué)習(xí)下。
    2019-06-06
  • Redis分布式鎖解決超賣問題的使用示例

    Redis分布式鎖解決超賣問題的使用示例

    超賣問題通常出現(xiàn)在多用戶并發(fā)操作的情況下,即多個用戶嘗試購買同一件商品,導(dǎo)致商品庫存不足或者超賣,本文就來介紹一下超賣問題,感興趣的可以了解一下
    2023-09-09
  • 一文帶你深入理解Redis的主從架構(gòu)

    一文帶你深入理解Redis的主從架構(gòu)

    Redis主從架構(gòu)是一種分布式數(shù)據(jù)庫架構(gòu),它包括一個主節(jié)點(Master)和一個或多個從節(jié)點(Slave),主節(jié)點處理所有寫操作,從節(jié)點負(fù)責(zé)復(fù)制主節(jié)點的數(shù)據(jù)并處理讀請求,本文將帶大家深入理解Redis主從架構(gòu),需要的朋友可以參考下
    2023-09-09
  • Redis中的數(shù)據(jù)結(jié)構(gòu)跳表詳解

    Redis中的數(shù)據(jù)結(jié)構(gòu)跳表詳解

    跳表是一種基于并聯(lián)的鏈表結(jié)構(gòu),用于在有序元素序列中快速查找元素的數(shù)據(jù)結(jié)構(gòu),本文給大家介紹Redis中的數(shù)據(jù)結(jié)構(gòu)跳表,感興趣的朋友跟隨小編一起看看吧
    2024-06-06
  • 深入理解Redis哈希槽

    深入理解Redis哈希槽

    Redis哈希槽是RedisCluster中實現(xiàn)數(shù)據(jù)分片和負(fù)載均衡的核心機(jī)制,本文就來介紹一下Redis哈希槽,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-11-11
  • Redis腦裂導(dǎo)致數(shù)據(jù)丟失的解決

    Redis腦裂導(dǎo)致數(shù)據(jù)丟失的解決

    本文主要介紹了Redis腦裂導(dǎo)致數(shù)據(jù)丟失的解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • Quarkus集成redis操作Redisson實現(xiàn)數(shù)據(jù)互通

    Quarkus集成redis操作Redisson實現(xiàn)數(shù)據(jù)互通

    這篇文章主要為大家介紹了Quarkus集成redis操作Redisson實現(xiàn)數(shù)據(jù)互通的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步
    2022-02-02

最新評論