這段彷佛都成我寫blog標準開頭。言歸正轉,公司之前業務涉及到秒殺,而且是白天從10點起到晚上10點每小時一次(TT每天心驚肉跳的),週六還有個大禮包活動(重量級,常常會出一些你意想不到的事情,例如不活躍的用戶忽然間活躍了,量級飆升TT)。同時,最近隨着創業的興起,仍是有不少人關注秒殺這技術怎麼作。雖然不少NB的大廠(小米,淘寶,JD等)已經講過這東西了,可是我仍是想講講這件事情。下面我就說說一個小廠是如何作秒殺的。 前端
後端只有2個研發工程師和2個前端工程師,當時尚未全職的運維,不過服務器的數量有40多臺(仍是挺多的)。用戶量呢,下載和註冊都在千萬級別了,活躍也在百萬級別。好了,小廠很小,可是小廠也很大。 算法
不少人感受,敢用初出茅廬這標題,應該很牛吧,然而並無。而且是意想不到的慘,慘不忍睹。第一個版本的秒殺系統,徹底是依賴MySQL的事務,不言而喻,你們都會知道有多慘。我直接告訴你們結果就能夠了: 數據庫
整個系統在秒殺期間基本上停擺了,500和超時異常的多。 後端
準備秒殺的產品數量是100,最後賣出去了400份。 緩存
咱們來分析下爲何會這樣: 服務器
MySQL自己能承載連接數量有限,在秒殺的時候大量的連接處在事務狀態,且絕大部分事務是須要回滾的,這就形成了很大的IO壓力和計算壓力 前端工程師
那爲何會超賣呢,由於最開始使用的主從結構,讀寫是分離的,主庫壓力那麼大,從庫同步跟不上,形成了賣出去的產品在毫秒級內再查詢結果看起來就是沒賣出去。簡而言之就是就是技術不熟悉致使設計失誤。 運維
出第一次事故的時候,說句內心話,對一個剛畢業1年的工程師仍是挺蒙,而後就各類猜測。不過好在當時淘寶的一我的的blog上提了MySQL句事務的問題,算是找到方向了。而後就這樣,秒殺活動就先暫停了一個星期,這個星期中我和同事都作了什麼呢? 性能
搭建了一個測試環境,模擬了下秒殺的狀況,觀察了MySQL的事務和主從的總體狀況 測試
修改秒殺流程
我先說下初版的流程:
從用戶數據庫查詢用戶積分是否充足,從規則數據庫中查詢用戶是否符合條件
從數據庫中讀出一個產品的ID
而後事務性的將產品ID和用戶ID關聯,減小用戶積分和更新用戶規則數據,更新產品ID的狀態
那麼問題就明顯了,讀產品ID的時候是沒有事務的,這必然會存在問題的。那麼咱們是如何修改的呢?將讀取產品ID這件事放入了整個事務中。那麼整個流程就變成了:
從用戶數據庫查詢用戶積分是否充足,從規則數據庫中查詢用戶是否符合條件
事務性的讀出符合條件的產品,並馬上更新狀態,接着完成用戶ID和產品ID的關聯及減小積分等工做
那這樣還有問題嗎?依然有,最後仍是超賣了,你們會問爲何?這裏面咱們犯了另外一個錯誤,使用代碼判斷產品的狀態而非存儲過程,這樣即使是在數據庫事務內,但沒有能夠觸發數據庫事務回滾的條件,因此還會錯誤的將賣出的產品再次更新爲賣出的狀態。經歷兩次慘痛的教訓,咱們才逐步的走上正軌,一個地方不會跌倒三次。
咱們已經發現了不少問題,最後該怎麼解決,咱們決定先解決正確性,再解決速度的問題,咱們使用了一段時間的存儲過程加關鍵ID作成惟一主鍵的方式,整個秒殺流程的第二部分,就是個完整的存儲過程(往事不堪回首,每天被用戶罵很是慢)。這個時候惟一能作的就是補充理論知識,發奮圖強了。
在這個第二個版本的設計中,咱們開始採用Redis,咱們測試了Redis的pubsub機制,最開始想使用Redis的pubsub進行排隊(如今想一想有點幼稚,可是老天幫了我一把,當時鬼使神差的就感受這機制不靠譜)。可是最終的方案嗯,使用了正向隊列。何爲正向隊列?咱們將產品的ID在秒殺開始前,所有讀入指定的隊列中,秒殺流程就變成了:
判讀Redis隊列是否爲0,爲0結束
判讀用戶是否符合規則,是否有足夠多的積分
從隊列pop出一個產品ID,若是pop不出來就結束
開事務,改變產品ID的狀態,關聯用戶ID和產品ID,更新規則和積分
這個時候基本上完全解決了超賣和性能的問題了,可是還會有用戶在罵,爲何?由於還不夠快。
咱們發現爲何會慢,由於數據庫的事務,回滾雖然少了,可是仍是處理不過來,1s也就那100多個事務能完成,剩下的各類跟不上。此時此刻,咱們直接採購了當時算是比較強勁的數據庫服務器,事務量一下提升到了1000tps。可是這遠遠跟不上用戶的增加速度(TT沒業務也哭,有業務也哭)。
咱們既然已經發現了排隊理論這麼有用,咱們決定使用RabbitMQ,延遲處理隊列。通過此次改造,咱們秒殺的流程就變成了:
判斷Redis隊列是否爲0,爲0結束
判讀用戶是否符合規則,是否有足夠積分
從隊列pop出一個產品ID,若是pop不出來就結束
將用戶ID和產品ID放入RabbitMQ中,後面的消費者慢慢的吞下去
這時候用戶在速度上算是基本滿意了,不過卻帶來了新的問題。判斷用戶是否符合規則的時候,因爲消費者慢慢的消化而數據庫沒有實時的更新,致使一個用戶能夠秒殺多個商品,不少用戶就不滿意了(TT用戶是上帝)。
咱們再次拿出了強大的Redis,咱們將Redis看成緩存。咱們把秒殺的業務邏輯直接變成了這樣:
先判斷Redis的隊列是否爲0,爲0結束
判斷Redis中用戶的信息是否符合規則,積分是否符合規則
從隊列pop出一個產品ID,若是pop不出來就馬上結束
馬上更新Redis中用戶的緩存信息和積分信息,再放入RabbitMQ,讓消費者消費
這樣看起起來彷佛沒什麼問題了,可是仍是存在問題的,就是pop出產品ID到更新Redis用戶信息的一瞬間仍是能讓部分用戶鑽空子的,畢竟Redis沒有MySQL那種強事務機制。
在這個階段,咱們用Erlang的mnesia寫了一個Redis特定功能替代品,但使用了段時間很快放棄了,由於咱們找到了更好的解決方式。讓RabbitMQ的消費者使用一致性的hash,那麼特定的用戶必定會落到特定的消費者身上,消費者作去重判斷。這樣減小了,咱們本身維護基礎軟件的成本(2個後端工程師TT,別瞎折騰)。
當咱們的用戶量逐步上升,系統依然出現吃緊和性能跟不上的階段。
這個時候,咱們大量使用一致性Hash和隨機算法,其中過程就變成了。
將秒殺的產品ID分紅多個隊列放在Redis集羣上,而後將一個產品總數量放在一個Redis上(這個Redis是瓶頸,可是基本上20W的TPS滿滿的達到了)
爲用戶隨機一個數字,在必定範圍內,直接告訴秒殺失敗(純看運氣,純丟給應用服務器去玩了)
檢查用戶規則和用戶積分,還有產品總數量,總數量爲0,直接結束。
爲用戶隨機一個產品ID隊列,嘗試pop,pop不出數據,直接結束(仍是看運氣)
更新用戶Redis的緩存和產品總數量的緩存(decr),而後交給RabbitMQ和消費者慢慢處理。
這個時候,基本上30wTPS,隨便玩。
說了這麼多廢話,總結下吧。對於秒殺這種業務,優先保穩定和正確,最後才能保服務量。不穩定沒得玩,不正確,極可能一單虧死。技術上,我我的認爲小廠也能作看似很NB的秒殺只要用好如下幾個相關技術:
削峯,不論是隨機丟棄,仍是多層篩選,儘量減小進入核心業務的用戶數
排隊,在秒殺場景下,排隊不僅僅能夠減小系統壓力,還能保證正確性
分區,使用分區能夠下降一個節點當機帶來總體性的損害或者雪崩性的系統不可用
最終一致,不少時候,不必定要強一致性,只要能保證最後數據的正確,哪怕是手工修復,都能帶來大規模的性能提高