結合redis實現秒殺業務


背景:

某電商網站實現秒殺功能,用戶在某個時間段內能夠搶購到特價商品,且某一商品最多只能被同一用戶搶購一次。

基本思路:

  1. 秒殺商品由商家后台添加,秒殺商品數據保存在tb_seckilll_goods表中,關鍵字段包括:
    id,status(審核狀態),start_time(開始時間),end_time(結束時間),stock_count(庫存量);
  2. 寫一個定時器,定時從秒殺商品表中掃描數據,將符合條件的商品加載到緩存中;條件:審核狀態="1",start_time < 當前時間 < end_time,庫存量大於0;
  3. 前端展示,此處略
  4. 點擊搶購,拿着秒殺商品的id去緩存中查詢,如果緩存中商品不存在或者為空,提示“已售罄”,否則生成訂單,保存到緩存中,訂單表tb_seckill_order
  5. 庫存-1,判斷減完之后緩存中商品的庫存是否大於0,大於0則更新緩存,否則刪除該秒殺商品的緩存,並更新到數據庫

技術選型:緩存redis,定時器:spring整合quartz

如下完成了一個基本的秒殺下單的業務:

掃描秒殺商品加載到redis:

@Scheduled(cron = "0 */1 * * * ?")//cron表達式:每分鍾執行一次,周期可任意定義
public void importToRedis(){
    //1.查詢合法秒殺商品數據
    TbSeckillGoodsExample example = new TbSeckillGoodsExample();
    Date date = new Date();
    example.createCriteria().andStatusEqualTo("1").andStockCountGreaterThan(0)
            .andStartTimeLessThan(date).andEndTimeGreaterThan(date);
    List<TbSeckillGoods> tbSeckillGoods = seckillGoodsMapper.selectByExample(example);
    for (TbSeckillGoods seckillGood : tbSeckillGoods) {//將秒殺商品依次存入redis
        //注意如果redis中已經有的商品,則不更新,只添加之前未加入過的秒殺商品
        if(redisTemplate.boundHashOps("TbSeckillGoods").get(seckillGood.getId()) == null){
            redisTemplate.boundHashOps("TbSeckillGoods").put(seckillGood.getId(), seckillGood);
        }
    }
}

對所有的秒殺商品都使用同一個key:“TbSeckillGoods”,值的存儲類型為hash

下單的service代碼:

public Result saveOrder(Long id, String userId) {
    //根據商品id從redis中查出商品
    TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);
    //如果緩存中秒殺商品不存在或者庫存為空,則提示已售罄
    if(seckillGood == null || seckillGood.getStockCount() <= 0){
        return new Result(false, "已售罄");
    }
    //如果時間已截止,提示秒殺時間已結束
    if(seckillGood.getEndTime().getTime() < System.currentTimeMillis()){
        return new Result(false, "活動已結束");
    }
    //生成訂單保存到緩存中
    TbSeckillOrder seckillOrder = new TbSeckillOrder();
    seckillOrder.setUserId(userId);
    seckillOrder.setSeckillId(idWorker.nextId());
    seckillOrder.setSellerId(seckillGood.getSellerId());
    seckillOrder.setMoney(seckillGood.getCostPrice());
    seckillOrder.setStatus("0");//未支付
    seckillOrder.setCreateTime(new Date());
    redisTemplate.boundHashOps(TbSeckillOrder.class.getSimpleName()).put(userId, seckillOrder);
    //秒殺商品庫存量減1
    seckillGood.setStockCount(seckillGood.getStockCount() - 1);
    //判斷減完之后redis中商品的庫存是否大於0,大於0則更新緩存,否則刪除該秒殺商品的緩存,並更新到數據庫
    if(seckillGood.getStockCount() > 0){
        redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).put(seckillGood.getGoodsId(), seckillGood);
    }else {
        redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).delete(seckillGood.getGoodsId());
        seckillGoodsMapper.updateByPrimaryKey(seckillGood);
    }
    return new Result(true, "恭喜您搶購到商品,請盡快支付");
}

以上是關鍵代碼,其他業務代碼可不關注,完整代碼可在我的github中查看

分析上述代碼:

上述代碼在多線程環境下存在三個問題:

1.超賣:

if(seckillGood == null || seckillGood.getStockCount() <= 0){
    return new Result(false, "已售罄");
}

業務邏輯是如果seckillGood不為null,且庫存>0,即可進行下單,但是在實際環境中,可能會有很多的用戶同時獲取到redis中的商品信息,每個用戶讀取到的庫存量一樣且均大於0,假如庫存只有2,但是有三個用戶都符合下單條件,就出現了超賣情況

2.沒有對用戶多次搶購做限制

3.下單和生成訂單串行,影響並發效率。完全可以在用戶搶購之后立即能夠下單成功,后續的訂單處理可以利用多線程來異步操作

解決方案:

1.對於超賣問題,很容易想到是就是對下單操作加鎖,一次只能有一個用戶進行下單並減庫存。這種方法可以避免超賣問題,但是卻會導致效率下降。

redis中有一種存儲結構list,它的元素在彈出時能夠保證一次只有一個線程進行操作,並且效率比較高。例如,我們在錄入秒殺商品的同時,對每一種商品都創建一個list,該商品的庫存有多少,list中的元素就有多少個,每次下單就從list中彈出一個元素,防止超賣。

如圖:以“SECKILLGOODS_ID_PREFIX_秒殺商品ID”的格式字符串作為list的key,商品庫存有n,則該list就有n個元素,元素的壓入在錄入商品時完成,每下單一次,就彈出一個元素。

2.對於同一用戶多次搶購的問題,我們同樣可以使用redis來記錄每種商品已搶購成功的用戶id,我們使用set來記錄用戶id,防止用戶id重復

如圖:以“USER_ID_PREFIX_秒殺商品ID”的格式字符串作為set的key,一旦有一個用戶搶購了該商品,則在先判斷Set集合中是否存在用戶id,不存在則添加

3.多線程處理訂單,在redis中創建一個隊列,每當一個用戶成功搶購一個商品,就往隊列中壓入一個下單數據,包含商品id和用戶id即可。線程從隊列中彈出一個包含下單數據的元素,進行訂單的生成

如圖:OrederRecorder作為key,集合中記錄了搶購成功的商品id和用戶id,等待多線程去從集合中彈出元素進行處理

整個秒殺業務的大致流程如下:

完整代碼可參考https://github.com/ithushuai/seckill-demo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM