電商高并發(fā)秒殺5 流量削峰技術(shù)

前言

對應(yīng)的交易優(yōu)化技術(shù)使用了緩存校驗+異步扣減庫存的方式湿颅,使得秒殺下單的方式有了明顯的提升讯赏。
即便查詢優(yōu)化,交易優(yōu)化技術(shù)用到極致后族檬,只要外部的流量超過了系統(tǒng)可承載的范圍就有拖垮系統(tǒng)的風險茬腿。本章通過秒殺令牌呼奢,秒殺大閘,隊列泄洪等流量削峰技術(shù)解決全站的流量高性能運行效率滓彰。

項目缺陷:
  • 秒殺下單接口會被腳本不停的刷新控妻;
  • 秒殺驗證邏輯和秒殺下單接口強關(guān)聯(lián),代碼冗余度高揭绑;
  • 秒殺驗證邏輯復雜弓候,對交易系統(tǒng)產(chǎn)生無關(guān)聯(lián)負載郎哭;

1. 秒殺令牌

1.1 原理

  • 秒殺接口需要依靠令牌才能進入,對應(yīng)的秒殺下單接口需要新增一個入?yún)⒐酱妫硎緦?yīng)前端用戶獲得傳入的一個令牌夸研,只有令牌處于合法之后,才能進入對應(yīng)的秒殺下單的邏輯依鸥;
  • 秒殺令牌由秒殺活動模塊負責生成亥至,交易系統(tǒng)僅僅驗證令牌的可靠性,以此來判斷對應(yīng)的秒殺接口是否可以被這次http的request進入贱迟;
  • 秒殺活動模塊對秒殺令牌生成全權(quán)處理姐扮,邏輯收口;
  • 秒殺下單前需要獲得秒殺令牌才能開始秒殺衣吠;

1.2 代碼實現(xiàn)

 /**
    * 每次下單生成秒殺令牌 替代原來令牌
    */
   @RequestMapping(value = "/generatetoken", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
   @ResponseBody
   public CommonReturnType generatetoken(@RequestParam(name = "itemId") Integer itemId,
                                         @RequestParam(name = "promoId") Integer promoId) throws BusinessException {
       //根據(jù)token獲取用戶信息
       String[] tokenArray = httpServletRequest.getParameterMap().get("token");
       if (ArrayUtils.isEmpty(tokenArray) || StringUtils.isEmpty(tokenArray[0]) || tokenArray[0].equals("null")) {
           throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用戶還未登陸茶敏,不能下單");
       }
       //獲取用戶的登陸信息
       UserModel userModel = (UserModel) redisTemplate.opsForValue().get(tokenArray[0]);
       if (userModel == null) {
           throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用戶還未登陸,不能下單");
       }
       /**
        * 以userid為維度防止token重復
        */
       //獲取秒殺訪問令牌
       String promoToken = promoService.generateSecondKillToken(promoId, itemId, userModel.getId());

       if (promoToken == null) {
           throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "生成令牌失敗");
       }
       //返回對應(yīng)的結(jié)果
       return CommonReturnType.create(promoToken);
   }



   //封裝下單請求
   @RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
   @ResponseBody
   public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId,
                                       @RequestParam(name = "amount") Integer amount,
                                       @RequestParam(name = "promoId", required = false) Integer promoId,
                                       @RequestParam(name = "promoToken", required = false) String promoToken) throws BusinessException {


       //根據(jù)token獲取用戶信息
       String[] tokenArray = httpServletRequest.getParameterMap().get("token");
       if (ArrayUtils.isEmpty(tokenArray) || StringUtils.isEmpty(tokenArray[0]) || tokenArray[0].equals("null")) {
           throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用戶還未登陸缚俏,不能下單");
       }
       //獲取用戶的登陸信息
       UserModel userModel = (UserModel) redisTemplate.opsForValue().get(tokenArray[0]);
       if (userModel == null) {
           throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用戶還未登陸惊搏,不能下單");
       }
       //校驗秒殺令牌是否正確
       if (promoId != null) {
           String inRedisPromoToken = (String) redisTemplate.opsForValue().get("promo_token_" + promoId + "_userid_" + userModel.getId() + "_itemid_" + itemId);
           if (inRedisPromoToken == null) {
               throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒殺令牌校驗失敗");
           }
           if (!org.apache.commons.lang3.StringUtils.equals(promoToken, inRedisPromoToken)) {
               throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒殺令牌校驗失敗");
           }
       }
....
   }


