以前一直有小夥伴私信我問我高併發場景下的訂單和庫存處理方案,我最近也是由於加班的緣由比較忙,就一直沒來得及回覆。今天好不容易閒了下來想了想不如寫篇文章把這些都列出來的,讓你們都能學習到,說一千道一萬都不如滿滿的乾貨來的實在,乾貨都下面了!前端
前提:分佈式系統,高併發場景
商品A只有100庫存,如今有1000或者更多的用戶購買。如何保證庫存在高併發的場景下是安全的。
預期結果:1.不超賣 2.很多賣 3.下單響應快 4.用戶體驗好mysql
下單思路:面試
(退單有單獨的庫存流水,申請退單插入流水,退單完成刪除流水+還庫存)redis
分析:sql
因此綜上所述:
選擇方案三比較合理。數據庫
重複下單問題緩存
前端攔截,點擊後按鈕置灰。安全
//key , 等待獲取鎖的時間 ,鎖的時間 redis.lock("shop-oms-submit" + token, 1L, 10L);
redis的key用token + 設備編號 一個用戶多個設備能夠同時下單。網絡
//key , 等待獲取鎖的時間 ,鎖的時間 redis.lock("shop-oms-submit" + token + deviceType, 1L, 10L);
(2)防止惡意用戶,惡意*** : 一分鐘調用下單超過50次 ,加入臨時黑名單 ,10分鐘後纔可繼續操做,一小時容許一次跨時段弱校驗。使用reids的list結構,過時時間一小時併發
/** * @param token * @return true 可下單 */ public boolean judgeUserToken(String token) { //獲取用戶下單次數 1分鐘50次 String blackUser = "shop-oms-submit-black-" + token; if (redis.get(blackUser) != null) { return false; } String keyCount = "shop-oms-submit-count-" + token; Long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")); //每一小時清一次key 過時時間1小時 Long count = redis.rpush(keyCount, String.valueOf(nowSecond), 60 * 60); if (count < 50) { return true; } //獲取第50次的時間 List<String> secondString = redis.lrange(keyCount, count - 50, count - 49); Long oldSecond = Long.valueOf(secondString.get(0)); //now > oldSecond + 60 用戶可下單 boolean result = nowSecond.compareTo(oldSecond + 60) > 0; if (!result) { //觸發限制,加入黑名單,過時時間10分鐘 redis.set(blackUser, String.valueOf(nowSecond), 10 * 60); } return result; }
多用戶搶購時,如何作到併發安全減庫存?
sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;
分析:
高併發場景下,假設庫存只有 1件 ,兩個請求同時進來,搶購該商品.
數據庫層面會限制只有一個用戶扣庫存成功。在併發量不是很大的狀況下能夠這麼作。可是若是是秒殺,搶購,瞬時流量很高的話,壓力會都到數據庫,可能拖垮數據庫。
/** * 缺點併發不高,同時只能一個用戶搶佔操做,用戶體驗很差! * * @param orderSkuAo */ public boolean subtractStock(OrderSkuAo orderSkuAo) { String lockKey = "shop-product-stock-subtract" + orderSkuAo.getOrderCode(); if(redis.get(lockKey)){ return false; } try { lock.lock(lockKey, 1L, 10L); //處理邏輯 }catch (Exception e){ LogUtil.error("e=",e); }finally { lock.unLock(lockKey); } return true; }
分析:
利用Redis 分佈式鎖,強制控制同一個商品處理請求串行化,缺點併發不高 ,處理比較慢,不適合搶購,高併發場景。用戶體驗差,可是減輕了數據庫的壓力。
/** * 扣庫存操做,秒殺的處理方案 * @param orderCode * @param skuCode * @param num * @return */ public boolean subtractStock(String orderCode,String skuCode, Integer num) { String key = "shop-product-stock" + skuCode; Object value = redis.get(key); if (value == null) { //前提 提早將商品庫存放入緩存 ,若是緩存不存在,視爲沒有該商品 return false; } //先檢查 庫存是否充足 Integer stock = (Integer) value; if (stock < num) { LogUtil.info("庫存不足"); return false; } //不可在這裏直接操做數據庫減庫存,不然致使數據不安全 //由於此時可能有其餘線程已經將redis的key修改了 //redis 減小庫存,而後才能操做數據庫 Long newStock = redis.increment(key, -num.longValue()); //庫存充足 if (newStock >= 0) { LogUtil.info("成功搶購"); //TODO 真正扣庫存操做 可用MQ 進行 redis 和 mysql 的數據同步,減小響應時間 } else { //庫存不足,須要增長剛剛減去的庫存 redis.increment(key, num.longValue()); LogUtil.info("庫存不足,併發"); return false; } return true; }
分析:
利用Redis increment 的原子操做,保證庫存安全,利用MQ保證高併發響應時間。可是事須要把庫存的信息保存到Redis,並保證Redis 和 Mysql 數據同步。缺點是redis宕機後不能下單。
increment 是個原子操做。
方案三知足秒殺、高併發搶購等熱點商品的處理,真正減扣庫存和下單能夠異步執行。在併發狀況不高,日常商品或者正常購買流程,能夠採用方案一數據庫樂觀鎖的處理,或者對方案三進行從新設計,設計成支持單訂單多商品便可,但複雜性提升,同時redis和mysql數據一致性須要按期檢查。
訂單時效問題
超過訂單有效時間,訂單取消,可利用MQ或其餘方案回退庫存。
設置定時檢查
Spring task 的cron表達式定時任務
MQ消息延時隊列
TCC 模型:Try/Confirm/Cancel:不使用強一致性的處理方案,最終一致性便可,下單減庫存,成功後生成訂單數據,若是此時因爲超時致使庫存扣成功可是返回失敗,則經過定時任務檢查進行數據恢復,若是本條數據執行次數超過某個限制,人工回滾。還庫存也是這樣。
冪等性:分佈式高併發系統如何保證對外接口的冪等性,記錄庫存流水是實現庫存回滾,支持冪等性的一個解決方案,訂單號+skuCode爲惟一主鍵(該表修改頻次高,少建索引)
樂觀鎖:where stock + num>0
消息隊列:實現分佈式事務 和 異步處理(提高響應速度)
redis:限制請求頻次,高併發解決方案,提高響應速度
分佈式鎖:防止重複提交,防止高併發,強制串行化
分佈式事務:最終一致性,同步處理(Dubbo)/異步處理(MQ)修改 + 補償機制
你們看完有什麼不懂的能夠在下方留言討論,也能夠私信問我通常看到後我都會回覆的。也歡迎你們關注個人公衆號:前程有光,金三銀四跳槽面試季,整理了1000多道將近500多頁pdf文檔的Java面試題資料,文章都會在裏面更新,整理的資料也會放在裏面。最後以爲文章對你有幫助的話記得點個贊哦,點點關注不迷路,天天都有新鮮的乾貨分享!