Mongodb技巧(部分摘自深入学习mongodb,持续更新)

2012-05-16 17:37

Mongodb技巧(部分摘自深入学习mongodb,持续更新)

by snoopyxdy

at 2012-05-16 09:37:17

original http://snoopyxdy.blog.163.com/blog/static/60117440201241591250871

以下内容摘自深入学习mongodb一书另加上自己的一些实战经验。

1、关于mongodb的分片:
根据infoq上的视觉中国使用mongodb的经验,mongodb的分片要数据量达到5000万以上时才着手考虑,当应用过小或者还尚小时完全可以做双机热备,就是副本集,我们可以用代码来做负载均衡和容灾,毕竟搭建一个mongodb cluster 耗财和费时。

2、关于集群的mongos
cluster需要多个mongos来做到,mongos就是整个集群的入口,如果我们的应用服务器有10台,建议每台应用服务器都启动一个mongos,让本机的应用连接本机的mongos来访问cluster。想要做的好点,可以将整个mongos列表整出来,让应用服务器优先访问本机的mongos,如果本机monogs挂了,可以去访问其他应用服务器的mongos,不过一般mongos不会单独挂,呵呵。

3、关于片键的选择
书中列举了几个片键选择的反面例子:
a、根据大洲来做片键:世界上就5个大洲,也就是说最多只能分出来5个分片,如果压力过大,将无法新增分片服务器。
b、根据_id或者时间戳来做片键:由于片键在不断更新,所以新的记录总是会落在指定的分片服务器上,而且由于长度的一直变化,分片服务器互相移动数据会相当频繁。
好的建议,我们尽量要录入的记录平均分配到每个分片服务器,当有查询任务时,尽量减少磁盘和内从的交换,而且当有新的分片服务器加入后,最小的移动数据。
基于上面3点,我们可以将一个自增长的粗粒度key作为主片键,还有一个随机散列的key作为第二片键。比如2012-5这个年月作为主片键,用户名或邮箱名作为第二片键,如果当前时间为2012-5,则用户录入数据时分根据用户名散列的录入到各个分片服务器。当有新的分配服务器加入后,每个分配服务器都会移动一小部分到新的分配服务器。当进入2012-6月后,2012-5月份的数据录入和访问都会下降很多,所以被交换进内存的概率也小了不少,分配服务器继续根据用户名或邮箱名进行切分。

4、关于集群key的唯一性
在集群中可能存在这样的一个现象。
比如:用户注册,我们要求邮箱是唯一的,是之前其他用户没有注册过的。由于我们根据上一条,将2012-6这类的年月作为主片键,将用户名作为第二片键。应用程序将用户输入的邮箱放入mongodb集群中查找,等待返回结果是否有相同的邮箱。同时另外一个用户也是用相同的邮箱名进行了注册。
这样会出现一个问题,两个相同的邮箱用户都注册成功了。如何避免这类问题的出现呢?虽然这类问题出现的概率很小,但是我们尽量将唯一的标识,比如用户名或者邮箱作为片键,只要在一个数据块上,唯一索引就会生效了,避免了两个相同邮箱的用户了。

5、更新集群中的文档
更新mongodb集群中的某一个文档面临的唯一索引的问题和上面是一样的,所以在做update操作时,第一个参数必须是分片的片键,这样可以让mongodb找到指定的数据块

6、范式和反范式
在mysql里多表查询的速度必然比单表慢,在mongodb中也是如此,多个collection的查询也必然比单个collection慢,但是如果将原本多collection查询整合成单collection查询必然产生数据的冗余,这里就需要权衡性能和开发成本以及产生的副作用的利弊了。比如数据量更新很快的应用,可以使用反范式做数据冗余,因为很少有人将几天前的东西再拿出来反复看。当有些敏感的及时性的应用时,就必须使用范式,要保证每次用户看到的或是操作的都是最新即时的数据。
书上列举了一个购物车的例子:
用户下单以后,商品信息,用户资料等推荐使用反范式,就算以后商品价格有变动,改用户的购买记录不会有变动了,即是当时的成交价。

7、分页
分页尽量使用查询索引key条件的范围,而不要使用skip。
比如我们要查第二页的数据,我们可以将第一页的最后一条_id作为查询条件,查询大于这个_id的limit(20)的记录,假设分页每页20条记录。

8、内嵌文档
例如一些标签,简单的评论或者其他不怎么会大幅度改变的值的情况下,可以将此类数据作为父文档的内嵌文档。
但是如果评论本身就是应用的热门,可能被搜索的价格很高或者增长幅度很大时,还是建议单独建立一个collection。

9、预填充数据
一些没有数据的key尽量也先填入默认值,当要更新这些数据时,mongodb不需要为其分配空间了。

10、内嵌数组还是内嵌文档
当对顺序比较看中的,比如评论,就可以使用数组,数组还支持$eleMatch的复杂查询。另外如果知道整个数组的长度,并不是每次都去计算它,而是另外维护一个key,用来计算维护内嵌数组的length。
当表示此文档的某一个属性时,则可以使用内嵌文档,因为不必为这些属性单独设立KEY了,比如游戏数据库中的角色攻击力,敏捷力,防御力等。

11、$where
尽量少的使用$where,和find提升性能一样,尽量将第一个大幅减少数据集的查询条件放在前面。

12、GridFs
尽量不要用GridFs处理小的2进制数据,小的2进制数据可以存放在文档中。

13、索引查询
不必多说了,查询条件的第一个必须是索引

14、禁用索引
如果你的某次查询不想使用索引,可能要返回这个collection的大部分数据,只需如下写:
db.foo.find().sort({"$natural":1}) 即可

15、分级文档
在可能的情况下,尽量减少文档的扁平结构,可以加速查询,因为mongodb搜索key也是需要时间的。可以将一些不常用的,分类作为内嵌文档存入父文档。

16、and和or查询要点
and查询要将匹配最少的条件放在第一,以此类推
or查询要将匹配最多的条件放在第一,以此类推

17、安全写入
开发过程中最好执行安全的写操作,这样可以避免很多错误,以及即时发现错误。

18、索引的建立顺序
我们知道,MongoDB和传统数据库一样,都是采用B树作为索引的数据结构。对于树形的索引来说,保存热数据使用到的索引在存储上越集中,索引浪费掉的内存也越小。所以我们对比下面两种索引结构:
1、 db.metrics.ensureIndex({ metric: 1, client: 1, date: 1})
2、 db.metrics.ensureIndex({ date: 1, metric: 1, client: 1 })

采用这两种不同的结构,在插入性能上的差别也很明显。

当采用第一种结构时,数据量在2千万以下时,能够基本保持10k/s 的插入速度,而当数据量再增大,其插入速度就会慢慢降低到2.5k/s,当数据量再增大时,其性能可能会更低。

而采用第二种结构时,插入速度能够基本稳定在10k/s。

其原因是第二种结构将date字段放在了索引的第一位,这样在构建索引时,新数据更新索引时,不是在中间去更新的,只是在索引的尾巴处进行修改。那些插入时间过早的索引在后续的插入操作中几乎不需要进行修改。而第一种情况下,由于date字段不在最前面,所以其索引更新经常是发生在树结构的中间,导致索引结构会经常进行大规模的变化。


(持续更新中)