国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題)

這篇具有很好參考價值的文章主要介紹了Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

?????作者簡介:一位大四、研0學生,正在努力準備大四暑假的實習
??上期文章:Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)6(封裝緩存工具(高級寫法)&&緩存總結(jié))
??訂閱專欄:Redis:原理速成+項目實戰(zhàn)
希望文章對你們有所幫助

這篇文章寫了很久。我自己在邊實現(xiàn)、邊用jmeter來測試、邊根據(jù)結(jié)果來優(yōu)化我的代碼,對于那些線程并發(fā)的問題,我大致是可以靠自己來解決,但是為了寫好這篇文章,為了做好線程并發(fā)問題的分析,我在獨立實現(xiàn)完之后,還是按黑馬程序員的進度走了一下,他埋坑的地方其實都是線程并發(fā)問題的坑,我也自己掉一掉,并在這篇文章中進行總結(jié)。

文章中會涉及一些java面試常見的問題:常量池Spring代理失效。如果沒有印象大家可以專門找一下這些面經(jīng)去了解一下。

聊到電商,一定離不開秒殺,而Redis在整個秒殺的業(yè)務中的作用是非常巨大的,接下來將會利用Redis實現(xiàn)全局ID,并實現(xiàn)秒殺,并且解決超賣問題、實現(xiàn)一人一單,逐漸優(yōu)化業(yè)務。

全局唯一ID

每個店鋪都可以發(fā)布優(yōu)惠券(代金券),當用戶搶購的時候,就會生成訂單并且保存到tb_voucher_order這張表中:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
可以發(fā)現(xiàn),我們的主鍵ID沒有使用自增長,這是因為如果使用數(shù)據(jù)庫自增ID就會存在一些問題:
1、ID的規(guī)律性太明顯,容易讓別人猜測到信息
2、受單表數(shù)據(jù)量的限制(訂單可能數(shù)據(jù)非常大,可能會分多表進行存儲,但表的自增長相互之間不受影響,所以不同表之間可能會出現(xiàn)ID相同的情況,也就是說這種時候會違背ID的唯一性,這顯然是不可以的)

而全局ID生成器,是一種分布式系統(tǒng)下用來生成全部唯一ID的工具,一般滿足以下特性:
1、唯一性
2、高可用
3、高性能
4、遞增性
5、安全性

除了第5點,Redis及其數(shù)據(jù)結(jié)構(gòu)已經(jīng)可以直接滿足前4點的要求了,為了增加ID的安全性,不要直接使用Redis自增的數(shù)值,而是拼接一些其他信息,最終我們將ID組成定義為64位的二進制數(shù),分別是1位符號位,31位時間戳,32位序列號:
1、符號位:1bit,永遠為0
2、時間戳:31bit,以秒為單位,可以用69年
3、序列號:32bit,秒內(nèi)的計數(shù)器,支持每秒產(chǎn)生2^32個不同的ID,這是用來處理相同秒內(nèi)(時間戳相同)的多個業(yè)務
這樣的結(jié)構(gòu)是可以大幅度提高安全性的,不同時間下的ID一定不同,相同時間的情況下,也會因為32位的序列號而導致ID不同。

Redis實現(xiàn)全局唯一ID

我們在utils包下創(chuàng)建RedisIdWorker類:

@Component
public class RedisIdWorker {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 開始時間戳由main函數(shù)運行得到
     */
    public static final long BEGIN_TIMESTAMP = 1704499200L;
    /**
     * 序列號的位數(shù)
     */
    public static final int COUNT_BITS = 32;

    public long nextId(String keyPrefix){
        //獲得當前時間
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        //生成時間戳
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        /**
         * 接下來生成序列號
         * 我們的key的設置除了加上icr表示是自增長的,還需要在最后拼接一個日期字符串
         * 這是因為我們的序列號上限是2^32,并不大,如果每天的key都是一樣的,這是很有可能超過上限的
         * 在后面拼接一個日期字符串,可以保證每一天的key都是不一樣的,而且一天內(nèi)也基本不可能到達2^32的上限
         * 這樣做還有一個好處,我們以后可以根據(jù)每天或者每月來查看value值,起到統(tǒng)計效果
         */
        //獲取當前日期,精確到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //ID自增長,這里最好用基本類型而不是包裝類,因為后面還會做運算
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        //拼接并返回,這里靈活用位運算
        return timestamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        //定義時間為2024年1月1日00:00:00
        LocalDateTime time = LocalDateTime.of(2024, 1, 6, 0, 0, 0);
        //將時間變成變成秒數(shù)的形式
        long second = time.toEpochSecond(ZoneOffset.UTC);
        //在這里運行出來的時間作為BEGIN_TIMESETAMP
        System.out.println(second);
    }
}

