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,來生成“促銷商品下單令牌”,並返回
下單
本次實戰,主要也是對該下單接口進行一個優化.
-
使用RateLimiter對象進行限流(tryAcquire()方法如果返回false,就直接返回對應異常)
-
(Session檢查)根據request中的UUID,嘗試獲取redis中的用戶信息(如果獲取不到說明沒有登錄)
-
(合法性檢查)校驗是否已經獲取過“都小商品下單令牌”,並驗證是否合法
-
生成一個庫存流水(用於消息回查)
-
基於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.3 1.2)
- 單獨提供一個秒殺服務模塊,數據庫也採用分庫的思想,單獨提供一個秒殺庫。 這樣做的好處就是即使秒殺庫掛了,服務掛了,也不會影響到其他業務,對於數據表也進行分表(庫存單獨分離出來)。
- 充分利用緩存(多級緩存的思路,本地緩存Guava的Cache,第三方緩存,redis緩存,緩存帶來的問題(穿透,雪崩,擊穿))
- 消息隊列保證redis和mysql的最終一致性
- Redis中的共享數據(比如在redis中的庫存),在多線程環境下使用分佈式鎖,用redis的setnx + px 來做
- 考慮到惡意下單,看上面的問題3
- 考慮到限流量,可以使用Guava的RateLimiter,信號量,線程池來做…
問題:
Q: 庫存預熱:
A: 這個一般給運營人員去做吧。。手動將秒殺商品的庫存加載到redis裏面,在規模比較大的時候的話,可以專門寫一個這種管理平臺系統吧…
Q: 扣減庫存怎麼保證線程安全:
A: 項目裏是通過分佈式鎖來去做的(todo 實現一下分佈式鎖), 就是向上提供一個查詢接口,如果獲取不到鎖就自旋…
Q: 緩存擊穿
A: redis預熱、永不失效、本地緩存或第三方緩存、限流策略…
Q: 客戶的惡意下單怎麼防範
A: 秒殺令牌機制。 其他還有: 驗證碼,封ip(nginx有個設置項,單個ip訪問的頻率和次數多了之後有個拉黑操作…服務端寫邏輯封用戶?),(接口隱藏 )秒殺前將下單接口置灰,使獲取不到下單接口… 或者也可以限制同一個用戶的最大下單數?