1. 程式人生 > 其它 >優惠券秒殺-一人一單

優惠券秒殺-一人一單

需求:修改秒殺業務,要求同一個優惠券,一個使用者只能下一單

現在的問題在於:

優惠卷是為了引流,但是目前的情況是,一個人可以無限制的搶這個優惠卷,所以我們應當增加一層邏輯,讓一個使用者只能下一個單,而不是讓一個使用者下多個單

具體操作邏輯如下:比如時間是否充足,如果時間充足,則進一步判斷庫存是否足夠,然後再根據優惠卷id和使用者id查詢是否已經下過這個訂單,如果下過這個訂單,則不再下單,否則進行下單

@Override
public Result seckillVoucher(Long voucherId) {
    // 1.查詢優惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    
// 2.判斷秒殺是否開始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未開始 return Result.fail("秒殺尚未開始!"); } // 3.判斷秒殺是否已經結束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // 尚未開始 return Result.fail("秒殺已經結束!"); } // 4.判斷庫存是否充足 if (voucher.getStock() < 1) {
// 庫存不足 return Result.fail("庫存不足!"); } // 5.一人一單邏輯 // 5.1.使用者id Long userId = UserHolder.getUser().getId(); int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); // 5.2.判斷是否存在 if (count > 0) { // 使用者已經購買過了 return Result.fail("使用者已經購買過一次!"); }
//6,扣減庫存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).update(); if (!success) { //扣減庫存 return Result.fail("庫存不足!"); } //7.建立訂單 VoucherOrder voucherOrder = new VoucherOrder(); // 7.1.訂單id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); return Result.ok(orderId); }

這裡有什麼問題呢?

我們必須要保證 判斷庫存是否充足,判斷訂單是否存在,扣減庫存,建立訂單 這一系列操作是原子性的。我在做這件事的時候,別人不能加進來。

怎麼保證呢,還是加鎖。

存在問題:現在的問題還是和之前一樣,併發過來,查詢資料庫,都不存在訂單,所以我們還是需要加鎖,但是樂觀鎖比較適合更新資料,而現在是插入資料,所以我們需要使用悲觀鎖操作

注意:在這裡提到了非常多的問題,我們需要慢慢的來思考,首先我們的初始方案是封裝了一個createVoucherOrder方法,同時為了確保他執行緒安全,在方法上添加了一把synchronized 鎖

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {

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

        // 6.扣減庫存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣減失敗
            return Result.fail("庫存不足!");
        }

        // 7.建立訂單
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.訂單id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.使用者id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回訂單id
        return Result.ok(orderId);
}

,但是這樣新增鎖,鎖的粒度太粗了,在使用鎖過程中,控制鎖粒度 是一個非常重要的事情,因為如果鎖的粒度太大,會導致每個執行緒進來都會鎖住,所以我們需要去控制鎖的粒度。 

我們的目標是相同的使用者不可以再去下單,那實際上可以用使用者id做為鎖,相同使用者要競爭鎖,不同使用者可以拿到不同的鎖。

Long userId = UserHolder.getUser().getId();

當我們使用了userId.toString()時,我們建立的鎖物件是通一個嗎? 點進去toString()發現並不是同一個,最終是new了一物件。 這時候需要用到我們的intern()方法

intern() 這個方法是從常量池中拿到資料,如果我們直接使用userId.toString() 他拿到的物件實際上是不同的物件,new出來的物件,我們使用鎖必須保證鎖必須是同一把,所以我們需要使用intern()方法.

@Transactional
public  Result createVoucherOrder(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    synchronized(userId.toString().intern()){
         // 5.1.查詢訂單
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判斷是否存在
        if (count > 0) {
            // 使用者已經購買過了
            return Result.fail("使用者已經購買過一次!");
        }

        // 6.扣減庫存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣減失敗
            return Result.fail("庫存不足!");
        }

        // 7.建立訂單
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.訂單id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.使用者id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回訂單id
        return Result.ok(orderId);
    }
}

現在還有什麼問題? 

我們鎖在執行完業務邏輯後,釋放鎖了鎖,但是spring的事務需要在方法執行完成才會去提交程式碼,那麼這中間仍然可能會出問題。

問題的原因在於當前方法被spring的事務控制,如果你在方法內部加鎖,可能會導致當前方法事務還沒有提交,但是鎖已經釋放也會導致問題,所以我們選擇將當前方法整體包裹起來,確保事務不會出現問題:如下:

在seckillVoucher 方法中,新增以下邏輯,這樣就能保證事務的特性,同時也控制了鎖的粒度。

 Long id = UserHolder.getUser().getId();
        synchronized (id.toString().intern()){
          return   createVoucherOrder(voucherId);
        }

這樣看似解決了我們的問題,實際上spring控制的事務卻失效了。 spring事務和aop本質上是依據spring的代理物件完成控制的,我們在本方法使用的是省卻this物件,沒有使用代理物件,所以事務不能生效。

我們要怎麼做呢, 使用代理物件去呼叫,怎麼獲取代理物件?

  Long id = UserHolder.getUser().getId();
        synchronized (id.toString().intern()){
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return   proxy.createVoucherOrder(voucherId);
        }

獲取AopContext的當前代理物件,那麼這個代理物件是Object型別,當前是在 IVoucherOrderService 的實現類中,那麼我們當前的代理物件就是  IVoucherOrderService 。 使用這種獲取代理物件的時候還需要aspectj的依賴和

啟動類上的註解去暴露這個代理物件,不暴露我們是獲取不到的。

      

至此單體架構的一人一單功能已經實現。

隨著業務的擴充套件,我們使用了叢集部署,多臺tomcat 同時進行,這個方法還能生效嗎?