編寫測試代碼:

    @Resource
    private RedisIdWorker redisIdWorker;

    //性能池
    private ExecutorService es = Executors.newFixedThreadPool(500);

    @Test
    void testIdWorker() throws InterruptedException {
        //因為線程池是異步的,因此我們要用CountDownLatch去截斷,這樣才能正常計時
        CountDownLatch latch = new CountDownLatch(300);
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };
        //將任務提交300次,并進行計時
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            es.submit(task);
        }
        latch.await();//等待所有的countDown結(jié)束
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end - begin));
    }

運行后可以發(fā)現(xiàn),id各不重復,估計id生成的花費時間差不多只有2秒(id的打印也是會花時間的)
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
打開Redis客戶端,可以發(fā)現(xiàn)我成功的生成了3萬條的id:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java

優(yōu)惠券秒殺下單

每個店鋪都可以發(fā)布優(yōu)惠券,分為平價券和特價券,平價券可以任意購買,而特價券需要秒殺搶購,表關(guān)系如下:
1、tb_voucher:優(yōu)惠券基本信息(金額,規(guī)則等)
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
上面的type可以表示標識出是平價券還是特價券,如果是特價券我們也需要一些特定的信息,因此我們會專門拓展出一張表。
2、tb_seckill_voucher:優(yōu)惠券庫存、開始搶購時間、結(jié)束搶購時間(特價券需要此表)
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java

添加優(yōu)惠券

在VoucherController中提供一個接口,調(diào)用就可以實現(xiàn)添加秒殺優(yōu)惠券:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
雖然我們傳入的參數(shù)只有Voucher,但是它也同樣可以用來保存需要秒殺的券:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
真正的添加不是客戶來做的,要給后臺來做,我們可以使用postman:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
可以發(fā)現(xiàn)我們的數(shù)據(jù)庫中已經(jīng)存儲了這個秒殺券:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java

實現(xiàn)秒殺下單

點擊限時搶購,查看請求URL:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java

說明
請求方式 POST
請求路徑 voucher-order/seckill/{id}
請求參數(shù) id,優(yōu)惠券id
返回值 訂單id

下單的時候我們需要判斷2點:
1、秒殺是否開始或結(jié)束
2、庫存是否充足

業(yè)務流程:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
controller:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java

serviceimpl:

