为什么MongoDB采用B树索引,而Mysql用B+树做索引
MongoDB的高可用性,弱/强一致性(可配置)
区分大小写;区分类型(3和"3"是不同的value); key-value是有序的;值可以是null; 数组里可以是不同类型的元素;
ObjectId前4个字节是秒,所以"大致"会按创建时间顺序排序的;
同类型的数据放到一个集合中,有利于查询速度快,磁盘存放更紧密,建索引效率更高;
设计原则:能交给客户端驱动做的事,就不交给服务器实现;因为扩展客户端比扩展服务器要容易很多;
mongoexport导出json或csv格式的数据;
mongodbimport适合初始导入数据(比batchInsert还要快)
使用_id来查找,速度快,因为建立了索引;
update更新文档时,可以用新文档替换老文档(注意_id字段),也可以用$set更新某些字段;用“author.name”的点来指定嵌套文档里的字段;
数组值,可以$push往数组末尾增加元素,用$slice限制最多只包含最后添加的N个元素;$each是一次添加多个元素;
往数组插入的时候,使用$ne或者$addToSet限制不插入重复元素,可以把数组当集合使用;
数组支持$pop从头或者从尾删除一个元素;tags.2.weight指定tags数组第2号元素的weight字段;update条件里选定了某个数组元素后,在更新语句里可以用tags.$.weight来指定这个元素;
collection里的doc修改后,可能变大,原来的空间装不下它了,就会被移动到文件末尾,导致出现空隙;find出来的顺序也会发生变化;还会改变“填充因子”,导致后面的新增或者新移动的文档都会预留更多的空间;之后的插入中如果不再发生移动,"填充因子"会逐渐变小;可以用参数指定该collection的所有空间分配都是2的幂次,适合反复修改的情况;
upsert(update & insert): update时如果发现满足条件的文档不在里面,就新创建一个文档;(原子操作)
常用技巧:find时用{"_id":0}把_id过滤掉;find时指定{"grade":null}会把grade是null和没有grade字段的文档都返回;用{"grade":{"$in":[null], "$exists":true}}来只返回有grade字段且为null的;
数组查询,只要有任何一个元素匹配查询条件,就返回整个文档;
$where里面可以使用代码,功能最强,开销也最大;
find()返回cursor,从而允许链式调用,比如db.poi.find().sort({"score":-1}).limit(10).skip(500); 真正调cursor.next()的时候才会查询数据库获取一批数据,然后后面好多次cursor.next()在这批数据上往后走,用完这批后再去查询数据库;
sort, limit, skip结合起来用,可以实现分页功能;skip大量文档等于浪费资源,优化:用上次最后一条的某个字段做bar,这次只查询大于(或小于)这个bar的文档;
随机选取文档:可以给每个文档赋一个随机数字段,查询的使用查大于某个随机数的一个文档即可;(也可能一个也查不到,多试几次)
find()之后,循环里,next()然后修改文档然后save,容易造成改动后的文档大于槽空间而导致move到尾部,从而find最后遍历了这些move的文档;解决方法:snapshot(会使查询变慢)
用了索引可以精确跳转,不用索引就要全表扫描;
.explain()可以查看查询的具体执行计划、使用了哪些索引、执行耗时,很好用!db.poi.getIndexes()查看表上已建立的索引;
索引的优点:查询变快(当从一个大数据集里查一个小数据集时,威力提现;如果要查出的结果数据集大,则没啥用了,这时还不如全表扫描快(只查文档),索引还得查索引和文档);索引的缺点:写操作会耗费更多时间(因为不仅要更新文档还要更新索引)
多字段的复合索引,哪个字段应该靠前,很有学问:db.table.find({"age":{某个范围}}).sort({"name":1}), 用{"age":1, "name":1}索引会先定位到age范围区间,把这个子集读到内存按name排序, 用{"name":1, "age":1}索引会全表扫描;不加条数限制的时候,前者快;加limit限制条数的时候,后者快(sort之后往往只需要看前面的);;精确匹配的字段放前面,范围匹配的字段放后面,可以减少遍历的数目;
单字段索引时,1和-1方向的索引是等价的,因为可以从后往前遍历;多字段索引是,会不同;
covered index: 如果这次查询只用了索引里的字段,则速度非常快;因为不需要读磁盘上的文档了;(索引一般是驻留内存的)
能用$in就不用$or, $or是执行了两次查询然后将结果集合并;
对数组建索引,索引的是数组里的每个元素;
取值个数太少的字段(低cardinality字段),索引带来不了什么好处;
如果一个表上有多个索引,这次查询和每个索引都不是精确匹配的,则系统会并行执行多份查询(每份查询使用一个不同的索引),谁先完成就杀掉剩下的;(Speculative Task;备份任务!)
hint()可以强制用某个索引;$natural强制全表扫描;
唯一索引:例如"_id",不能重复插入该字段里已有的值;
固定集合(capped collection): 大小固定的环形队列;可以使用tailable cursor实现类似tail -f的功能(可实现生产者-消费者模式) 特点:1.插入速度极快;2.按照插入顺序的查询输出速度极快; 3.队列快满的时候,能够在插入最新数据时,淘汰最老的数据;用法1:储存日志信息;用法2:缓存一些少量的文档;
日期字段上可以建TTL索引,即该字段值的日期之后过N长时间会被系统自动删除(每一分钟激活一次删除线程,所以只能精确到分钟级)
全文本索引("text"类型索引): 全文搜索时使用;会给新的插入操作带来更大的延迟;按query分词后的OR关系进行召回和相关性计算,例如查询"hello world",则包含2个词的会排前面,包含1个词的会排后面;优化:如果搜索该字段时,在其他字段上有过滤,则建text索引的时候可以把那个字段也稍进来,字段的先后决定了先用哪个字段确定文档范围(决定了查询性能);如果把所有要查询的字段都包含进索引,就覆盖了查询(即只查循内存索引不查询磁盘文档了),速度更快;
地理空间索引:2dsphere是球面;2d是平面;2dsphere支持"点,线,面"三种数据类型,支持"intersection","within","nearness"三种操作;可以和其他字段结合建立复合索引;
聚合pipeline常用功能:
1. $match: 过滤;优化:尽量在早期阶段过滤掉大部分数据,因为内存使用量有上限的;
2. $project: 映射;除了可以字段重命名外,还支持数学表达式,日期表达式,字符串表达式,逻辑表达式;也就是构成一个新的临时字段可以由老字段通过各种变换得到;
3. $group: 分组聚合;组内求count, sum, avg, min, max, first, last等;
4. $sort: 在第一阶段sort可以利用上索引,后面做sort用不上索引了;
5. $limit
6. $skip
MapReduce, 使mongodb的数据查询能力更加强大;
count, distinct, group+reduce(组内reduce);
内嵌or外链关联:前者查询快,后者写入快;以下情况适合内嵌:1.子文档较小;2.数据不会定期改变;3.最终数据一致即可;4.文档数据小幅增加;5.子文档的数据通常是要和父文档一起查出来的;6.快速读取;
优化读操作:正确使用索引(ObjectId是默认索引的,所以用这个做查询key时最快的!);尽可能将信息放在当个文档中返回;
优化写操作:减少索引数量;尽可能提高更新的效率;
应对数据更新时导致的槽位空间不够而move至尾部:可以先用垃圾字段占坑,然后删掉该字段;这样后面再更新就可以用这个坑了,省去了move至尾部;要更新的字段放在占坑垃圾字段相邻的前面,比较好,这样更新字段变大后不会有其他字段一起往后移动;
删除旧数据:1.固定集合(环形队列);2.TTL; 3.每天(周,月)使用一个集合;
结构相似的文档,放到同一个集合里;经常一起查询或者聚合的文档,放到同一个集合里;
线上重要而且小的数据,放在SSD上;不太重要而且大的数据(如日志),放在机械磁盘上;
服务器端为每个连接维护一个任务队列,因此同一个连接的先写后读必定是一致结果,不同连接的先写后读可能不一致;注意客户端连接池时的一致性!读副本的话,可能会读不到刚写到主分片上的数据,解决方法:读不到就去主分片上读;
出于性能考虑,mongodb不支持事务,不支持多表join
主节点可处理用户读写请求;备份节点只可处理用户读请求(也可配置成不可读),不能处理用户写请求;集群只允许有1个主节点,所以只有它能被写入;
为防止脑裂,只有半数以上节点同意后,某节点才能被选举为主节点(如果主节点得到的支持票数小于等于半数,会"退位")
集群里拥有最新数据(最后一条复制操作时间最新)的可达节点,才能胜出,如果不够新,会被一票否决(任何节点投反对票);如果有多个节点都拥有最新数据,会根据谁配置文件里的优先级指定的高就选谁;
延迟备份节点:为防止人为的误操作,设置某节点延迟很久才复制;这个节点要设成不可读,拒绝客户端读请求,只为备份恢复而生;
如果备份节点不需要处理用户读请求,可以设置成不建索引;
每个节点都有自己的oplog日志,每个节点都可以作为供其他节点同步oplog的数据源;某个备份节点挂掉后,它会自动从自己oplog的最后一个操作之后,开始同步(从别的数据源的oplog复制过来); 写操作先执行,oplog后写入;如果执行了写操作还没写入oplog就挂了,那下次同步的时候再写一次也不影响;
备份节点启动后,它会尽量从别的节点用oplog进行同步;如果实在不行,才会从别的节点进行完全复制(例如主节点只保留最近一个小时的oplog,备份节点挂了之后一个小时内要起不来,那它落后主节点太多,从而只能完全复制);
完全复制的话,会分阶段做,复制全量数据阶段的写入操作会在后一阶段复制;
维护集群视图的方法还是心跳;每个节点每2秒向其他节点发送心跳请求,反复得不到回应就认为对方挂了;这期间如果大家人为主节点已经不被半数以上节点赞成了,可以把主节点退位;
选举过程:当某个节点(A)发现自己无法连通主节点后,他就会申请自己当主节点,想所有其他节点发这个申请;其他成员如果不同意,可能有2个原因:1. A节点的数据落后于其他节点;2.已经有一个主节点在运行了(也就是只有A和主节点断开了);如果不满足以上两个反对理由,则投赞成票,票数最多的节点被选举为主节点;选举成功则会切换到主节点状态,直到不受半数以上节点支持或者挂了而退位;
我们用的阿里云MongoDB副本集架构,3个节点,有1主1从1隐藏节点;主节点可写,从节点只读,隐藏节点不让连接;隐藏节点应该是做数据恢复和选主使用的;Java Client连接的时候推荐用主从节点URI组成的字符串,这样可以做到主从故障切换的用户无感;
读取副本节点,数据上可能比读主节点有一定的延后性(即不是最新的);
如果为了增大并发读吞吐量而读副本节点,可能会对故障恢复中的集群造成过载(副本节点在忙着给太子恢复数据,或者有节点落后了而同步)
副本集:镜像;分片(Sharding):每个机器放不同的数据(同一个collection可以被分散存放到多台机器);
被索引过的字段,才能作为分片的key;按照key的范围区间进行分片的(和Redis不同,Redis是哈希到65536个桶里,然后按照桶的范围进行分片的)
key相同的文档只会被放到相同的数据块里(如果2个数据块就要查询2次了);
数据块大到阈值以后,系统自动对其进行拆分;
路由节点会承担负载均衡的任务,定期看看有没有负担过重的节点,把他的数据块搬给别的节点;对客户端是透明的(数据搬完了才会修改Metadata,最后删除旧数据;期间尽量路由到旧分片,旧分片上没有的话,系统会查路由表,把请求路由到新分片上去)
如果是_id这种递增的字段,如果用来做分片的key,频繁写入的话会导致新数据总是插入到最后一个数据块里(最后一个数据块是最大bar至max范围的),导致最后一个数据块频繁被写入和拆分,负载非常不均衡;
哈希分片键:可以先在某字段上建立哈希索引,然后以哈希值作为分片键,这样虽然按哈希值的范围划分数据块,但是字段映射到哈希值是均匀的,因此分到各个数据块上也是均匀的;
选择使用最频繁的索引做分片key,比较好;分片key字段不能被修改;分片key字段取值尽量多,只有男女这种最多只能分到2个数据块上;可以指定某个Collection只被分到某些机器上,或者不分到某些机器上(适合日志这种烂数据,把它分到一台固定的垃圾机器,别影响集群其他高性能节点)
几个有用命令:db.currentOp()查看当前运行的操作信息;profiler可以记录耗时长的操作,(会使读写速度变慢),可以配置多长时间的操作才记录;db.poi.stats()查看表大小等信息;db.stats()查看全库的大小等信息;mogotop工具和mongostat类似于top,实时查看数据库的状态;
建立索引的时候,库会被锁住(其他用户请求只可读不可写);用background参数可以让建索引在后台运行,这会时不时的释放写锁,从而允许其他写请求执行,但是会让建索引的时间拉长;
副本集集群,在主节点上建索引,默认会被备份到其他节点上;如果表非常大,这样可能会突然间让所有从节点都忙碌起来无法处理用户的写请求了,解决方式是挨个从节点下线去独立创建索引,最后主节点后台创建索引;
建索引期间如果mongodb挂了,很可能是linux的OOM killer机制触发了,内存占用量超过一定阈值后linux会杀掉占内存最多的进程;可以配置让mongodb不被杀;
预热数据(避免刚上线那会儿读磁盘到内存的时间开销):linux的dd命令可将数据库文件加载至内存;用touch命令将某个数据库表加载至内存;用find和hint和explain把索引加载至内存(注意条件字句里只写索引字段,写了其他字段那其他字段也会被加载至内存);加载最近创建的文档,可以使用ObjectId的时间戳来find,大于某个时间戳的find且explain;
碎片整理:compact命令;回收磁盘空间:repairDatabase命令(要有一倍磁盘剩余空间才行);
renameCollection可重命名集合;cloneCollection命令可在不同集群之间copy集合;
新数据默认60秒向磁盘flush一次;期间的写入操作会被记录到journal里面,journal默认每0.1秒向磁盘flush一次;
lock文件可以用来检测系统是否正常关闭;如果正常关闭了,系统会自动删掉该文件;如果非正常关闭,则该文件还在;这样下次启动的时候系统发现该文件还在,就可以进入手工或自动数据修复状态;
MongoDB的内存映射原理(mmap)
缺页中断:高的话,说明物理内存小于频繁访问的数据集大小,所以需要经常读磁盘;磁盘性能跟不上读取速度时,就是瓶颈;
索引脱靶(btree miss):往往会造成2次缺页中断,1次是读索引,1次是读文档;
IO延迟:CPU等磁盘响应的时间,往往是因为缺页中断造成的;
后台Flush平均时间:即MongoDB把dirty page写入磁盘所花费的时间;当这个值开始增长的时候,说明写磁盘速度跟不上刷新频率了;(默认60秒flush一次,所以只要写一次的时间不超过60秒就不会撑死)
一般情况下,全部索引和部分频繁访问的文档数据,会加载在物理内存里;
CPU使用率;请求队列的长度(长了一般是因为等待锁);锁比例(高的话往往是因为缺乏合适的索引)
副本集落后(lag)和oplog(operation log)的长度进行监测:lag是指备份节点的数据落后于主节点数据;副本集出现阻塞可能是因为_id索引不存在(此时需要让备份节点先下线,然后建立索引,再上线);主节点不会"等"备份节点;oplog足够长时,故障恢复时才能降低全部复制的几率,尽量采用故障机和好机的oplog之差来同步(从后半程开始跑一小截就结束了);
备份:
1. 使用文件系统的snapshot;
2. Copy数据目录:正常关闭mongodb(会自动flush);或者运行状态下,使用db.fsyncLock锁住写请求(放入队列),copy完之后,再用db.fsyncUnlock解锁写请求;
3. 使用mongodump工具;如果同时该库还在写入,则可加选项让dump期间的写操作写到某日志文件,恢复的时候也用这个选项,恢复完dump结果再把日志文件执行一遍即可;
RAID0:无备份,数据分组到各节点;
RAID1:同步镜像备份,1主盘1备份盘;
RAID5:N+1个磁盘,每N个数据块分成一组,每组有一份奇偶校验块(各个盘);没有单独的校验盘,校验块是分散在各个磁盘上的;可以容忍最多一块磁盘的损坏;
RAID10:N个磁盘是主盘,N个磁盘是备份盘;数据块分散在N个主磁盘里;
禁用NUMA: NUMA在CPU0使用满自己local内存后,会倾向宁可换出local内存来装载新数据,也不动用空空的CPU1的local内存;mongodb需要使用越多越好的内存,跨CPU访问内存的开销远远小于内存太小的开销;
一次预读数据太多的话,容易挤掉Working Set的内存;
MongoDB的顺序访问比关系数据库少很多,所以开启hugepage其实是亏的(占用更多的内存,并没有利用上大页面里的其他数据)(hugepage这个功能竟然是为了关系数据库而生的)
操作系统进程的最大线程上限,最大fd打开上限,要设为无穷才行;MongoDB会为每个客户端连接,创建一个线程;会为每个客户端连接,打开至少一个fd(如果启用了分片,路由节点会对每个客户端连接打开分片数量个fd)
OOM killer: 如果MongoDB突然挂了,而本身日志没有任何异常信息,则可以检查linux的/var/log/messages,看看是不是OOM Killer杀死的它。可以设置一些Swap空间,MongoDB应该不会傻到用这些空间,但是会让OOM Killer放松下来,别太积极的杀进程;
一些定期任务,可能会造成内存和磁盘压力激增,对MongoDB性能造成影响(可能被OOM Killer杀死)