秒殺項目總結(附秒殺系統設計)

GitHub:鏈接地址

UserController("/user")

獲取驗證碼("/getotp")

  • 隨機生成10000~99999的數字
  • 將驗證碼同對應手機號關聯,即使將 < 手機號, 驗證碼 > 這個KV對存到session中
  • 模擬短信發送(僅僅將其打印出來)
  • 返回通用對象

註冊("/register")

  • 根據手機號從session中獲取驗證碼
  • 若驗證成功,則調用userService的註冊方法(註冊過程是一個事務)
    • 註冊內容主要就是合法性判斷,數據庫的更新等等(使用MD5對密碼進行加密)

獲取User信息("/get")

  • 調用UserService的接口方法,根據userid來獲取用戶信息
  • 返回

登錄("/login")

  • 登錄參數的合法性校驗(手機號, 密碼)
  • 根據登錄參數,判斷能否登錄成功(即判斷用戶名密碼是否正確)
  • 如果正確的話,就生成一個登錄成功的Token,存儲到Redis中(該Token爲< UUID, userModel > ),並設置失效時間爲1個小時
  • 返回TokenId(用UUID生成的Token的唯一標識符)

ItemController("/item")

創建商品("/create")

  • 根據傳入參數來調用創建商品的Service(往數據庫裏寫對應商品信息,沒什麼好說的)