生成秒殺令牌

    /**
     * 生成秒殺用的令牌
     *
     * @param promoId
     * @param itemId
     * @param userId
     */
    @Override
    public String generateSecondKillToken(Integer promoId, Integer itemId, Integer userId) {
        //判斷是否庫存已售罄,若對應(yīng)的售罄key存在忧换,則直接返回下單失敗
        if (redisTemplate.hasKey(PROMO_ITEM_STOCK_INVALID + itemId)) {
            return null;
        }
        PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);

        //dataobject->model
        PromoModel promoModel = convertFromDataObject(promoDO);
        if (promoModel == null) {
            return null;
        }

        //判斷當前時間是否秒殺活動即將開始或正在進行
        if (promoModel.getStartDate().isAfterNow()) {
            promoModel.setStatus(1);
        } else if (promoModel.getEndDate().isBeforeNow()) {
            promoModel.setStatus(3);
        } else {
            promoModel.setStatus(2);
        }
        //判斷活動是否正在進行
        if (promoModel.getStatus().intValue() != 2) {
            return null;
        }

        //判斷item信息是否存在
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);
        if (itemModel == null) {
            return null;
        }
        //判斷用戶信息是否存在
        UserModel userModel = userService.getUserByIdInCache(userId);
        if (userModel == null) {
            return null;
        }

        /**
         * 如果已發(fā)放令牌數(shù)量大于活動商品的數(shù)量 * 系數(shù)恬惯,就不在發(fā)放秒殺令牌
         *
         * 獲取秒殺大閘的count數(shù)量
         */

        long result = redisTemplate.opsForValue().increment(PROMO_DOOR_COUNT + promoId, -1);
        if (result < 0) {
            return null;
        }
        //生成token并且存入redis內(nèi)并給一個5分鐘的有效期
        String token = UUID.randomUUID().toString().replace("-", "");
        /**
         * 以USERid為維度防止token重復
         */
        redisTemplate.opsForValue().set("promo_token_" + promoId + "_userid_" + userId + "_itemid_" + itemId, token, 5, TimeUnit.MINUTES);
        return token;
    }
image.png
點擊下單按鈕,前端兩次接口請求:
  • 1.創(chuàng)建與用戶id亚茬、商品id酪耳、促銷id關(guān)聯(lián)的時效性token( 非用戶token)&
  • 2.創(chuàng)建訂單createOrder
秒殺令牌的缺陷
  • 在活動剛開始的時候,比如有 1000w個用戶下單才写,就會生成 1000w個秒殺令牌葡兑;
    秒殺令牌的生成是耗資源與性能的奖蔓;
  • 如果1000w個用戶都得到秒殺令牌赞草,那么一直關(guān)注活動的用戶,在秒殺的用戶沒有搶占先機吆鹤;

2. 秒殺大閘

為了解決秒殺令牌在活動一開始無限制生成厨疙,影響系統(tǒng)的性能,提出了秒殺大閘的解決方案疑务;

2.1 原理

依靠秒殺令牌的授權(quán)原理定制化發(fā)牌邏輯沾凄,解決用戶對應(yīng)流量問題,做到大閘功能知允;
根據(jù)秒殺商品初始化庫存頒發(fā)對應(yīng)數(shù)量令牌撒蟀,控制大閘流量;
用戶風控策略前置到秒殺令牌發(fā)放中温鸽;
庫存售罄判斷前置到秒殺令牌發(fā)放中保屯。

2.2 代碼實現(xiàn):

        /**
         * 將大閘的限制數(shù)字設(shè)到redis內(nèi)
         */
        redisTemplate.opsForValue().set(PROMO_ITEM_STOCK +itemModel.getId(), itemModel.getStock());
        String promo_itemid = "promoid_" + promoId + "_itemid_" + itemModel.getId();

        redisTemplate.opsForValue().set(promo_itemid, 1, 10, TimeUnit.MINUTES);

        /**
         * 每隔一段時間重新設(shè)置令牌最大數(shù)量
         */
        String promo_itemid = "promoid_" + promoId + "_itemid_" + itemId;

        if (!redisTemplate.hasKey(promo_itemid)) {
            /**
             * 將大閘的限制數(shù)字設(shè)到redis內(nèi)
             */
            redisTemplate.opsForValue().set(PROMO_DOOR_COUNT + promoId, itemModel.getStock().intValue() * 5);

            redisTemplate.opsForValue().set(promo_itemid, 1, 10, TimeUnit.MINUTES);
        }

        long result = redisTemplate.opsForValue().increment(PROMO_DOOR_COUNT + promoId, -1);
        if (result < 0) {
            return null;
        }

方案缺陷

浪涌流量涌入后系統(tǒng)無法應(yīng)對
多庫存多商品等令牌限制能力弱手负;

3. 隊列泄洪

采用秒殺大閘之后,還是無法解決浪涌流量涌入后臺系統(tǒng)姑尺,并且多庫存多商品等令牌限制能力較弱竟终;