/**
 * <p>
 *  服務實現(xiàn)類
 * </p>
 *
 * @author 王雄俊
 * @since 2024-01-06
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    //注入秒殺優(yōu)惠券的service
    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //查詢優(yōu)惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //判斷秒殺是否開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒殺尚未開始");
        }
        //判斷秒殺是否結(jié)束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒殺已經(jīng)結(jié)束");
        }
        //判斷庫存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("庫存不足");
        }
        //扣減庫存,用mybatis-plus來寫
        boolean success = seckillVoucherService.update().
                setSql("stock = stock - 1").
                eq("voucher_id", voucherId).update();//where條件
        if (!success){
            return Result.fail("庫存不足");
        }
        System.out.println("啊啊啊啊啊");
        //創(chuàng)建訂單,需要訂單id、用戶id、代金券id
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        Long userId = UserHolder.getUser().getId();//用戶Id去ThreadLocal中取
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //返回訂單ID
        return Result.ok(orderId);
    }
}

Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
這邊實現(xiàn)了最基礎的訂單秒殺,但是它存在很多問題

庫存超賣問題

既然是秒殺,那每秒鐘很可能會有成千上萬的用戶進行訪問,那么這就對我們的并行化要求非常高,線程安全問題肯定是很重要的,上面的代碼肯定是會存在線程安全問題的,我們可以用jmeter來做測試,為了方便我們到時候觀察測試結(jié)果,我們?nèi)?shù)據(jù)庫手動把優(yōu)惠券數(shù)量調(diào)回100,接著在jmeter中用200個線程來進行搶購:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
這里設置的請求頭則表示200個線程全部都由這一個用戶來執(zhí)行:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
運行后可以看到有些請求成功,有些請求失?。?br>Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
預期是有100個線程失敗的,但是打開聚合報告可以發(fā)現(xiàn)失敗的線程數(shù)量不到一半:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
說明有些線程意外成功了,打開數(shù)據(jù)庫,發(fā)現(xiàn)票數(shù)為-9,說明發(fā)生了超賣:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
這會給商家?guī)頁p失。

庫存超賣問題分析

假設庫存容量為1(相當于一種臨界資源),高并發(fā)的時候可能出現(xiàn)的異常情況:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
也就是說,我們在某一時段會同時有多個線程查詢庫存的時候,得到的庫存量為1,這時候都會進行扣減操作,造成超賣。
針對這種線程安全問題,常見解決方法就是直接加鎖,可以分為悲觀鎖和樂觀鎖:
悲觀鎖:認為線程安全問題一定會發(fā)生,因此在操作數(shù)據(jù)之前先獲取鎖,確保線程串行執(zhí)行。(Synchronized、Lock等)

樂觀鎖:認為線程安全問題不一定會發(fā)生,因此不加鎖,只是在更新數(shù)據(jù)時去判斷有沒有其它線程對數(shù)據(jù)做了修改。(如果沒有修改,那就是安全的;如果已經(jīng)被其他線程修改說明發(fā)生了安全問題,此時可以重試或異常)

顯然樂觀鎖的性能會好很多,但是實現(xiàn)起來會更復雜,我們要處理好關(guān)鍵的一點,那就是更新數(shù)據(jù)的時候,該如何去判斷有沒有其它線程對數(shù)據(jù)做了修改。
樂觀鎖的實現(xiàn)方式有2種方法(其實思想相同):
1、版本號法
給數(shù)據(jù)增加一個字段version,初始值為1,每次我們要修改庫存量之前都需要先查詢庫存量與版本號,然后線程執(zhí)行SQL語句,執(zhí)行SQL語句必須要確定數(shù)據(jù)庫中的這條數(shù)據(jù)的版本號就是查詢出來的版本號,如果不相同說明有其他線程修改了數(shù)據(jù),導致當前數(shù)據(jù)的版本號與之前查詢的不一樣:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
2、CAS法
上面的方法加一個版本號其實是一種標識,但是我們不一定要借助version,實際上我們可以直接依靠庫存量來做標識,在對數(shù)據(jù)庫進行修改的時候,我們要首先判斷當前數(shù)據(jù)的庫存量與之前線程查詢出來的庫存量是否相同,不相同則說明發(fā)生線程安全問題,不能修改:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java

樂觀鎖解決超賣

我們選用CAS法來解決超賣,根據(jù)上述思想,我們只需要在SQL語句那增加一個判斷庫存量的條件:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
測試一下上面的代碼,先把數(shù)據(jù)庫做還原,把訂單數(shù)據(jù)刪光,并還原stock為100,然后測試jmeter,可以發(fā)現(xiàn)jmeter中顯示大量的失敗,數(shù)據(jù)庫中也顯示沒有超賣:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
超賣問題確實沒有出現(xiàn)了,但是這顯然是不合常理的,200個線程搶100張票,票居然只能賣出20張。這說明樂觀鎖有弊端。
我們對于樂觀鎖的分析,是拿stock=1的情況來說的,所以當線程查詢出來的stock與數(shù)據(jù)庫的stock不一致的時候,足以說明票已經(jīng)賣完了。
假設stock=100,當線程查詢出來的stock與數(shù)據(jù)庫的stock不一致的時候,并不能說明票賣完了,理論上庫存量大概率不為0,該線程還是應該要能夠?qū)崿F(xiàn)買票操作,但全都因為查詢的stock與數(shù)據(jù)庫不一致導致有大量線程買票失敗。

傳統(tǒng)樂觀鎖太謹慎了!我們應該要對其進行改進!

我們不再判斷查詢條件,而只需要查詢數(shù)據(jù)庫中的stock是否大于0:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
再次打開jmeter進行測試,異常率50%,解決了上述問題:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
但是這不代表樂觀鎖就是完美的,很顯然代碼邏輯中要操作數(shù)據(jù)庫,大量的線程就會給數(shù)據(jù)庫帶來壓力,僅僅使用樂觀鎖在更高并發(fā)的場景下還是不太夠的。

實現(xiàn)一人一單功能

我們在jmeter中的測試,200個線程全部都由一個用戶來執(zhí)行,因此打開訂單表,我們可以發(fā)現(xiàn)訂單全部被同一個用戶買了:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
商家做優(yōu)惠券就是為了吸引更多的用戶,一人多單可能會導致商家變相虧本。
其實思路是很簡單的,我們只需要判斷當前嘗試搶優(yōu)惠券的線程,其用戶id在訂單表中是否已經(jīng)存在了,如果存在則不允許下單:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
我們在庫存修改的代碼之前加上這一部分邏輯:

		//一人一單
        Long userId = UserHolder.getUser().getId();
        //查詢訂單
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //判斷是否存在
        if (count > 0){
            return Result.fail("您已購買過一次!");
        }

再次測試jmeter:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
數(shù)據(jù)庫顯示這個用戶買了10張優(yōu)惠券,一人多單的問題有所緩解,但依舊存在:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
這是因為上面的那一串邏輯還是存在了并發(fā)安全問題,在某一時刻還是會有很多的線程(同一個用戶)進入了這部分邏輯,判斷了count為0,因此進行了刪減庫存的操作。
這里我們肯定也要加鎖,由于這一串邏輯并沒有涉及到修改數(shù)據(jù)庫的操作,所以我們只能加悲觀鎖。

	@Override
    public Result seckillVoucher(Long voucherId) {
        //查詢優(yōu)惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //判斷秒殺是否開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒殺尚未開始");
        }
        //判斷秒殺是否結(jié)束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒殺已經(jīng)結(jié)束");
        }
        //判斷庫存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("庫存不足");
        }

        //返回訂單ID
        return createVoucherOrder(voucherId);
    }

    @Transactional //事務回滾放到這個函數(shù)
    public Result createVoucherOrder(Long voucherId) {
        //一人一單
        Long userId = UserHolder.getUser().getId();
        /**
         * userId值一樣的,我們用同一把鎖,但是每個請求一來,我們的id對象都是全新的
         * Long類型會存在這個問題,所以我們要用toString方法
         * 但是toString方法其實是對long類型new了一個字符串,所以每調(diào)用一個toString都是一個全新對象
         * 所以要加上intern()方法,從常量池中返回字符串的規(guī)范表示
         */
        synchronized (userId.toString().intern()) {
            //查詢訂單
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            //判斷是否存在
            if (count > 0) {
                return Result.fail("您已購買過一次!");
            }
            //扣減庫存
            boolean success = seckillVoucherService.update().
                    setSql("stock = stock - 1").
                    eq("voucher_id", voucherId).
                    gt("stock", 0).
                    update();
            if (!success) {
                return Result.fail("庫存不足");
            }
            //創(chuàng)建訂單,需要訂單id、用戶id、代金券id
            VoucherOrder voucherOrder = new VoucherOrder();
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            voucherOrder.setUserId(userId);
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
            //返回訂單ID
            return Result.ok(orderId);
        }
    }

