【分散式技術專題】「架構設計方案」盤點和總結秒殺服務的功能設計及注意事項技術體系

2022-12-09 15:00:29

秒殺應該考慮哪些問題

超賣問題

分析秒殺的業務場景,最重要的有一點就是超賣問題,假如備貨只有100個,但是最終超賣了200,一般來講秒殺系統的價格都比較低,如果超賣將嚴重影響公司的財產利益,因此首當其衝的就是解決商品的超賣問題。

高並行

秒殺具有時間短、並行量大的特點,秒殺持續時間只有幾分鐘,而一般公司都為了製造轟動效應,會以極低的價格來吸參照戶,因此參與搶購的使用者會非常的多。短時間內會有大量請求湧進來,後端如何防止並行過高造成快取擊穿或者失效,擊垮資料庫都是需要考慮的問題。

介面防刷

現在的秒殺大多都會出來針對秒殺對應的軟體,這類軟體會模擬不斷向後臺伺服器發起請求,一秒幾百次都是很常見的,如何防止這類軟體的重複無效請求,防止不斷髮起的請求也是需要我們針對性考慮的

秒殺url

對於普通使用者來講,看到的只是一個比較簡單的秒殺頁面,在未達到規定時間,秒殺按鈕是灰色的,一旦到達規定時間,灰色按鈕變成可點選狀態。這部分是針對小白使用者的,如果是稍微有點電腦功底的使用者,會通過F12看瀏覽器的network看到秒殺的url,通過特定軟體去請求也可以實現秒殺。或者提前知道秒殺url的人,一請求就直接實現秒殺了。這個問題我們需要考慮解決

資料庫設計

秒殺有把我們伺服器擊垮的風險,如果讓它與我們的其他業務使用在同一個資料庫中,耦合在一起,就很有可能牽連和影響其他的業務。如何防止這類問題發生,就算秒殺發生了宕機、伺服器卡死問題,也應該讓他儘量不影響線上正常進行的業務

大量請求問題

按照1.2的考慮,就算使用快取還是不足以應對短時間的高並行的流量的衝擊。如何承載這樣巨大的存取量,同時提供穩定低時延的服務保證,是需要面對的一大挑戰。我們來算一筆賬,假如使用的是redis快取,單臺redis伺服器可承受的QPS大概是4W左右,如果一個秒殺吸引的使用者量足夠多的話,單QPS可能達到幾十萬,單體redis還是不足以支撐如此巨大的請求量。快取會被擊穿,直接滲透到DB,從而擊垮mysql.後臺會將會大量報錯

秒殺系統的設計和技術方案

秒殺系統資料庫設計

針對提出的秒殺資料庫的問題,因此應該單獨設計一個秒殺資料庫,防止因為秒殺活動的高並行存取拖垮整個網站。這裡只需要兩張表,一張是秒殺訂單表

秒殺系統資料庫設計

其實應該還有幾張表,商品表:可以關聯goods_id查到具體的商品資訊,商品影象、名稱、平時價格、秒殺價格等,還有使用者表:根據使用者user_id可以查詢到使用者暱稱、使用者手機號,收貨地址等其他額外資訊,這個具體就不給出範例了。

秒殺url的設計

為了避免有程式存取經驗的人通過下單頁面url直接存取後臺介面來秒殺貨品,我們需要將秒殺的url實現動態化,即使是開發整個系統的人都無法在秒殺開始前知道秒殺的url。

具體的做法就是通過md5加密一串隨機字元作為秒殺的url,然後前端存取後臺獲取具體的url,後臺校驗通過之後才可以繼續秒殺。

秒殺頁面靜態化

將商品的描述、引數、成交記錄、影象、評價等全部寫入到一個靜態頁面,使用者請求不需要通過存取後端伺服器,不需要經過資料庫,直接在前臺使用者端生成,這樣可以最大可能的減少伺服器的壓力。

具體的方法可以使用freemarker模板技術,建立網頁模板,填充資料,然後渲染網頁

單體redis升級為叢集redis

