拆分大文檔
很常見的一種優化手段,在一些特定的業務場景中,會有一些很大的文檔,這些文檔有很多字段,而且有一些特定的字段還特別的大。可以考慮拆分這些文檔
大文檔對MongoDB的性能影響還是很大的,就我個人經驗而言,認為可以考慮從兩個角度出發拆分大文檔:
- 按照字段的訪問頻率拆分: 訪問頻繁的放一個文檔,訪問不頻繁的拆出去作為另一個文檔
- 按照字段的大小來劃分: 小字段放一個文檔,大字段拆除去作為另外一個文檔
之前拆分過一個文檔,非常龐大。而且在業務中,有一些龐大的字段根本用不上,在這種情況下,一次拆除了三個文檔。
- 訪問頻繁的小字段放在一起,作為一個文檔
- 把訪問不頻繁的大字段拆出去作為一個文檔。可以進一步優化為特定的巨大的字段可以直接作為一個文檔
- 剩余的合并在一起作為一個文檔
這樣做的優點很明顯,比較多的業務查詢其實只需要第一種文檔,極少數會需要第二種文檔;但是缺點也很明顯,如果調用者需要整個文檔,也就意味著需要查詢三次,再合并組成一個業務上完整的文檔。
也可以升華一下:這種拆分終究是下策,最好還是在一開始使用MongoDB的時候就約束住文檔的大小。
不過還有一個和這種策略完全相反的優化手段:嵌入文檔
嵌入文檔
如果A文檔和B文檔有關聯關系,那么就在A文檔里面嵌入B文檔,做成一個大文檔。
相當于原本A文檔和B文檔都是單獨存儲的,可能A文檔里面有一個B文檔的ID字段,又或者B文檔里有A文檔的ID字段,可以考慮合并這兩個文檔
可以這么介紹你的方案:
早期有一個過度設計的場景,就是有兩個文檔A和B,其中A里面有一個B的文檔ID,建立了一對一的映射關系。但是實際上,業務查詢的時候,基本上是分兩次查詢的,先把A查詢出來,再根據A里面的文檔ID也把B查出來。
后來這個地方慢慢成為了性能瓶頸,我就嘗試優化了這個地方。我的想法是既然A和B在業務上聯系那么緊密,我可以直接把他們整合成一個文檔。整合之后,一次查詢就能拿到所有需要的數據了,直接節約了一個MongoDB查詢,提高了業務的響應時間,而且MongoDB的壓力也變小了。
如果面試官問怎么直接整合成一個文檔呢?
采用的是懶惰的、漸進式的整合方案。如果我先查詢A文檔之后發現A文檔還沒有嵌入B文檔,那么就查詢B文檔,嵌入進A文檔之后,直接更新A文檔。在更新A文檔的時候,要采用樂觀鎖策略,也就是在更新的條件里,加上A文檔不包含B文檔這個條件。
這個業務有一個好處是,沒有直接更新B文檔的場景,都是通過A來操作B文檔,所以不需要考慮其他的并發問題
這種懶惰更新策略里的最后一步更新動作,實際上就是一個樂觀鎖。所以也可以嘗試把話題引導到樂觀鎖上。
不過,嵌入整個文檔是很罕見的優化手段。更加常見的是嵌入部分字段,也叫做冗余字段。這種優化手段在關系型數據庫里也很常見,比如A經常使用B的某幾個字段,那么就可以在A里面冗余一份。但是這種冗余的方案會有比較嚴重的數據一致性問題,只有在你能夠容忍這種數據不一致的時候,才可以應用這個方案。
在現實中最常見的場景就是在別的模塊的文檔里冗余用戶的昵稱、頭像,這樣可以避免再次去用戶文檔里查詢昵稱或頭像。畢竟這兩個在很多時候都不是什么關鍵字段。
操作系統優化
前面基本都是查詢本身的優化,也可以準備一些操作系統優化的點。
內存優化
在MongoDB里,索引對性能的影響很大,所以應該盡可能保證有足夠的物理內存來放所有的索引。
swap
同樣需要避免觸發交換,可以調小vm.swappiness 這個參數