秒殺模塊的實現

需求分析

所謂“秒殺”,就是網絡賣家發佈一些超低價格的商品,所有買家在同一時間網上搶購的一種銷售方式。
秒殺商品通常有兩種限制:庫存限制、時間限制。

(1)商品詳細頁顯示秒殺商品信息,點擊立即搶購實現秒殺下單,下單時扣減庫存。當庫存爲0或不在活動期範圍內時無法秒殺。
(2)秒殺下單成功,直接跳轉到支付頁面(支付寶掃碼),支付成功,跳轉到成功頁,填寫收貨地址、電話、收件人等信息,完成訂單。
(3)當用戶秒殺下單5分鐘內未支付,取消預訂單,調用支付寶支付的關閉訂單接口,恢復庫存。

秒殺技術實現核心思想是運用緩存減少數據庫瞬間的訪問壓力!讀取商品詳細信息時運用緩存,當用戶點擊搶購時減少緩存中的庫存數量,當庫存數爲0時或活動期結束時,同步到數據庫。產生的秒殺預訂單也不會立刻寫到數據庫中,而是先寫到緩存,當用戶付款成功後再寫入數據庫。

實現

秒殺商品的查詢

  • 判斷redis是否爲空,如果不爲空,就從redis中查詢
  • 如果爲空就從數據庫中查詢並將查詢結果存儲到redis中
public List<TbSeckillGoods> findList() {
    List<TbSeckillGoods> secKillGoods = redisTemplate.boundHashOps("secKillGoods").values();
    if (secKillGoods==null || secKillGoods.size()==0){
        TbSeckillGoodsExample example = new TbSeckillGoodsExample();
        TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
        criteria.andStatusEqualTo("1");
        //當前時間大於開始時間,小於結束時間
        criteria.andStartTimeLessThan(new Date());
        criteria.andEndTimeGreaterThan(new Date());
        //庫存限制
        criteria.andStockCountGreaterThan(0);

        secKillGoods = seckillMapper.selectByExample(example);

        //存入redis中
        for (TbSeckillGoods secKillGood : secKillGoods) {
            redisTemplate.boundHashOps("secKillGoods").put(secKillGood.getId(),secKillGood);
        }
    }
    return secKillGoods;
}

秒殺訂單的創建

  • 訂單在redis中存儲的大鍵名任意,每個小鍵爲用戶id,值爲訂單的list集合
  • 同一個商品一個用戶只能秒殺一件
  • 判斷秒殺商品已經從秒殺完從redis中刪除和秒殺商品數量小於等於0的情況(在高併發情況下,庫存數量是有可能爲負的)
  • 扣減庫存並更新到redis中
  • 當庫存扣減爲0的時候,從緩存中移除 該秒殺商品, 同時將數據同步到 mysql中
  • 將秒殺訂單保存在redis中
public Long submitOrder(Long seckillId, String userId) {
    //從redis中查詢出用戶的秒殺訂單
    List<TbSeckillOrder> OrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(userId);
    if (OrderList==null){
        OrderList = new ArrayList<>();
    }
    else {
        for (TbSeckillOrder secKillorder : OrderList) {
            if (secKillorder.getSeckillId().longValue() == seckillId.longValue()){//一個商品一個用戶只能秒殺一次
                throw new RuntimeException("你已經成功秒殺到了該商品!");
            }
        }
    }
    //從redis中獲取當前秒殺商品的數據,判斷該商品是否還能購買
    TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps("secKillGoods").get(seckillId);
    //判斷秒殺商品已經從秒殺完從redis中刪除和秒殺商品數量小於等於0的情況(在高併發情況下,庫存數量是有可能爲負的)
    if (seckillGood==null || seckillGood.getStockCount()<=0){
        throw new RuntimeException("很遺憾,該商品已經搶完,謝謝參與");
    }
    //扣減庫存
    seckillGood.setStockCount(seckillGood.getStockCount()-1);
    redisTemplate.boundHashOps("secKillGoods").put(seckillId,seckillGood);
    // 當庫存扣減爲0的時候,從緩存中移除 該秒殺商品, 同時將數據同步到 mysql中
    if (seckillGood.getStockCount()==0){
        redisTemplate.boundHashOps("secKillGoods").delete(seckillId);
        seckillGoodsMapper.updateByPrimaryKey(seckillGood);
    }
    //將秒殺訂單保存在redis中
    TbSeckillOrder seckillOrder = new TbSeckillOrder();
    long orderId = idWorker.nextId();

    seckillOrder.setId(orderId);
    seckillOrder.setSeckillId(seckillId);
    seckillOrder.setMoney(seckillGood.getCostPrice());
    seckillOrder.setUserId(userId);
    seckillOrder.setSellerId(seckillGood.getSellerId());
    seckillOrder.setCreateTime(new Date());
    // 未付款
    seckillOrder.setStatus("0");
    //將訂單加入用戶的秒殺訂單表
    OrderList.add(seckillOrder);
    redisTemplate.boundHashOps("secKillOrders").put(userId,OrderList);
    //返回訂單id
    return orderId;
}