秒殺是一個讀多寫少的場景,使用redis做快取再合適不過。不過考慮到快取擊穿問題,我們應該構建redis叢集,採用哨兵模式,可以提升redis的效能和可用性。

使用nginx

nginx是一個高效能web伺服器,它的並行能力可以達到幾萬,而tomcat只有幾百。通過nginx對映使用者端請求,再分發到後臺tomcat伺服器叢集中可以提升並行能力。

精簡sql

典型的一個場景是在進行扣減庫存的時候,傳統的做法是先查詢庫存,再去update。這樣的話需要兩個sql,而實際上一個sql我們就可以完成的。

可以用這樣的做法:update miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0;這樣的話,就可以保證庫存不會超賣並且一次更新庫存,還有注意一點這裡使用了版本號的樂觀鎖,相比較悲觀鎖,它的效能較好。

redis預減庫存

很多請求進來,都需要後臺查詢庫存,這是一個頻繁讀的場景。可以使用redis來預減庫存,在秒殺開始前可以在redis設值,比如redis.set(goodsId,100),這裡預放的庫存為100可以設值為常數),每次下單成功之後,Integer stock = (Integer)redis.get(goosId); 然後判斷sock的值,如果小於常數值就減去1;

不過注意當取消的時候,需要增加庫存,增加庫存的時候也得注意不能大於之間設定的總庫存數(查詢庫存和扣減庫存需要原子操作,此時可以藉助lua指令碼)下次下單再獲取庫存的時候,直接從redis裡面查就可以了。

介面限流

秒殺最終的本質是資料庫的更新,但是有很多大量無效的請求,我們最終要做的就是如何把這些無效的請求過濾掉,防止滲透到資料庫。限流的話,需要入手的方面很多:

前端限流

首先第一步就是通過前端限流,使用者在秒殺按鈕點選以後發起請求,那麼在接下來的5秒是無法點選(通過設定按鈕為disable)。這一小舉措開發起來成本很小,但是很有效。

同一個使用者xx秒內重複請求直接拒絕

具體多少秒需要根據實際業務和秒殺的人數而定,一般限定為10秒。具體的做法就是通過redis的鍵過期策略,首先對每個請求都從String value = redis.get(userId);如果獲取到這個

value為空或者為null,表示它是有效的請求,然後放行這個請求。如果不為空表示它是重複性請求,直接丟掉這個請求。如果有效,採用redis.setexpire(userId,value,10).value可以是任意值,一般放業務屬性比較好,這個是設定以userId為key,10秒的過期時間(10秒後,key對應的值自動為null)

令牌桶演演算法限流

介面限流的策略有很多,我們這裡採用令牌桶演演算法。令牌桶演演算法的基本思路是每個請求嘗試獲取一個令牌,後端只處理持有令牌的請求,生產令牌的速度和效率我們都可以自己限定,guava提供了RateLimter的api供我們使用。

非同步下單

為了提升下單的效率,並且防止下單服務的失敗。需要將下單這一操作進行非同步處理。最常採用的辦法是使用佇列,佇列最顯著的三個優點:非同步、削峰、解耦。這裡可以採用rabbitmq,在後臺經過了限流、庫存校驗之後,流入到這一步驟的就是有效請求。然後傳送到佇列裡,佇列接受訊息,非同步下單。下完單,入庫沒有問題可以用簡訊通知使用者秒殺成功。假如失敗的話,可以採用補償機制,重試。

服務降級

假如在秒殺過程中出現了某個伺服器宕機,或者服務不可用,應該做好後備工作。之前的部落格里有介紹通過Hystrix進行服務熔斷和降級,可以開發一個備用服務,假如伺服器真的宕機了,直接給使用者一個友好的提示返回,而不是直接卡死,伺服器錯誤等生硬的反饋。

這就是我設計出來的秒殺流程圖,當然不同的秒殺體量針對的技術選型都不一樣,這個流程可以支撐起幾十萬的流量,如果是成千萬破億那就得重新設計了。比如資料庫的分庫分表、佇列改成用kafka、redis增加叢集數量等手段。