拆分大文档
很常见的一种优化手段,在一些特定的业务场景中,会有一些很大的文档,这些文档有很多字段,而且有一些特定的字段还特别的大。可以考虑拆分这些文档
大文档对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 这个参数