從MySQL和MongoDB的對比,看SQL與NoSQL的較量
貴金屬(注:貴金屬為筆者部門業(yè)務(wù))的行情系統(tǒng)提供的接口通過Redis獲取數(shù)據(jù),目前使用Redis最多只存儲了大概8000條左右的分鐘k的行情數(shù)據(jù),考慮到將來可能會有更大數(shù)據(jù)量的查詢需求,需要查詢幾月甚至幾年的行情數(shù)據(jù),要求數(shù)據(jù)庫在提供功能的同時(shí)又能保證性能和穩(wěn)定性。Redis通常只用做較小數(shù)據(jù)量的內(nèi)存數(shù)據(jù)庫,而傳統(tǒng)關(guān)系數(shù)據(jù)庫又有一定的查詢性能瓶頸,所以考慮調(diào)研一下其它的NoSQL數(shù)據(jù)庫。
一、為什么調(diào)研MongoDB?
圖1-1是DB-Engines2017年11月數(shù)據(jù)庫的排名統(tǒng)計(jì),可以看到MongoDB總排名在第5,在NoSQL數(shù)據(jù)庫中排名第1。
圖1-1 DB-Engines2017年11月數(shù)據(jù)庫的排名統(tǒng)計(jì)
優(yōu)點(diǎn):
- 社區(qū)活躍,用戶較多,應(yīng)用廣泛
- MongoDB在內(nèi)存充足的情況下數(shù)據(jù)都放入內(nèi)存且有完整的索引支持,查詢效率較高
- MongoDB的分片機(jī)制,支持海量數(shù)據(jù)的存儲和擴(kuò)展
缺點(diǎn):
- 不支持事務(wù)
- 不支持join、復(fù)雜查詢
初步調(diào)研下來,MongoDB具備我們需要的特性,而缺點(diǎn)不影響應(yīng)用場景,故接下來我們就開始做實(shí)際的性能壓測。
二、壓測性能對比
1、準(zhǔn)備條件
(1)MySQL 、MongoDB數(shù)據(jù)庫所在服務(wù)器硬件環(huán)境
表2-1 服務(wù)器硬件環(huán)境主要參數(shù)
(2)***的數(shù)據(jù)庫版本
MongoDB server version: 3.4.5
MongoDB client version: mongo-java-driver-2.14.3
MySQL server version:5.6.34
MySQL connector version: MySQL-connector-java-6.0.6
MongoDB使用的儲存引擎wiredTiger
MySQL使用的儲存引擎InnoDB
(3)數(shù)據(jù)庫表結(jié)構(gòu)及索引
MongoDB索引為dateTime 且是唯一索引。我們實(shí)際測試使用的MongoDB數(shù)據(jù)結(jié)構(gòu)及字段如圖2-1所示。
圖2-1 MongoDB數(shù)據(jù)表記錄示例
MySQL索引為DATETIME,PARTNER_ID,GOODS_ID,SCOPE且是唯一索引。我們實(shí)際測試使用的MySQL數(shù)據(jù)結(jié)構(gòu)及字段如圖2-2所示。
圖2-2 MySQL數(shù)據(jù)表記錄示例
SQL語句根據(jù)datetime字段進(jìn)行時(shí)間范圍的查詢
(4)連接池***連接數(shù)都設(shè)置為200個(gè),SQL語句調(diào)到***
2、百萬、***別的下不同查詢量不同并發(fā)量的壓測結(jié)果
數(shù)據(jù)庫表中記錄數(shù)總量在百萬、***別的壓測數(shù)據(jù)及結(jié)果如表2-2所示。
表2-2 百萬、***別的壓測數(shù)據(jù)及結(jié)果
3、億級別的下不同查詢量不同并發(fā)量的壓測結(jié)果
數(shù)據(jù)庫表中記錄數(shù)總量在億級別的壓測數(shù)據(jù)及結(jié)果如表2-3所示。
表2-3 億級別的壓測數(shù)據(jù)及結(jié)果
壓測結(jié)果分析:
- 當(dāng)每次查詢數(shù)據(jù)量在500條時(shí),無論表中數(shù)據(jù)總量千萬或者億級別,MySQL和MongoDB在100線程并發(fā)的情況下查詢性能相當(dāng),表現(xiàn)良好,平均響應(yīng)時(shí)間在500ms以內(nèi),TPS在230左右。
- 當(dāng)每次查詢數(shù)據(jù)量在5000條時(shí),表中數(shù)據(jù)總量為***別時(shí),MongoDB在50線程并發(fā)情況下查詢性能不及MySQL 的一半,100線程并發(fā)情況查詢性能都很差,平均響應(yīng)時(shí)間在4500ms左右,表中數(shù)據(jù)總量為億級別時(shí),在50個(gè)及以上的并發(fā)情況下,MongoDB和MySQL性能都較差。
在本案例簡單數(shù)據(jù)模型下時(shí)間范圍內(nèi)的等值查詢應(yīng)用場景下,MongoDB在高并發(fā)條件下的大數(shù)據(jù)量查詢性能并沒有比MySQL更好。另外還有一點(diǎn)需要注意的是,在本案例中,數(shù)據(jù)總量由***別到***別再到億級別的變化過程中,對于查詢性能的影響都不是很大,但對于查詢數(shù)據(jù)量的數(shù)倍增長卻十分敏感,所以在考量數(shù)據(jù)庫查詢性能時(shí),也要重點(diǎn)考量應(yīng)用的單次查詢量的需求。
盡管MongoDB在我們的這種應(yīng)用場景下并沒有達(dá)到預(yù)期的性能,我們也簡單地的調(diào)研了下MySQL和MongoDB對于內(nèi)存的使用機(jī)制以及一些可能影響查詢效率的內(nèi)部配置。
三、MySQL和MongoDB內(nèi)存結(jié)構(gòu)
1、InnoDB內(nèi)存使用機(jī)制
InnoDB體系結(jié)構(gòu)如圖3-1所示。
圖3-1 InnoDB體系結(jié)構(gòu)
壓測MySQL使用的是InnoDB存儲引擎,InnoDB關(guān)于查詢效率有影響的兩個(gè)比較重要的參數(shù)分別是innodb_buffer_pool_size,innodb_read_ahead_threshold。
innodb_buffer_pool_size指的是InnoDB緩沖池的大小,本例中InnoDB緩沖池大小為20G,該參數(shù)的大小可通過命令指定innodb_buffer_pool_size 20G。緩沖池使用改進(jìn)的LRU算法進(jìn)行管理,維護(hù)一個(gè)LRU列表、一個(gè)FREE列表,F(xiàn)REE列表存放空閑頁,數(shù)據(jù)庫啟動時(shí)LRU列表是空的,當(dāng)需要從緩沖池分頁時(shí),首先從FREE列表查找空閑頁,有則放入LRU列表,否則LRU執(zhí)行淘汰,淘汰尾部的頁分配給新頁。
innodb_read_ahead_threshold相對應(yīng)的是數(shù)據(jù)預(yù)加載機(jī)制,innodb_read_ahead_threshold 30表示的是如果一個(gè)extent中的被順序讀取的page超過或者等于該參數(shù)變量的,InnoDB將會異步的將下一個(gè)extent讀取到buffer pool中,比如該參數(shù)的值為30,那么當(dāng)該extent中有30個(gè)pages被sequentially的讀取,則會觸發(fā)InnoDB linear預(yù)讀,將下一個(gè)extent讀到內(nèi)存中;在沒有該變量之前,當(dāng)訪問到extent的***一個(gè)page的時(shí)候,InnoDB會決定是否將下一個(gè)extent放入到buffer pool中;可以在MySQL服務(wù)端通過show InnoDB status中的Pages read ahead和evicted without access兩個(gè)值來觀察預(yù)讀的情況:
Innodb_buffer_pool_read_ahead:表示通過預(yù)讀請求到buffer pool的pages;
Innodb_buffer_pool_read_ahead_evicted:表示由于請求到buffer pool中沒有被訪問,而驅(qū)逐出內(nèi)存的頁數(shù)。
可以看出來,MySQL的緩沖池機(jī)制是能充分利用內(nèi)存且有預(yù)加載機(jī)制,在某些條件下目標(biāo)數(shù)據(jù)完全在內(nèi)存中,也能夠具備非常好的查詢性能。
2、MongoDB的存儲結(jié)構(gòu)及數(shù)據(jù)模型
(1)本例中MongoDB使用的儲存引擎是WiredTiger,WiredTiger的結(jié)構(gòu)如圖3-2所示。
圖3-2 WiredTiger Engine的結(jié)構(gòu)
WiredTiger Cache的實(shí)現(xiàn)原理圖如圖3-3所示。
圖3-3 WiredTiger Cache的實(shí)現(xiàn)原理圖
Wiredtiger的Cache采用Btree的方式組織,每個(gè)Btree節(jié)點(diǎn)為一個(gè)page,root page是btree的根節(jié)點(diǎn),internal page是btree的中間索引節(jié)點(diǎn),leaf page是真正存儲數(shù)據(jù)的葉子節(jié)點(diǎn);btree的數(shù)據(jù)以page為單位按需從磁盤加載或?qū)懭氪疟P。
可以通過在配置文件中指定storage.wiredTiger.engineConfig.cacheSizeGB參數(shù)設(shè)定引擎使用的內(nèi)存量。此內(nèi)存用于緩存工作集數(shù)據(jù)(索引、namespace,未提交的write,query緩沖等)。
(2)數(shù)據(jù)模型
內(nèi)嵌
MongoDB的文檔是無模式的,所以可以支持各種數(shù)據(jù)結(jié)構(gòu),內(nèi)嵌模型也叫做非規(guī)格化模型(denormalized)。在MongoDB中,一組相關(guān)的數(shù)據(jù)可以是一個(gè)文檔,也可以是組成文檔的一部分。
圖3-4 內(nèi)嵌文檔示例
內(nèi)嵌類型支持一組相關(guān)的數(shù)據(jù)存儲在一個(gè)文檔中,這樣的好處就是,應(yīng)用程序可以通過比較少的的查詢和更新操作來完成一些常規(guī)的數(shù)據(jù)的查詢和更新工作。
當(dāng)遇到以下情況的時(shí)候,我們應(yīng)該考慮使用內(nèi)嵌類型:
如果數(shù)據(jù)關(guān)系是一種一對一的包含關(guān)系,例如下面的文檔,每個(gè)人都有一個(gè)contact字段來描述這個(gè)人的聯(lián)系方式。像這種一對一的關(guān)系,使用內(nèi)嵌類型可以很方便的進(jìn)行數(shù)據(jù)的查詢和更新。
- {
- ”_id”: ,
- ”name”: “Wilber”,
- ”contact”: {
- “phone”: “12345678”,
- “email”: “wilber@shanghai.com”
- }
- }
如果數(shù)據(jù)的關(guān)系是一對多,那么也可以考慮使用內(nèi)嵌模型。例如下面的文檔,用posts字段記錄所有用戶發(fā)布的博客。在這中情況中,如果應(yīng)用程序會經(jīng)常通過用戶名字段來查詢改用戶發(fā)布的博客信息。那么,把posts作為內(nèi)嵌字段會是一個(gè)比較好的選擇,這樣就可以減少很多查詢的操作。
- {
- “_id”: ,
- “name”: “Wilber”,
- “contact”: {
- ”phone”: “12345678”,
- ”email”: “wilber@shanghai.com”
- },
- ”posts”: [
- {
- ”title”: “Indexes in MongoDB”,
- ”created”: “12/01/2014”,
- ”link”: “www.linuxidc.com”
- },
- {
- ”title”: “Replication in MongoDB”,
- ”created”: “12/02/2014”,
- ”link”: “www.linuxidc.com”
- },
- {
- ”title”: “Sharding in MongoDB”,
- ”created”: “12/03/2014”,
- ”link”: “www.linuxidc.com”
- }
- ]
- }
根據(jù)上面的描述可以看出,內(nèi)嵌模型可以給應(yīng)用程序提供很好的數(shù)據(jù)查詢性能,因?yàn)榛趦?nèi)嵌模型,可以通過一次數(shù)據(jù)庫操作得到所有相關(guān)的數(shù)據(jù)。同時(shí),內(nèi)嵌模型可以使數(shù)據(jù)更新操作變成一個(gè)原子寫操作。然而,內(nèi)嵌模型也可能引入一些問題,比如說文檔會越來越大,這樣就可能會影響數(shù)據(jù)庫寫操作的性能,還可能會產(chǎn)生數(shù)據(jù)碎片(data fragmentation)。
引用
相對于嵌入模型,引用模型又稱規(guī)格化模型(Normalized data models),通過引用的方式來表示數(shù)據(jù)之間的關(guān)系。這里同樣使用來自MongoDB文檔中的圖片,在這個(gè)模型中,把contact和access從user中移出,并通過user_id作為索引來表示它們之間的聯(lián)系。
圖3-5 引用文檔示例
當(dāng)我們遇到以下情況的時(shí)候,就可以考慮使用引用模型了:
- 使用內(nèi)嵌模型往往會帶來數(shù)據(jù)的冗余,卻可以提升數(shù)據(jù)查詢的效率。但是,當(dāng)應(yīng)用程序基本上不通過內(nèi)嵌模型查詢,或者說查詢效率的提升不足以彌補(bǔ)數(shù)據(jù)冗余帶來的問題時(shí),我們就應(yīng)該考慮引用模型了。
- 當(dāng)需要實(shí)現(xiàn)復(fù)雜的多對多關(guān)系時(shí),可以考慮引用模型。比如我們熟知的例子,學(xué)生-課程-老師關(guān)系,如果用引用模型來實(shí)現(xiàn)三者的關(guān)系,可能會比內(nèi)嵌模型更清晰直觀,同時(shí)會減少很多冗余數(shù)據(jù)。
- 當(dāng)需要實(shí)現(xiàn)復(fù)雜的樹形關(guān)系時(shí),可以考慮引用模型。
四、應(yīng)用場景分析
1、MongoDB的應(yīng)用場景
(1)表結(jié)構(gòu)不明確且數(shù)據(jù)不斷變大
MongoDB是非結(jié)構(gòu)化文檔數(shù)據(jù)庫,擴(kuò)展字段很容易且不會影響原有數(shù)據(jù)。內(nèi)容管理或者博客平臺等,例如圈子系統(tǒng)、存儲用戶評論之類的。
(2)更高的寫入負(fù)載
MongoDB側(cè)重高數(shù)據(jù)寫入的性能,而非事務(wù)安全,適合業(yè)務(wù)系統(tǒng)中有大量“低價(jià)值”數(shù)據(jù)的場景。本身存的就是json格式數(shù)據(jù)。例如做日志系統(tǒng)。
(3)數(shù)據(jù)量很大或者將來會變得很大
MySQL單表數(shù)據(jù)量達(dá)到5-10G時(shí)會出現(xiàn)明細(xì)的性能降級,需要做數(shù)據(jù)的水平和垂直拆分、庫的拆分完成擴(kuò)展,MongoDB內(nèi)建了sharding、很多數(shù)據(jù)分片的特性,容易水平擴(kuò)展,比較好的適應(yīng)大數(shù)據(jù)量增長的需求。
(4)高可用性
自帶高可用,自動主從切換(副本集)
不適用的場景
(1)MongoDB不支持事務(wù)操作,需要用到事務(wù)的應(yīng)用建議不用MongoDB。
(2)MongoDB目前不支持join操作,需要復(fù)雜查詢的應(yīng)用也不建議使用MongoDB。
2、關(guān)系型數(shù)據(jù)庫和非關(guān)系型數(shù)據(jù)庫的應(yīng)用場景對比
關(guān)系型數(shù)據(jù)庫適合存儲結(jié)構(gòu)化數(shù)據(jù),如用戶的帳號、地址:
- 這些數(shù)據(jù)通常需要做結(jié)構(gòu)化查詢,比如join。這時(shí)候,關(guān)系型數(shù)據(jù)庫就要勝出一籌
- 這些數(shù)據(jù)的規(guī)模、增長的速度通常是可以預(yù)期的
- 事務(wù)性、一致性
NoSQL適合存儲非結(jié)構(gòu)化數(shù)據(jù),如文章、評論:
- 這些數(shù)據(jù)通常用于模糊處理,如全文搜索、機(jī)器學(xué)習(xí)
- 這些數(shù)據(jù)是海量的,而且增長的速度是難以預(yù)期的
- 根據(jù)數(shù)據(jù)的特點(diǎn),NoSQL數(shù)據(jù)庫通常具有***(至少接近)伸縮性
- 按key獲取數(shù)據(jù)效率很高,但是對join或其它結(jié)構(gòu)化查詢的支持就比較差