需要注意一個細節(jié),上面代碼還是會發(fā)生并發(fā)安全問題:

我們這邊的整個函數(shù)已經(jīng)是被Spring托管了,所以事務的提交會在函數(shù)執(zhí)行完畢之后,也就是說我們會先釋放鎖,再提交事務,當我們事務還沒有提交完成,修改數(shù)據(jù)還沒寫入數(shù)據(jù)庫,卻又有其他線程進來了,再次發(fā)生線程并發(fā)問題。

所以,鎖的范圍太小了,我們應該要把整個函數(shù)都鎖起來:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
但,依舊有問題!直接調(diào)用createVoucherOrder方法是不行的,因為它相當于調(diào)用了this.createVoucherOrder,然而當前類并不是代理對象,這會導致Sping代理失效!
所以我們要先獲得當前對象的代理對象,然后再去調(diào)用這個函數(shù):

IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);

需要引入依賴:

<dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
</dependency>

并且在啟動類中需要暴露代理對象:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
運行項目,打開jmeter進行測試:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
完美解決!
注意我沒有在createVoucherOrder這個函數(shù)上面直接加鎖,不然所有進行操作的線程都串行執(zhí)行實在太影響效率了!

集群下的線程并發(fā)安全問題

現(xiàn)在已經(jīng)通過加鎖解決一人一單問題安全,但是這只能解決單機情況的,集群模式依舊不行,在這里試著模擬一下集群的方式來進行測試。
1、將服務啟動2份,端口分別為8081與8082:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
重啟形成2個機子的集群:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
2、修改nginx的conf目錄下的nginx.conf文件,配置反向代理、負載均衡:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
最后重新加載一下Nginx并重啟:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
最后訪問網(wǎng)址,并連續(xù)刷新2次:

http://localhost:8080/api/voucher/list/1

