文章目錄
- 第一個問題:Key-Value資料庫可以有好多的Key,沒錯,但對MongoDB來說,大錯特錯
- 第二個問題:FindOne({_id:xxx})就快嗎?
- 第三個問題:精細的使用Update
上一篇為求振聾發聵的效果,有些口號主義,現在開始實戰,歸於實用主義。
使用情景
開始之前,我們先設定這樣一個情景:
1.一百萬註冊使用者的頁遊或者手遊,這是不溫不火的一個狀態,剛好是資料量不上不下的一個情況。也剛好是傳統MySql資料庫效能開始吃緊的時候。
2.資料庫就用一台很普通的伺服器,只有一台。讀寫分離、水平擴充、記憶體緩衝都不談。一百萬註冊使用者如果貢獻度和活躍度都不高,恐怕公司的日子還不是那麼寬裕,能夠在資料庫上的投資也有限。
以此情景為例,設每個使用者都擁有100個道具,使用者隨時會獲得或失去道具。
我們就來看看這一億的道具怎麼搞。
道具一般要使用原型、執行個體的設計方法,這個不屬於資料庫的範疇。
道具類型001 是屠龍刀,屠龍刀價格1500,基礎攻擊150,這些,我們把它們稱為道具原型,儲存在原型資料檔案中。
這個原型資料檔案,無論是存在何種資料庫或者本地檔案中,對伺服器來說都不是問題,也不干擾資料庫設計,所以我們不去討論他。
關聯式資料庫設計方法
典型的關聯式資料庫設計方法:
使用者表:欄位 xxx userid xxx ,記錄數量100萬
xxx是其他欄位,userid標示使用者
使用者道具表:欄位 xxx userid itemtype xxx ,記錄數量一億
xxx是其他欄位,userid 標示
一個億的記錄數是不是看起來有點頭疼,mysql這個時候就要想各種辦法了。
MongoDB設計方法
但我們用mongoDB來實現這個需求,直接就沒有問題
首先第一個集合:users集合,用UserName 作為_id ,記錄數100萬
然後道具的組織,我們有兩種選擇
1.在users集合的值中建立Items對象,用Bson數組儲存道具(Mongo官方稱為Bson,和Json一模一樣的儲存方法)
方法一,沒有額外的記錄數
2.建立userItems集合,同樣用UserName作為_id 每個UserItems集合的值中建立一個Item對象,使用一個Bson數組來儲存道具
方法二,多了一個集合和100萬記錄數
我們的道具資料看起來像下面這樣:
{_id:xxx,Items:[
{Itemtype:xxx,ItemPower:xxx},
...
...
...
]}
測試方法
測試方法如下:測試用戶端隨機檢查一個使用者的道具數量,小於100加一個道具,大於100 刪除一個道具。
連續100萬次,採用10個線程並發。
如果用關聯式資料庫設計方法+mysql來實現,這是一個很壓力很大的資料處理需求。
可是用文檔資料庫設計方法+MongoDB來實現,這個測試根本算不上有壓力。
注意事項
即使我們用了一個如此勝之不武的設計方式,你依然有可能還是能把他寫的很慢。
因為MongoDB在介面設計上並沒有很好的引導和約束,如果你不注意,你還是能把他用的非常慢。
第一個問題:Key-Value資料庫可以有好多的Key,沒錯,但對MongoDB來說,大錯特錯
MongoDB的索引代價很大,大到什麼程度:
1.巨大的記憶體佔用,100萬條索引約佔50M記憶體,如果這個設計中,你一個道具一條記錄,5G記憶體將用於索引。
我們的屌絲情景不可能給你這樣的伺服器,
2.巨大的效能損失,作為一個資料庫,所有的東西終將被寫入硬碟,沒有關聯式資料庫那樣的表結構,MongoDB的索引寫入效能看起來很差,如果記錄資料較小的時候,你可以觀測到這樣震撼的景象,加一個索引,效能變成了1/2,加兩個索引,效能變成了1/3。
只有當第二個索引的查詢不可避免,才值得增加額外索引。因為沒索引的資料,查詢效能是加幾個零的慢,比加索引更慘。
我們既然選擇了Key-Value資料庫,應盡量避免需要多個索引的情況。
所有的索引只能存在於記憶體中,而讀取記錄時,也需要將Bson在記憶體中處理,記憶體還承擔著更重要的作用:讀取緩衝。
本來就不充裕的記憶體,應該嚴格控制我們的記錄條數,能夠用Bson儲存的,盡量用之。
那麼我們之前在MongoDB的設計中怎麼還考慮第二種設計方法呢?獨立一個userItems 集合,不是又多出100萬條記錄了嗎?
這基於另兩個考慮:a.Bson的處理是要反覆硬碟和記憶體交換的,如果每條記錄更小,則IO壓力更小。記憶體和硬碟對伺服器來說都是稀缺資源,至於多大的資料拆分到另一個集合中更划算,這需要根據業務情況,伺服器記憶體、硬碟情況來測試出一個合適大小,我們暫時使用1024這個數值,單使用者的道具表肯定是會突破1024位元組的,所以我們要考慮將他獨立到一個集合中
b.可以不部署分區叢集,將另一個集合挪到另一個伺服器上去。只要伺服器可以輕鬆承載100萬使用者,200萬還會遠嗎?在有錢部署分區叢集以前,考慮第二組伺服器更現實一些。
第二個問題:FindOne({_id:xxx})就快嗎?
毋庸置疑,FindOne({_id:xxx})就是最直接的用Key取Value。
也的確,用Key取Value 就是我們能用的唯一訪問Value的方式,其他就不叫Key-Value資料庫了。
但是,由於我們要控制Key的數量,單個Value就會比較大。
不要被FindOne({_id:xxx}).Items[3].ItemType這優雅的代碼欺騙,這是非常慢的,他幾乎謀殺你所有的流量。
無論後面是什麼 FindOne({_id:xxx})總是返回給你完整的Value,我們的100條道具,少說也有6~8K.
這樣的查詢流量已經很大了,如果你採用MongoDB方案一設計,你的單個Value是包含一個使用者的所有資料的,他會更大。
如果查詢用戶端和資料庫伺服器不在同一個機房,流量將成為一個很大的瓶頸。
我們應該使用的查詢函數是FindOne({_id:xxx},filter),filter裡面就是設定返回的過濾條件,這會在發送給你以前就過濾掉
比如FindOne({_id:xxx},{Items:{"$slice":[3,1]}}),這和上面那條優雅的代碼是完成同樣功能,但是他消耗很少的流量
第三個問題:精細的使用Update
這和問題二相對的,不要暴力的FindOne,也盡量不要暴力的Update一整個節點。雖然MangoDB的效能挺暴力的,IO效能極限約等於MongoDB效能,暴力的Update就會在佔用流量的同時迎接IO的效能極限。
除了建立節點時的Insert或者Save之外,所有的Update都應該使用修改器精細修改.
比如Update({_id:xxx},{$set:{"Items.3.Item.Health":38}});//修改第三把武器的健康值
至於一次修改和批量修改,MongoDB預設100ms flush一次(2.x),只要兩次修改比較貼近,被一起儲存的可能性很高。
但是合并了肯定比不合并強,合并的修改肯定是一起儲存,這個也要依賴於是用的開發方式,如果使用php做資料用戶端,緩衝起來多次操作合并了一起提交,實現起來就比較複雜。
注意以上三點,一百萬註冊使用者並不算很多,4G記憶體,200G硬碟空間的MongoDB伺服器即可輕鬆應對。效能瓶頸是硬碟IO,可以很容易的使用Raid和固態硬碟提升幾倍的輸送量。不使用大量的Js計算,CPU不會成為問題,不要讓索引膨脹,記憶體不會成為問題。你根本用不著志強的一堆核心和海量的記憶體,更多的記憶體可以讓緩衝的效果更好一些,可是比讀寫分離還是差遠了。如果是高並發時查詢效能不足,就要採用讀寫分離的部署方式。當IO再次成為瓶頸時,就只能採用叢集部署MongoDB啟用分區功能,或者自行進行分集合與key散列的工作。