首頁 > 軟體

Redis優惠券秒殺解決方案

2022-12-07 14:00:20

1 實現優惠券秒殺功能

下單時需要判斷兩點:1.秒殺是否開始或者結束2.庫存是否充足

所以,我們的業務邏輯如下

1. 通過優惠券id獲取優惠券資訊

2.判斷秒殺是否開始,如果未返回錯誤資訊

3.判斷秒殺是否結束,如果已經結束返回錯誤資訊

4.如果在秒殺時間內,判斷庫存是否充足

5.如果充足,扣減庫存

6.建立訂單資訊,並儲存到優惠券訂單表中

6.1 儲存訂單id

6.2儲存使用者id

6.3儲存優惠券id

7.返回訂單id

程式碼實現:(Service層實現類)

package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
 * <p>
 *  服務實現類
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService iSeckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //4.判斷庫存是否充足
        if (voucher.getStock() < 1) {
            Result.fail("庫存不充足!");
        }
        //5.扣減庫存
        boolean success = iSeckillVoucherService.update()
                .setSql("stock = stock-1").eq("voucher_id",voucherId)
                .update();
        if (!success){
            Result.fail("庫存不充足!");
        }
        //6. 建立訂單
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1新增訂單id
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //6.2新增使用者id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //6.3新增優惠券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回訂單id
        return Result.ok(orderId);
    }
}

2 超賣問題(重點)

我們先嚐試在高並行的情況下執行上述程式碼。(使用jmx工具)

下圖是建立了兩百個執行緒,在一瞬間發出優惠券請求

但是我們看聚合報告,發現異常值只有45.5%,按道理來說應該是50%(因為庫存有100個,這裡發出了200個請求)

一看庫存數,好傢伙,是-9

訂單也是新增了109個,這顯然發生了超賣的問題。

那麼,為什麼會發生這種問題呢?

看圖說話:

按照我們正常的流程來走,就是執行緒1線查詢完庫存,然後扣減庫存,這個時候執行緒2再來查詢庫存,扣減庫存,這樣是沒問題的。

超賣的問題就出在,在訂單1查詢庫存後,發現是1,但還沒去扣減的時候,執行緒2也來查詢庫存,發現也是1,也進行了扣減(高並行的場景下)

這就導致了超賣的問題。

對於這種高並行的問題,最常見的解決方法就是:上鎖~

但鎖又包括悲觀鎖和樂觀鎖。

悲觀鎖簡單的講就是:覺得執行緒一定會發生,然後在操作之前每個人先拿鎖,你執行完後,在輪到下一個來執行(序列執行)

樂觀鎖 :就是樂觀(認為執行緒安全一定不會發生),只要在每次對資料修改之前,判斷其他執行緒是否對資料進行的修改來保證執行緒安全。

悲觀鎖較為簡單,這裡實現樂觀鎖。

樂觀鎖的關鍵是判斷之前查詢得到的資料是否有被修改過,常見的方式有兩種

溫馨提示:左邊表格的資料都是執行緒1執行後的資料哦~

1.版本號法

就是在查詢庫存的步驟上加上一個版本號,每次修改完資料後給版本號+1並在後面加上where條件判斷版本號是否和修改前的一致

這樣就可以做到執行緒安全啦~

2.CAS法

這個就是不用版本號了,直接在修改資料庫後加上where條件判斷庫存是否是修改前的庫存

解決超賣問題程式碼實現:

說到底就是在我們扣減庫存的時候加上一個where條件判斷庫存是否大於0

//5.1扣減庫存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id" , voucherId).gt("stock" ,0)
.update();
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
 * <p>
 *  服務實現類
 * </p>
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService iSeckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //4.判斷庫存是否充足
        if (voucher.getStock() < 1) {
            Result.fail("庫存不充足!");
        }
        //5.扣減庫存
        boolean success = iSeckillVoucherService.update()
                .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
                .update();
        if (!success){
            Result.fail("庫存不充足!");
        }
        //6. 建立訂單
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1新增訂單id
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //6.2新增使用者id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //6.3新增優惠券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回訂單id
        return Result.ok(orderId);
    }
}

超賣問題解決

到此這篇關於Redis優惠券秒殺解決方案的文章就介紹到這了,更多相關Redis優惠券秒殺內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com