查看后臺可以發(fā)現(xiàn)兩個啟動服務都可以接受到信息,因為api(8080)包括了8081與8082,訪問是以輪轉(zhuǎn)的方式進行的:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
這樣就實現(xiàn)了負載均衡。

測試大家只需要在鎖那里打個斷點,并且在postman里面分別搶券(都用同一個用戶)來進行優(yōu)惠券搶購,可以發(fā)現(xiàn)只用1個用戶信息,數(shù)據(jù)庫中卻少了2張券,說明又一次發(fā)生了并發(fā)問題。

從頭分析一下:
1、對于一個服務中的2個線程,可能發(fā)生下面的并發(fā)問題:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
2、我們解決方法是加鎖:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
之所以這樣能實現(xiàn),是因為我們鎖住的對象是userId.toString().intern(),也就是從這臺Tomcat常量池中取出userId.toString(),同一個userId之間肯定是相同的,因此可以鎖住,防止并發(fā)。

3、但如果我們部署另外一臺Tomcat,這是鎖的鎖監(jiān)視器,其監(jiān)視的內(nèi)容和之前鎖中的監(jiān)視器內(nèi)容是不一樣的,那么新Tomcat的線程獲取鎖就會成功(獲取的userId.toString()是不一樣的,不理解的可以去看toString方法的源碼),并成功的操作數(shù)據(jù)庫,因此才會造成線程并行問題。
如下圖,線程1、3發(fā)生了線程安全問題:
Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題),Redis:原理速成+項目實戰(zhàn),redis,數(shù)據(jù)庫,緩存,spring boot,java
因此我們只能保證單個JVM下的線程安全,卻無法保證集群中多個JVM的線程安全,我們需要在集群中加鎖,也就是分布式鎖,將在后續(xù)講解。文章來源地址http://www.zghlxwxcb.cn/news/detail-819676.html

到了這里,關(guān)于Redis:原理速成+項目實戰(zhàn)——Redis實戰(zhàn)7(優(yōu)惠券秒殺+細節(jié)解決超賣、一人一單問題)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務器費用