促銷商品的發佈("/publishpromo")

  • 調用商品的促銷模塊來發布商品,除了創建對應的促銷商品實體之外,最重要的是將促銷商品的庫存信息預存到redis中.(提前把火爆的熱鍵存到redis中)
  • 同時設定 秒殺大閘 (這個是幹嘛的忘了。。看到後面再補充把 todo

商品詳情頁的瀏覽("/get")

  • 先嚐試從本地緩存獲取數據(使用的是Guava中的Cache)
  • 再嘗試從redis從根據商品id獲取商品信息
  • 如果都沒獲取到,就去訪問數據庫(訪問數據庫的時候,需要先通過布隆過濾器的判斷,防止緩存穿透)

商品列表("/list")

  • 沒啥好說的,也是調用Item的服務…

OrderController("/order")

初始化方法:

  • 線程池初始化
  • RateLimiter初始化

生成驗證碼("/generateverifycode")

  • todo

生成促銷商品下單令牌("/generatetoken")、

之所以需要使用這一步,是爲了防止下單接口被刷。 在前端點下下單按鈕的時候,會先申請一個促銷商品下單的令牌,只有獲取這個令牌,才能在之後購買促銷的商品

  • 嘗試從redis中根據tokenID獲取用戶登錄信息(token),獲取不到說明沒登錄
  • 驗證希望獲取的商品令牌的商品是否是正在促銷的商品(這裏的下單令牌僅對促銷商品有用
  • 根據用戶id,促銷商品id, 商品本身id,來生成“促銷商品下單令牌”,並返回

下單

本次實戰,主要也是對該下單接口進行一個優化.

  1. 使用RateLimiter對象進行限流(tryAcquire()方法如果返回false,就直接返回對應異常)

  2. (Session檢查)根據request中的UUID,嘗試獲取redis中的用戶信息(如果獲取不到說明沒有登錄)

  3. (合法性檢查)校驗是否已經獲取過“都小商品下單令牌”,並驗證是否合法

  4. 生成一個庫存流水(用於消息回查)

  5. 基於rocketmq,調用消息服務,發送一條事務消息。

    • 消息服務(MQProducer)是個單例Bean,在初始化方法中,會對自身進行一個初始化,包括設置NameservAddr,設置listener等等…
    • 事務消息的兩個回調函數的實現內容:
      • 在回調方法中(executeLocalTransaction),執行本地事務,這裏其實就是將本地事務的執行和消息的發送綁成了一個事務,若本地事務執行成功,則消息才能發送成功,若本地事務執行失敗,則消息回滾,依然對消費者不可見。
      • 這裏的本地事務就是對redis中的庫存進行扣減,並生成對應的訂單等操作…
      • 在回查方法中(checkLocalTransaction), 會根據庫存流水的狀態,來決定當前消息的狀態。 即具體邏輯就是,傳入的參數裏有庫存流水ID,根據這個ID去查詢對應的下單流水信息,如果status狀態是1的話,就說明是正在等待處理,依然是unknown,如果是2的話,則說明已經處理完了,直接返回commit,如果是3的話,說明發生了回滾,就返回回滾狀態.
      • 同時在庫存流水中也添加一個字段,即回查該消息的次數,如果超過的一定的次數則判定該消息出現問題,對其進行一個回滾…(例如在消息COMMIT之前出現了宕機,那麼該消息就一直處於了UNKNOWN狀態…所以,如果處於UNKNOWN狀態過久的話,就判斷出現了問題。。對其進行回滾)
  • 如果事務消息發送成功,則本地事務一定執行成功,所以也就表明下單成功。。。(這裏只解決了保證消息投遞成功,但沒解決消費端一定能消費成功)

另一端:

  • 對於消費端而言,會消費生產者投遞的庫存扣減的消息。。。

如何設計一個秒殺系統

問題一:瞬時高併發

時間極短, 瞬間用戶量大, 出現緩存雪崩,緩存擊穿,緩存穿透等情況,打掛DB可能會造成系統的聯級失效。

1.1 服務單一職責——防止聯級失效

單獨提供一個秒殺服務模塊,數據庫也採用分庫的思想,單獨提供一個秒殺庫。 這樣做的好處就是即使秒殺庫掛了,服務掛了,也不會影響到其他業務。

1.2 Redis集羣——解決單臺redis性能瓶頸

單機的Redis頂不住高併發的問題可以通過搭建Redis集羣來解決,寫master,讀slave,然後開啓哨兵模式,開啓持久化保證高可用。

1.3 Ngnix反向代理——解決單臺服務器性能瓶頸

一臺ngix服務器負責反向代理,多臺web服務器提供服務。

1.4 緩存雪崩、緩存擊穿、緩存穿透問題也要考慮

  • 緩存雪崩
    • 問題:當緩存層出現問題沒法工作時,流量會打到後臺數據庫上,會出現問題。
    • 解決方法:
      • 保證redis高可用
      • 進行限流(可以使用線程池,信號量,Guava的RateLimiter等來進行限流)
  • 緩存穿透
    • 問題:訪問緩存和數據庫中都沒有的數據,這種請求會穿過緩存層打到數據庫上,若有大量的這種請求,那麼緩存層就形同虛設了…
    • 解決方法:
      • 緩存空數據(不推薦,因爲組合出數據庫裏不存在的數據的方式很多,這麼做不僅沒法解決問題甚至還會讓redis掛掉…)
      • 布隆過濾器:布隆過濾器能夠保證無法通過的數據一定不存在,所以,可以事先把數據庫裏的主鍵給預存到布隆過濾器裏,只有通過了布隆過濾器的那些訪問請求,纔會去redis,數據庫中去查找.
  • 緩存擊穿
    • 問題: 一個超級熱點數據如果因爲超時失效或其他原因而從redis中被刪除,那麼短期大量的流量就會打到數據庫上.
    • 解決辦法:
      • 對於熱點數據不設置失效時間
      • 提前將熱點數據預存到redis中
      • 使用第三方緩存或本地緩存(例如Guava的Cache),如果是熱點數據的話,頻繁被訪問的情況話就不會被置換出去
      • 限流(線程池、信號量、RateLimiter。。。)熔斷(to study)

1.5 消息隊列保證Redis和Mysql的最終一致性

  • 使用rocketmq的事務型消息 ( rocketmq的事務型消息保證了若成功發送了commit消息那麼本地事務一定成功執行了(ps:只有commit的消息才能被消費者看到,而prepare的消息無法被看到) ):
    • 在本地事務中執行對Redis等其他不是太消時間的,比較關鍵的操作,對mysql的同步交給消費者去做.

問題二:超賣問題

就比如現在庫存只剩下1個了,我們高併發嘛,4個服務器一起查詢了發現都是還有1個,那大家都覺得是自己搶到了,就都去扣庫存,那結果就變成了-3,是的只有一個是真的搶到了,別的都是超賣的。

  • 解決方法:
    • 增加全局售罄標誌,如果發現秒殺商品已經售罄,那就直接返回了.
    • 使用Lua腳本實現Redis的CAS功能。Lua腳本是類似Redis事務,有一定的原子性,不會被其他命令插隊,可以完成一些Redis事務性的操作。寫一個Lua腳本把判斷庫存和扣減庫存的操作都寫在一個腳本丟給Redis去做。(to study)
    • 使用分佈式鎖來對redis的庫存個數進行上鎖然後再進行判斷,扣減.

問題三:惡意刷單

  • 讓前端控制,秒殺商品的下單按鈕在秒殺前置灰。
  • 使用秒殺令牌,在頁面端點下單按鈕的時候,會先向服務器申請一個秒殺令牌,然後纔是真正的下單,在下單時會去檢查這個秒殺令牌是否存在,是否合法。
  • 服務端限制同一個用戶的最大下單個數?
  • 封IP(從nginx這邊來控制)
  • 使用驗證碼

考慮的點:

  1. 如果秒殺的流量單機無法抗住的話,就用集羣+分佈式來擴展…(1.3 1.2)
  2. 單獨提供一個秒殺服務模塊,數據庫也採用分庫的思想,單獨提供一個秒殺庫。 這樣做的好處就是即使秒殺庫掛了,服務掛了,也不會影響到其他業務,對於數據表也進行分表(庫存單獨分離出來)。
  3. 充分利用緩存(多級緩存的思路,本地緩存Guava的Cache,第三方緩存,redis緩存,緩存帶來的問題(穿透,雪崩,擊穿))
  4. 消息隊列保證redis和mysql的最終一致性
  5. Redis中的共享數據(比如在redis中的庫存),在多線程環境下使用分佈式鎖,用redis的setnx + px 來做
  6. 考慮到惡意下單,看上面的問題3
  7. 考慮到限流量,可以使用Guava的RateLimiter,信號量,線程池來做…

問題:

Q: 庫存預熱:
A: 這個一般給運營人員去做吧。。手動將秒殺商品的庫存加載到redis裏面,在規模比較大的時候的話,可以專門寫一個這種管理平臺系統吧…

Q: 扣減庫存怎麼保證線程安全:
A: 項目裏是通過分佈式鎖來去做的(todo 實現一下分佈式鎖), 就是向上提供一個查詢接口,如果獲取不到鎖就自旋…

Q: 緩存擊穿
A: redis預熱、永不失效、本地緩存或第三方緩存、限流策略…

Q: 客戶的惡意下單怎麼防範
A: 秒殺令牌機制。 其他還有: 驗證碼,封ip(nginx有個設置項,單個ip訪問的頻率和次數多了之後有個拉黑操作…服務端寫邏輯封用戶?),(接口隱藏 )秒殺前將下單接口置灰,使獲取不到下單接口… 或者也可以限制同一個用戶的最大下單數?

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