在介紹架構之前,我們需要先知道秒殺系統面臨的難點是什麼。
首先在普通的系統中, 最大的瓶頸是在於底層的資料庫端 。 因為底層資料庫(比如常見的mysql)是磁碟儲存的,所以讀寫IO較慢,而且連線數有限。
而在秒殺業務場景,最大的特點是 瞬時的高併發 ,即在短時間內會有大量的請求到來。 如果讓所有請求都打到底層資料庫上,很大可能資料庫會直接崩掉,即使資料庫能承受住大量的連線請求,但大量的請求讀寫都會導致大量的鎖衝突,導致響應速度大大減慢。 而響應速度對於使用者體驗來說,無疑是十分重要的。
所以在這裡,需要明確第一個目標: 讓儘可能少、儘可能有效的請求打到底層資料庫 。
當我們回頭再考慮這個業務場景,其實絕大部分的請求都不應該打到底層資料庫。 因為一般商品庫存可能只有搶購使用者數的百分之一,甚至更少。 所以我們需要一些機制、策略,提前將無效的請求返回。
而站在整個網站設計的角度,第二個目標: 越上層越容易實現,越有效。
這裡的層指:
頁面層網路層應用層服務層資料層例如在前端頁面層,如果不做處理,使用者在點選搶購按鈕以後,見網頁沒有響應,可能會再點選3-4次甚至更多,這樣可能會導致最終有80%的請求都是重複無效的。 但只需要前端在設計時,將點選後的按鈕置灰,防止使用者多次點擊發送請求。 即簡單又有效。
以下簡單指出各層可實施的策略:
頁面層(簡單的實現可以遮蔽 90%的請求)按鈕置灰,禁止使用者重複提交驗證碼網路層通過ip限制一定時間內的請求次數應用層一個頁面最佔用資源、頻寬的是cs js 圖片等靜態資源避免所有請求都到伺服器的硬碟上取動靜分離,壓縮快取處理(CDN nginx)根據uid限頻,頁面快取技術(web伺服器 nginx)反向代理 + 負載均衡 (nginx)服務層微服務redis訊息佇列 削峰 非同步處理資料層讀寫分離分庫分表叢集每一層具體實現起來都是一個很大的架構,這裡我們主要專注於服務層,使用redis+訊息佇列。
基礎架構
架構.png
核心: 服務非同步拆分,減少耦合,使用快取,加快響應。
避免同步 的請求執行,如: 請求→訂單→支付→修改庫存→結束返回,這種模型在高併發場景下,阻塞多,響應慢,伺服器壓力大,不可取。
這裡實現的架構是: 1. 請求→返回 2. 支付→返回 3. 修改庫存
這種服務拆分歸功於 訊息佇列。 核心思想是,將接收到的請求 儲存到佇列中就可以響應使用者了,後端在佇列中取出請求再做後續操作即可。 簡單理解就是,我們將請求記錄下來,晚點空閒了再處理。
基礎資料儲存
資料、請求的儲存情況如:
mysql中儲存商品資訊、訂單資訊redis存入商品資訊、設定計數器、儲存成功訂單的資料結構等rabbitmq建立佇列訂單佇列(使用者提交請求)延遲佇列(訂單必須在15分鐘內支付)成交佇列(訂單支付成功,等待寫入資料庫)流程
以下所有程式碼都是擷取核心部分,完整程式碼參看
訂單請求
redis計數器
假設我們只有100件商品庫存,但可能會收到10萬條搶購請求。 也就是會有將近9.9萬條無效的請求,所以我們要將這些請求阻隔。
最簡單的方法,也是我們使用的方法: 實現一個count變數,每個請求進入都加一,當count大於100時則直接返回失敗即可 。
這裡我們使用redis也是因為記憶體讀寫速度要遠大於類似mysql的磁碟讀寫。
程式碼實現增加了分散式鎖。相關知識可以看:https://www.jianshu.com/p/cf311cfb1689
訂單佇列
非同步拆分服務的關鍵。 為了提高響應速度,我們只需要 將請求訂單任務儲存下來 (訊息佇列),就可以 直接返回使用者 了。 而 不需要將請求轉到後端做大量的判斷、處理、資料庫讀寫操作後才返回使用者 。 所有可以 大大的加快響應速度 。 後端可以隨時從佇列中取出請求再做各自處理,即使等搶購活動結束再進行底層資料庫讀寫也沒有問題。
所以核心思路就是把請求放入佇列,然後直接返回使用者即可。
enter_order_queue是將訂單請求(訂單資訊),也就是order_info傳送到對應的佇列。 與之對應的消費者,只需要將該訂單資訊寫入資料庫對應的訂單表即可。
注意: 此時訂單還沒支付,所以資料庫表中可以設定一個status欄位,標識訂單的狀態。
唯一標識
不侷限於uuid,可用毫秒時間戳之類的唯一標識。
可以看到上面程式碼中,我們利用uuid生成了一個唯一標識作為訂單號,並且返回給使用者。
主要的作用是:
標識訂單。因為訂單請求僅僅只是被我們入佇列,消費者可能還沒開始處理。(即訂單可能還未被建立在資料庫中)返回給使用者,可用於後續的支付操作。當用戶支付時需要校驗使用者與對應的單號是否正確,這裡我們仍用redis,以提高查詢速度。
所以在上面的基礎上,我們需要加多一步,將訂單資訊寫入redis。
正如前面所見,我們提示使用者在15分鐘之內支付,符合日常業務場景。
在訊息佇列中有延遲佇列的應用,符合我們的超時需求。 所以我們同樣用訊息佇列來實現這一業務需求。 即我們在建立訂單時,同樣將訂單資訊傳入佇列中。
try :
# redis儲存訂單資訊
create_order(order_info)
# 訂單佇列
enter_order_queue(order_info)
# 超時佇列
enter_overtime_queue(order_info)
最終,當一個訂單請求通過計數器後,需要經歷的三個過程如上。 無論是redis或是rabbitmq訊息佇列,都是記憶體操作,速度都是足夠快的。 不需要經過資料層即可響應使用者。
至此,一個訂單“建立”完成。
支付請求
訂單請求完成後,使用者會獲得訂單號。 使用者必須在15分鐘內完成支付操作。 在執行支付時需要考慮:
檢查使用者和對應的訂單號是否正確create_order(order_info) 時,我們已將訂單資訊寫入redis。可從這裡取得資料做校驗檢查訂單是否超時如果我們設定的超時佇列超過指定時間,則佇列裡的請求會被處理(消費)我們只需要將超時的單號寫入redis即可做校驗支付成功入成交佇列同理於訂單佇列。只需將成交的訂單資訊寫入訊息佇列中,後續系統空閒時再寫入資料庫即可。也是為了提高使用者響應速度,使用者不需要等待資料庫io完成後才收到結果。程式碼流程為:
但訂單通過檢查、並支付完成後。 我們還需要將成交的訂單寫入redis,記錄狀態(用於其他判斷)。 再將訂單請求寫入佇列即可返回。 全程記憶體操作,速度快,帶來了快響應。 之後,我們可以等搶購活動結束後,系統比較空閒的時間將訂單同步到底層資料庫,同步資料。
總覽
所以兩個核心的操作是:
通過rabbitmq訊息佇列非同步拆分服務,加快了響應的速度通過redis記憶體讀寫,減少操作時間再總結整個框架:
流程
注意
程式碼持續更新,完整程式碼: /file/2019/11/04/20191104213051_154446.jpg (覺得有幫助的可以給個star)本架構只專注於服務層的業務架構,有很多沒有涉及的點(高可用,資料一致性等),一個完整的搶購系統是一個非常龐大的。文中沒有介紹mysql資料層相關的操作,一方面是為了提示大家,在高併發的情景下應該儘可能的避免這類的磁碟io操作。 另一方面,mysql資料層相關操作是在訊息佇列 消費者進行操作的,這裡不詳解操作。 只注重整體架構。 具體操作見程式碼。