高併發壓力測試

測試之前秒殺商品庫存數量是10
在這裏插入圖片描述
在spring-security.xml文件中對測試url進行放行
在這裏插入圖片描述
使用jmeter進行測試,併發測試500
在這裏插入圖片描述
併發測試之後,庫存清爲0
在這裏插入圖片描述
在redis中查看創建訂單的數量爲17,發生了超賣現象
在這裏插入圖片描述redis分佈式鎖解決超賣
我們通過單節點Redis實現一個分佈式鎖。
利用redis在同一時刻操作一個鍵的值只能有一個進程的特性,如果能設值成功就獲取到鎖;解鎖,就是刪除指定的鍵;
爲防止死鎖可以設置鎖超時時間,如果鎖超時就釋放鎖。

秒殺訂單支付及保存

支付只需要我們從redis中將訂單號和支付金額查詢出,然後調用支付方法即可
頁面監聽訂單是否支付
如果已支付,將訂單保存到mysql數據庫以及更新redis數據庫的信息
將方法寫在業務層還有個好處是,業務層切入了事務,如果沒有執行成功,可以進行回滾
在支付控制層的支付成功判斷裏調用更新方法
在這裏插入圖片描述

public void updateSecKillOrder(String userId, String out_trade_no) {
    
    TbSeckillOrder saveOrder = null;
    List<TbSeckillOrder> secOrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(userId);
    for (TbSeckillOrder seckillOrder : secOrderList) {
        if (out_trade_no.equals(seckillOrder.getId().longValue()+"")){
            seckillOrder.setPayTime(new Date());
            seckillOrder.setStatus("1");
            //同步到mysql
            saveOrder = seckillOrder;
        }
    }
    redisTemplate.boundHashOps("secKillOrders").put(userId,secOrderList);
  
    seckillOrderMapper.insert(saveOrder);
}

秒殺付款超時

在這裏插入圖片描述

public void backAndRemoveOrder(String name, String out_trade_no) {
    //還原庫存
    //先查詢出超時未付款訂單
    TbSeckillOrder order = findSecKillOrderByUserIdAndOrderId(name, Long.valueOf(out_trade_no));
    TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps("secKillGoods").get(order.getSeckillId());
    if (seckillGood==null){
        TbSeckillGoods dbGoods = seckillGoodsMapper.selectByPrimaryKey(order.getSeckillId());
        // dbGoods可不可用就不一定了:可能在用戶3分鐘未付款期間,該商品到了秒殺截止時間,
        // 此時就不用再添加到 redis緩存了, 但是 數據庫 的庫存 應該 +1

        //過期
        if (dbGoods.getEndTime().getTime()<new Date().getTime()){
            dbGoods.setStockCount(dbGoods.getStockCount()+1);
            seckillGoodsMapper.updateByPrimaryKey(dbGoods);
        }else {
            dbGoods.setStockCount(dbGoods.getStockCount()+1);
            seckillGoodsMapper.updateByPrimaryKey(dbGoods);
            //更新redis中的數據
            redisTemplate.boundHashOps("secKillGoods").put(dbGoods.getId(),dbGoods);
        }
    }else {
        seckillGood.setStockCount(seckillGood.getStockCount()+1);
        //更新redis中的數據
        redisTemplate.boundHashOps("secKillGoods").put(seckillGood.getId(),seckillGood);
    }
    //移除未付款訂單
    List<TbSeckillOrder> newList = new ArrayList<>();
    List<TbSeckillOrder> secOrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(name);
    for (TbSeckillOrder seckillOrder : secOrderList) {
        if (out_trade_no.equals(seckillOrder.getId().longValue()+"")){

        }else{
            newList.add(seckillOrder);
        }
    }
    redisTemplate.boundHashOps("secKillOrders").put(name,newList);
}

注意
list刪除複雜對象使用remove沒有效果,基本類型的數據可以刪除

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章