相關(guān)文章

  • 小程序中如何核銷訂單和優(yōu)惠券

    小程序中如何核銷訂單和優(yōu)惠券

    小程序已成為許多商家線上線下開展業(yè)務的重要渠道??蛻粼谛〕绦蛑邢聠?領(lǐng)券后,可能需要商家現(xiàn)場掃碼核銷,例如超市購物、賣票、游樂園等線下場景。下面就介紹小程序中如何核銷訂單和優(yōu)惠券。 一、訂單核銷 訂單核銷是指商家在小程序中確認顧客已經(jīng)支付的訂單并

    2024年03月21日
    瀏覽(39)
  • 微信小程序?qū)崿F(xiàn)一些優(yōu)惠券/卡券

    微信小程序?qū)崿F(xiàn)一些優(yōu)惠券/卡券

    ?? 前幾周有小伙伴問我如何用css實現(xiàn)一些優(yōu)惠券/卡券,今天就來分享一波吧!速速來Get吧~ ??文末分享源代碼。記得點贊+關(guān)注+收藏! 2.1 實現(xiàn)內(nèi)凹圓角 假設我們要實現(xiàn)這樣的一個效果,兩側(cè)透明內(nèi)圓角+外側(cè)投影,有幾種實現(xiàn)方式呢? 2.1.1 方法一:半圓偽元素(投影不準確

    2024年02月09日
    瀏覽(21)
  • 機器學習:基于邏輯回歸對優(yōu)惠券使用情況預測分析

    機器學習:基于邏輯回歸對優(yōu)惠券使用情況預測分析

    作者:i阿極 作者簡介:Python領(lǐng)域新星作者、多項比賽獲獎者:博主個人首頁 ??????如果覺得文章不錯或能幫助到你學習,可以點贊??收藏??評論??+關(guān)注哦!?????? ??????如果有小伙伴需要數(shù)據(jù)集和學習交流,文章下方有交流學習區(qū)!一起學習進步!?? 訂閱專欄案

    2024年02月02日
    瀏覽(25)
  • 【java爬蟲】將優(yōu)惠券數(shù)據(jù)存入數(shù)據(jù)庫排序查詢

    【java爬蟲】將優(yōu)惠券數(shù)據(jù)存入數(shù)據(jù)庫排序查詢

    本文是在之前兩篇文章的基礎上進行寫作的 (1條消息) 【java爬蟲】使用selenium爬取優(yōu)惠券_haohulala的博客-CSDN博客 (1條消息) 【java爬蟲】使用selenium獲取某寶聯(lián)盟淘口令_haohulala的博客-CSDN博客? 前兩篇文章介紹了如何獲取優(yōu)惠券的基礎信息,本文將獲取到的基本信息存到數(shù)據(jù)庫中

    2024年02月16日
    瀏覽(26)
  • 兩天擼一個優(yōu)惠券小程序,記錄下開發(fā)的小小經(jīng)驗

    兩天擼一個優(yōu)惠券小程序,記錄下開發(fā)的小小經(jīng)驗

    下載微信開發(fā)者工具???? 新建項目文件夾project,比如 D:workProjectproject 在project下創(chuàng)建src目錄放微信小程序的源碼,.gitignore文件是用來git上傳gitee上忽略一些文件用的,另外三個js文件時用來混淆小程序源碼的腳本,將腳本和小程序源碼分開是為了腳本更好處理混淆過程。

    2023年04月08日
    瀏覽(22)
  • 【實踐篇】教你玩轉(zhuǎn)JWT認證---從一個優(yōu)惠券聊起

    【實踐篇】教你玩轉(zhuǎn)JWT認證---從一個優(yōu)惠券聊起

    最近面試過程中,無意中跟候選人聊到了JWT相關(guān)的東西,也就聯(lián)想到我自己關(guān)于JWT落地過的那些項目。 關(guān)于JWT,可以說是分布式系統(tǒng)下的一個利器,我在我的很多項目實踐中,認證系統(tǒng)的第一選擇都是JWT。它的優(yōu)勢會讓你欲罷不能,就像你領(lǐng)優(yōu)惠券一樣。 大家回憶一下一個

    2024年02月05日
    瀏覽(24)
  • 智慧影院--java開源電影票優(yōu)惠券制作系統(tǒng)快速開發(fā)

    智慧影院--java開源電影票優(yōu)惠券制作系統(tǒng)快速開發(fā)

    搭建一個智慧影院可以通過使用Java開源電影票優(yōu)惠券制作系統(tǒng)來快速開發(fā)。這個系統(tǒng)可以幫助影院管理電影票的銷售和優(yōu)惠活動,提供便捷的購票方式和優(yōu)惠券的生成與使用功能。 首先,我們需要建立一個數(shù)據(jù)庫來存儲電影、影廳、放映計劃、訂單等信息。在數(shù)據(jù)庫中,我

    2024年02月13日
    瀏覽(17)
  • 淘寶APP商品詳情接口(商品信息,價格銷量,優(yōu)惠券信息,詳情圖等)

    淘寶APP商品詳情接口(商品信息,價格銷量,優(yōu)惠券信息,詳情圖等)

    淘寶APP商品詳情接口(商品信息接口,價格銷量接口,優(yōu)惠券信息接口,詳情圖接口等)代碼對接如下: 公共參數(shù) 名稱 類型 必須 描述 key String 是 調(diào)用key(必須以GET方式拼接在URL中),點擊獲取請key和secret secret String 是 調(diào)用密鑰 api_name String 是 API接口名稱(包括在請求地址

    2024年02月12日
    瀏覽(28)
  • 業(yè)務安全情報第16期 | 大促8成優(yōu)惠券竟被“羊毛黨”搶走!?

    近期,某電商小程序舉辦美食節(jié)營銷活動,提供高額折扣券,并允許用戶進行秒殺。然而,羊毛黨團伙利用作弊手段,搶購囤券,然后倒賣變現(xiàn),嚴重損害了商家的利益。 根據(jù)頂象防御云編號為BSI-2023-rutq業(yè)務安全情報發(fā)現(xiàn),某電商平臺為吸引人氣和促進銷售推,推出高額折

    2024年02月07日
    瀏覽(31)
  • 斐訊1200M四天線雙頻路由器PSG1208只要88元 快來領(lǐng)取優(yōu)惠券

    斐訊斐賽克斯專賣店針對1200M斐訊PSG1208無線路由器啟動淘搶購活動,現(xiàn)價88元(原價98元),其擁有1200M傳輸速率、2.4G/5.8G雙頻并發(fā)、WISP中繼等特性。 立刻下單 。 斐訊PSG1208使用802.11ac協(xié)議,2.4G/5.8G雙頻并發(fā),數(shù)據(jù)傳輸速率達1167Mbps;配以聯(lián)發(fā)科SOC MT7620A處理器、4天線4*4MIMO架構(gòu)

    2024年02月08日
    瀏覽(95)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包