高並發秒殺系統下的個性化問題解決

  • 2019 年 10 月 4 日
  • 筆記

前面文章整體介紹了秒殺系統的設計架構原則,在高並發秒殺系統架構下還存在一些個性化問題需要解決。

個性化問題解決

JVM在大流量系統下的表現

Java和通用的Web伺服器相比(Nginx或Apache)在處理大並發HTTP請求時要弱一點,所以一般我們都會對大流量的Web系統做靜態化改造,讓大部分請求和數據直接在Nginx伺服器或者Web代理伺服器(Varnish、Squid等)上直接返回(可以減少數據的序列化與反序列化),不要將請求落到Java層上,讓Java層只處理很少數據量的動態請求,當然針對這些請求也有一些優化手段可以使用:

  • 直接使用Servlet處理請求。避免使用傳統的MVC框架也許能繞過一大堆複雜且用處不大的處理邏輯,節省個1ms時間,當然這個取決於你對MVC框架的依賴程度。
  • 直接輸出流數據。使用resp.getOutputStream()而不是resp.getWriter()可以省掉一些不變字元數據編碼,也能提升性能;還有數據輸出時也推薦使用JSON而不是模板引擎(一般都是解釋執行)輸出頁面。

熱點商品大並發讀處理

你會說這個問題很容易解決,無非放到Tair快取裡面就行,集中式Tair快取為了保證命中率,一般都會採用一致性Hash,所以同一個key會落到一台機器上,雖然我們的Tair快取機器單台也能支撐30w/s的請求,但是像大秒這種級別的熱點商品還遠不夠,那如何徹底解決這種單點瓶頸?答案是採用應用層的Localcache,即在秒殺系統的單機上快取商品相關的數據,如何cache數據?也分動態和靜態:

  • 像商品中的標題和描述這些本身不變的會在秒殺開始之前全量推送到秒殺機器上並一直快取直到秒殺結束。
  • 像庫存這種動態數據會採用被動失效的方式快取一定時間(一般是數秒),失效後再去Tair快取拉取最新的數據。

你可能會有疑問,像庫存這種頻繁更新數據一旦數據不一致會不會導致超賣?其實這就要用到我們前面介紹的讀數據分層校驗原則了,讀的場景可以允許一定的臟數據,因為這裡的誤判只會導致少量一些原本已經沒有庫存的下單請求誤認為還有庫存而已,等到真正寫數據時再保證最終的一致性。這樣在數據的高可用性和一致性做平衡來解決這種高並發的數據讀取問題。

熱點數據大並發更新

解決大並發讀問題採用Localcache和數據的分層校驗的方式,但是無論如何像減庫存這種大並發寫還是避免不了,這也是秒殺這個場景下最核心的技術難題。

同一數據在資料庫里肯定是一行存儲(MySQL),所以會有大量的執行緒來競爭InnoDB行鎖,當並發度越高時等待的執行緒也會越多,TPS會下降RT會上升,資料庫的吞吐量會嚴重受到影響。說到這裡會出現一個問題,就是單個熱點商品會影響整個資料庫的性能,就會出現我們不願意看到的0.01%商品影響99.99%的商品,所以一個思路也是要遵循前面介紹第一個原則進行隔離,把熱點商品放到單獨的熱點庫中。但是無疑也會帶來維護的麻煩(要做熱點數據的動態遷移以及單獨的資料庫等)。

分離熱點商品到單獨的資料庫還是沒有解決並發鎖的問題,要解決並發鎖有兩層辦法。

  • 應用層做排隊。按照商品維度設置隊列順序執行,這樣能減少同一台機器對資料庫同一行記錄操作的並發度,同時也能控制單個商品佔用資料庫連接的數量,防止熱點商品佔用太多資料庫連接。
  • 資料庫層做排隊。應用層只能做到單機排隊,但應用機器數本身很多,這種排隊方式控制並發仍然有限,所以如果能在資料庫層做全局排隊是最理想的,淘寶的資料庫團隊開發了針對這種MySQL的InnoDB層上的patch,可以做到資料庫層上對單行記錄做到並發排隊。

你可能會問排隊和鎖競爭不要等待嗎?有啥區別?

如果熟悉MySQL會知道,InnoDB內部的死鎖檢測以及MySQL Server和InnoDB的切換會比較耗性能,淘寶的MySQL核心團隊還做了很多其他方面的優化,如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的patch,配合在SQL裡面加hint,在事務里不需要等待應用層提交COMMIT而在數據執行完最後一條SQL後直接根據TARGET_AFFECT_ROW結果提交或回滾,可以減少網路的等待時間(平均約0.7ms)。

據我所知,目前阿里MySQL團隊已將這些patch及提交給MySQL官方評審。

寫在最後

以秒殺這個典型系統為代表的熱點問題總結了些通用原則:

隔離、動態分離、分層校驗,必須從整個全鏈路來考慮和優化每個環節,除了優化系統提升性能,做好限流和保護也是必備的功課。

除去前面介紹的這些熱點問題外,還有多種其他數據熱點問題:

  • 數據訪問熱點,比如Detail中對某些熱點商品的訪問度非常高,即使是Tair快取這種Cache本身也有瓶頸問題,一旦請求量達到單機極限也會存在熱點保護問題。有時看起來好像很容易解決,比如說做好限流就行,但你想想一旦某個熱點觸發了一台機器的限流閥值,那麼這台機器Cache的數據都將無效,進而間接導致Cache被擊穿,請求落地應用層資料庫出現雪崩現象。這類問題需要與具體Cache產品結合才能有比較好的解決方案,這裡提供一個通用的解決思路,就是在Cache的client端做本地Localcache,當發現熱點數據時直接Cache在client里,而不要請求到Cache的Server。
  • 數據更新熱點,更新問題除了前面介紹的熱點隔離和排隊處理之外,還有些場景,如對商品的lastmodifytime欄位更新會非常頻繁,在某些場景下這些多條SQL是可以合併的,一定時間內只執行最後一條SQL就行了,可以減少對資料庫的update操作。另外熱點商品的自動遷移,理論上也可以在數據路由層來完成,利用前面介紹的熱點實時發現自動將熱點從普通庫里遷移出來放到單獨的熱點庫中。

按照某種維度建的索引產生熱點數據,比如實時搜索中按照商品維度關聯評價數據,有些熱點商品的評價非常多,導致搜索系統按照商品ID建評價數據的索引時記憶體已經放不下,交易維度關聯訂單資訊也同樣有這些問題。

這類熱點數據需要做數據散列,再增加一個維度,把數據重新組織。