本章以京東商品詳情頁為例,京東商品詳情頁雖然僅是單個頁面,但是其數據聚合源是非常多的,除了一些實時性要求比較高的如價格、庫存、服務支持等通過AJAX異步加載加載之外,其他的數據都是在后端做數據聚合然后拼裝網頁模板的。
如圖所示,商品頁主要包括商品基本信息(基本信息、圖片列表、顏色/尺碼關系、擴展屬性、規格參數、包裝清單、售后保障等)、商品介紹、其他信息(分類、品牌、店鋪【第三方賣家】、店內分類【第三方賣家】、同類相關品牌)。更多細節此處就不闡述了。
整個京東有數億商品,如果每次動態獲取如上內容進行模板拼裝,數據來源之多足以造成性能無法滿足要求;最初的解決方案是生成靜態頁,但是靜態頁的最大的問題:1、無法迅速響應頁面需求變更;2、很難做多版本線上對比測試。如上兩個因素足以制約商品頁的多樣化發展,因此靜態化技術不是很好的方案。
通過分析,數據主要分為四種:商品頁基本信息、商品介紹(異步加載)、其他信息(分類、品牌、店鋪等)、其他需要實時展示的數據(價格、庫存等)。而其他信息如分類、品牌、店鋪是非常少的,完全可以放到一個占用內存很小的Redis中存儲;而商品基本信息我們可以借鑒靜態化技術將數據做聚合存儲,這樣的好處是數據是原子的,而模板是隨時可變的,吸收了靜態頁聚合的優點,彌補了靜態頁的多版本缺點;另外一個非常嚴重的問題就是嚴重依賴這些相關系統,如果它們掛了或響應慢則商品頁就掛了或響應慢;商品介紹我們也通過AJAX技術惰性加載(因為是第二屏,只有當用戶滾動鼠標到該屏時才顯示);而實時展示數據通過AJAX技術做異步加載;因此我們可以做如下設計:
接收商品變更消息,做商品基本信息的聚合,即從多個數據源獲取商品相關信息如圖片列表、顏色尺碼、規格參數、擴展屬性等等,聚合為一個大的JSON數據做成數據閉環,以key-value存儲;因為是閉環,即使依賴的系統掛了我們商品頁還是能繼續服務的,對商品頁不會造成任何影響;
接收商品介紹變更消息,存儲商品介紹信息;
介紹其他信息變更消息,存儲其他信息。
整個架構如下圖所示:
技術選型
MQ可以使用如Apache ActiveMQ;
Worker/動態服務可以通過如Java技術實現;
RPC可以選擇如alibaba Dubbo;
KV持久化存儲可以選擇SSDB(如果使用SSD盤則可以選擇SSDB+RocksDB引擎)或者ARDB(LMDB引擎版);
緩存使用Redis;
SSDB/Redis分片使用如Twemproxy,這樣不管使用Java還是Nginx+Lua,它們都不關心分片邏輯;
前端模板拼裝使用Nginx+Lua;
數據集群數據存儲的機器可以采用RAID技術或者主從模式防止單點故障;
因為數據變更不頻繁,可以考慮SSD替代機械硬盤
核心流程
首先我們監聽商品數據變更消息;
接收到消息后,數據聚合Worker通過RPC調用相關系統獲取所有要展示的數據,此處獲取數據的來源可能非常多而且響應速度完全受制於這些系統,可能耗時幾百毫秒甚至上秒的時間;
將數據聚合為JSON串存儲到相關數據集群;
前端Nginx通過Lua獲取相關集群的數據進行展示;商品頁需要獲取基本信息+其他信息進行模板拼裝,即拼裝模板僅需要兩次調用(另外因為其他信息數據量少且對一致性要求不高,因此我們完全可以緩存到Nginx本地全局內存,這樣可以減少遠程調用提高性能);當頁面滾動到商品介紹頁面時異步調用商品介紹服務獲取數據;
如果從聚合的SSDB集群/Redis中獲取不到相關數據;則回源到動態服務通過RPC調用相關系統獲取所有要展示的數據返回(此處可以做限流處理,因為如果大量請求過來的話可能導致服務雪崩,需要采取保護措施),此處的邏輯和數據聚合Worker完全一樣;然后發送MQ通知數據變更,這樣下次訪問時就可以從聚合的SSDB集群/Redis中獲取數據了。