最簡單有效的秒殺限流

情景

整點秒殺、限時特賣等活動會吸引大量用戶在同一時間點、時間段訪問請求系統,從而產生大的併發量,若是沒有合理處理的話會出現商品超賣頁面訪問速度較慢更嚴重的可能出現系統宕機其餘服務也沒法正常使用。git

本文主要處理服務端使用redis進行限流github

涉及到的技術點主要有: redis分佈式鎖 redis經常使用命令redis

主要邏輯方案:bash

  1. 預熱庫存數據架構

  2. 限流(容許進入兩倍庫存的訪問量)併發

  3. 10秒失效一次redis庫存數據 從新獲取(處理由於進入下單頁沒有提交訂單 而致使的庫存剩餘)分佈式

實現

  1. 主要實現代碼 判斷是否有庫存isEnoughRepositoryide

    /**
     * @author lemon
     * @date 2018-10-06
     * @desc jedis實現秒殺限流
     */
    @Component
    public class JedisSecKill {
    
        public static final Log LWT_LOG = LogFactory.getLog(JedisSecKill.class.getName());
    
        @Resource
        MallProductDao mallProductDao;
    
        /**
         * 初始化商品的庫存
         * 1. 若是已經初始化則不須要初始化 直接返回true
         * 2. 獲取jedis分佈式鎖 獲取成功查詢庫 更新jedis庫存(若是庫存爲0則標記已售完)
         * 3. 庫存信息10秒後失效
         * @param productId
         * @return 是否初始化成功(以前初始化過/獲取鎖成功)
         */
        private boolean initRepository(final int productId) {
            final String secProductKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT, productId + "");
            if (JedisUtil.exists(secProductKey)) {
                return true;
            }
    
            final String secProductStatusKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_STATUS, productId + "");
            boolean lock = JedisLock.lock(productId + "", new LockFunction() {
                @Override
                public void call() {
                    List<MallProductSpec> productSpecs = mallProductDao.listMallProductSpec(productId);
                    if (CollectionUtils.isNotEmpty(productSpecs)) {
                        boolean isSoldOut = true;
                        for (MallProductSpec spec : productSpecs) {
                            if (StringUtils.equals(spec.getUseRepository(), "N")) {
                                isSoldOut = false;
                                break;
                            }
    
                            if (StringUtils.equals(spec.getUseRepository(), "Y") && spec.getRepository() > 0) {
                                isSoldOut = false;
                                break;
                            }
    
                        }
    
                        if (isSoldOut) {
                            JedisUtil.set(secProductStatusKey, "soldOut");
                            JedisUtil.expire(secProductStatusKey, 60);
                            return;
                        } else {
                            JedisUtil.set(secProductStatusKey, "inSale", "NX", "EX", 60);
                        }
    
                        initPerRepository(productSpecs);
                    }
                }
    
                private void initPerRepository(List<MallProductSpec> productSpecs) {
                    Jedis jedis = null;
                    try {
                        jedis = JedisUtil.getJedis();
                        String setRes = jedis.set(secProductKey, "inSale", "NX", "EX", 10);
                        if (StringUtils.equals("OK", setRes)) {
                            for (MallProductSpec spec : productSpecs) {
                                String specKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_SPEC, spec.getSpecKey());
                                if (StringUtils.equals(spec.getUseRepository(), "N")) {
                                    jedis.set(specKey, 99 + "", "NX", "EX", 10);
                                } else {
                                    jedis.set(specKey, (spec.getRepository() * 2) + "", "NX", "EX", 10);
                                }
                            }
                        }
    
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        if (jedis != null) {
                            jedis.close();
                        }
                    }
                }
            });
            return lock;
        }
    
        /**
         * 根據商品id和規格 獲取jedis是否有足夠庫存
         * 核心使用 jedis.decr 進行單線程操做庫存 處理併發
         * @param productId
         * @param specKey
         * @return
         */
        public boolean isEnoughRepository(int productId, String specKey) {
            boolean initRes = initRepository(productId);
            if (!initRes) {
                return false;
            }
    
            final String secProductStatusKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_STATUS, productId + "");
            Jedis jedis = null;
            try {
                jedis = JedisUtil.getJedis();
                String secStatus = jedis.get(secProductStatusKey);
                if (StringUtils.equals("soldOut", secStatus)) {
                    return false;
                }
    
                String specParamKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_SPEC, specKey);
                Boolean exists = jedis.exists(specParamKey);
                Long decr = jedis.decr(specParamKey);
                LWT_LOG.debug("enoughRepository key:"+ specParamKey+" exists:" + exists + ",value:" + jedis.get(specParamKey) + ",decrValue:" + decr);
                if (exists && decr >= 0) {
                    return true;
                }
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
    
            return false;
        }
    
    }
    複製代碼

  2. 其中常量值優化

    //秒殺使用此key控制庫存是否過時
    public final static String SEC_KILL_PRODUCT = "sec_kill_product_%s";    
    //使用此key控制秒殺是否售罄
    public final static String SEC_KILL_PRODUCT_STATUS = "sec_kill_product_status_%s";
    //使用此key控制秒殺商品sku
    public final static String SEC_KILL_PRODUCT_SPEC = "sec_kill_product_spec_%s";  
    複製代碼
  3. redis分佈式鎖ui

    /**
     * @author lemon(lemon @ laowantong.cc)
     * @date 2018-10-06
     * @desc jedis 分佈式鎖
     */
    
    public class JedisLock {
    
        /**
         * 經過獲取key的setnx
         * 若是獲取成功 執行獲取成功以後的function
         * 執行完成以後刪除key釋放鎖
         * @param key
         * @param function
         * @return
         */
        public static boolean lock(String key, LockFunction function) {
            Jedis jedis = null;
            try {
                jedis = JedisUtil.getJedis();
                if (jedis != null) {
                    String setRes = jedis.set(key, "b", "NX", "EX", 5);
                    if (StringUtils.equals("OK", setRes)) {
                        function.call();
                        jedis.del(key);
                        return true;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
    
            return false;
        }
    }
    複製代碼

此方案的問題

  1. 分佈式鎖競爭期間 其餘爭奪資源的併發請求 處理結果爲失敗
  2. 沒有完全知足先到先得的原則(可以使用隊列方案實現)

附一秒殺架構圖

架構優化

歡迎交流 原文地址:不愛更新的博客

未經做者容許 請勿轉載,謝謝 :)

相關文章
相關標籤/搜索