3.1 技術(shù)來源
  • 排隊有些時候比并發(fā)更高效(例如redis單線程模型,innodb mutex key等)切蟋;
  • 依靠排隊去限制并發(fā)流量统捶;
  • 依靠排隊和下游阻塞窗口程度調(diào)整隊列釋放流量大小柄粹;
  • 以支付寶銀行網(wǎng)關(guān)隊列為例喘鸟,支付寶需要對接許多銀行網(wǎng)關(guān),當你的支付寶綁定多張銀行卡驻右,那么支付寶對于這些銀行都有不同的支付渠道迷守。在大促活動時,支付寶的網(wǎng)關(guān)會有上億級別的流量旺入,銀行的網(wǎng)關(guān)扛不住兑凿,支付寶就會將支付請求隊列放到自己的消息隊列中,依靠銀行網(wǎng)關(guān)承諾可以處理的TPS流量去泄洪茵瘾;

阻塞隊列就像“水庫”一樣礼华,攔蓄上游的洪水,削減進入下游河道的洪峰流量拗秘,從而達到減免洪水災(zāi)害的目的圣絮;

3.2 代碼實現(xiàn)
  private ExecutorService executorService;

    @PostConstruct
    public void init() {
        executorService = new ThreadPoolExecutor(20, 20,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(1024),new ThreadPoolExecutor.AbortPolicy());
    }
       /**
         * 同步調(diào)用線程池的submit方法
         */
        /**
         * 擁塞窗口為20的等待隊列,用來隊列化泄洪
         */
        Future<Object> future = executorService.submit(new Callable<Object>() {

            @Override
            public Object call() throws Exception {
                //加入庫存流水init狀態(tài)
                String stockLogId = itemService.initStockLog(itemId, amount);


                //再去完成對應(yīng)的下單事務(wù)型消息機制
                if (!mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId, promoId, amount, stockLogId)) {
                    throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下單失敗");
                }
                return null;
            }
        });

        try {
            future.get();
        } catch (InterruptedException e) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        } catch (ExecutionException e) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        }

3.3 本地雕旨、分布式實現(xiàn)隊列方案比較
本地:將隊列維護在本地內(nèi)存中扮匠;
分布式:將隊列設(shè)置到外部redis中

比如說我們有100臺機器,假設(shè)每臺機器設(shè)置20個隊列凡涩,那我們的擁塞窗口就是2000棒搜,但是由于負載均衡的關(guān)系,很難保證每臺機器都能夠平均收到對應(yīng)的createOrder的請求活箕,那如果將這2000個排隊請求放入redis中力麸,每次讓redis去實現(xiàn)以及去獲取對應(yīng)擁塞窗口設(shè)置的大小,這種就是分布式隊列育韩;

本地和分布式有利有弊:

分布式隊列最嚴重的就是性能問題克蚂,發(fā)送任何一次請求都會引起call網(wǎng)絡(luò)的消耗,并且要對Redis產(chǎn)生對應(yīng)的負載筋讨,Redis本身也是集中式的埃叭,雖然有擴展的余地。單點問題就是若Redis掛了悉罕,整個隊列機制就失效了赤屋。

本地隊列的好處就是完全維護在內(nèi)存當中的误墓,因此其對應(yīng)的沒有網(wǎng)絡(luò)請求的消耗 ,只要JVM不掛益缎,應(yīng)用是存活的谜慌,那本地隊列的功能就不會失效。因此企業(yè)級開發(fā)應(yīng)用還是推薦使用本地隊列莺奔,本地隊列的性能以及高可用性對應(yīng)的應(yīng)用性和廣泛性欣范。可以使用外部的分布式集中隊列令哟,當外部集中隊列不可用時或者請求時間超時恼琼,可以采用降級的策略,切回本地的內(nèi)存隊列屏富。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晴竞,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狠半,更是在濱河造成了極大的恐慌噩死,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件神年,死亡現(xiàn)場離奇詭異已维,居然都是意外死亡,警方通過查閱死者的電腦和手機已日,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門垛耳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人飘千,你說我怎么就攤上這事堂鲜。” “怎么了护奈?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵缔莲,是天一觀的道長。 經(jīng)常有香客問我逆济,道長酌予,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任奖慌,我火速辦了婚禮,結(jié)果婚禮上松靡,老公的妹妹穿的比我還像新娘简僧。我一直安慰自己,他們只是感情好雕欺,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布岛马。 她就那樣靜靜地躺著棉姐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啦逆。 梳的紋絲不亂的頭發(fā)上伞矩,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機與錄音夏志,去河邊找鬼乃坤。 笑死,一個胖子當著我的面吹牛沟蔑,可吹牛的內(nèi)容都是我干的湿诊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瘦材,長吁一口氣:“原來是場噩夢啊……” “哼厅须!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起食棕,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤朗和,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后簿晓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體例隆,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年抢蚀,在試婚紗的時候發(fā)現(xiàn)自己被綠了镀层。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡皿曲,死狀恐怖唱逢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屋休,我是刑警寧澤坞古,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站劫樟,受9級特大地震影響痪枫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叠艳,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一奶陈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧附较,春花似錦吃粒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽事示。三九已至,卻和暖如春僻肖,著一層夾襖步出監(jiān)牢的瞬間肖爵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工臀脏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留劝堪,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓谁榜,卻偏偏與公主長得像幅聘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子窃植,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容