由于之前从图书馆借的书已经到期了,前天下午趁着好天气去图书馆了一趟,顺便借回来两本《Mongodb权威指南》《Web性能权威指南》。没想到两天时间就把前边那本看完了。总体感觉还不错,这本书写的比较细致,很适合入门学习,同时在看书的过程中,我发现了之前没有主要到的一些知识点,并且改变了我之前的一些错误认识,所以还是很有必要记录一下的。

没注意到的点

TTL索引

TTL(time-to-live)索引在mongodb中主要起到的作用就是在指定时间里,如果一条记录都没有发生变化就会被自动移除,可以起到类似redis中EXPIRE命令的作用。它的用法如下:

1
2
# 在表col中为lastOp字段建立TTL索引,过期时间是60*60,单位为秒
db.col.ensuerIndex({'lastOp':1},{'expireAfterSecs':60*60})

注意点:

  1. TTL索引必须建立在日期类型的字段上
  2. TTL索引不能够是复合索引,但是如果建立了TTL索引,它可以在排序和查询中对检索速度有优化
  3. 在实际应用中,如果建立了这种索引,为防止删除正在进行的会话,可以在进行操作的过程中update这个字段

explain()

数据入了库,如何能够在最短的时间内检索出来是一个很重要的问题。在mongodb中,有各式各样的索引:最为普通的单字段索引、多个字段联合起来的复合索引、上边刚说到的TTL索引还有针对地理空间的地理空间索引。索引往往会对检索效率有质的提升,但是如何才能够知道自己建立的索引有没有起作用?explain()和hint()这两个方法可以给你一个答复。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 未建立索引
> db.Position.find({'company':'拉勾网'}).explain()
{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "lagou.Position",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"company" : {
				"$eq" : "拉勾网"
			}
		},
		"winningPlan" : {
			"stage" : "COLLSCAN",
			"filter" : {
				"company" : {
					"$eq" : "拉勾网"
				}
			},
			"direction" : "forward"
		},
		"rejectedPlans" : [ ]
	},
	...
}
# 索引之后
> db.Position.find({'company':'拉勾网'}).explain()
{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "lagou.Position",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"company" : {
				"$eq" : "拉勾网"
			}
		},
		"winningPlan" : {
			"stage" : "FETCH",
			"inputStage" : {
				"stage" : "IXSCAN",
				"keyPattern" : {
					"company" : 1
				},
				"indexName" : "company_1",
				"isMultiKey" : false,
				"multiKeyPaths" : {
					"company" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",
				"indexBounds" : {
					"company" : [
						"[\"拉勾网\", \"拉勾网\"]"
					]
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	...
}

可以看到,在建立索引前后,explain()的结果在"winningPlan"字段中有明显不同,未建立索引的时候:"stage" : "COLLSCAN"建立索引之后:"stage" : "IXSCAN"。前者的COLLSCAN表示的意思是通过全表扫描的方式检索,后边的则是通过已经建立的索引进行检索。 再来一例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 未建立索引
> db.Position.find({'loc':{$near:{$geometry:{type:'Point',coordinates:[113,40]}}}}).explain()
2016-11-25T14:13:23.421+0800 E QUERY    [main] Error: explain failed: {
	"ok" : 0,
	"errmsg" : "error processing query: ns=lagou.PositionTree: GEONEAR  field=loc maxdist=1.79769e+308 isNearSphere=0\nSort: {}\nProj: {}\n planner returned error: unable to find index for $geoNear query",
	"code" : 2,
	"codeName" : "BadValue"
}
# 建立索引
> db.Position.ensureIndex({loc:'2dsphere'})
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 2,
	"numIndexesAfter" : 3,
	"ok" : 1
}
# 索引之后
> db.Position.find({'loc':{$near:{$geometry:{type:'Point',coordinates:[113,40]}}}}).explain()
{
	"queryPlanner" : {
		...,
		"winningPlan" : {
			"stage" : "GEO_NEAR_2DSPHERE",
			"keyPattern" : {
				"loc" : "2dsphere"
			},
			"indexName" : "loc_2dsphere",
			"indexVersion" : 2
		},
		"rejectedPlans" : [ ]
	},
	...
}

由于之前一直只是停留在会用mongodb的阶段,像类似explain()这样的数据库调优的方法会用的比较少。现在的话感觉还是仔细研究关于mongodb的各个方面还是很有必要的。不能在使用的过程中只会有“啊!好特么快呀!”或者是“沃艸,怎么这么慢”的感性认识,知道如何让它变快才是重点。

如何用索引

还记得当初曾经去南通封闭开发,遇到一个问题,由于在mongodb中一个集合存的数据过多,造成在检索的时候出现超时的情况。如果没有记错的话,我记得造成检索失败的原因是由于那个集合的大小超出了我笔记本的物理内存大小,通过普通的“全表扫描”的方法已经无法完成正常的检索。在建立了索引之后,数据量增大10倍检索速度也是杠杠的。 所以说,索引对于数据库来说是极其重要的,当然,这个东西我会在以后深入研究它的原理。在平常的使用过程中,对于一个集合,该不该建立索引,如何建立索引,都是很应该考虑的事情。

索引通常适用的情况:在集合数据量较大的时候,同时经常需要查询的字段索引基数较大的情况。这里所说的索引基数指的是集合中某个字段拥有不同值的数量,一个字段的索引基数越高,在这个键上建立索引就越有用,这是因为索引能够快速地讲搜索范围缩小。另外,如果你需要进行地理空间方面的检索,2dsphere或者2d索引是必须的。

mongodb自带工具

在各个版本的mongodb发行版中,bin目录下除了mongo和mongod其他几个工具也都很好用: 使用mongostat可以实时看到当前mongod实例的运行和负载情况,mongotop则类似linux系统中的top工具,mongofiles则可以很方便的操作数据库的gridfs。其他的各个工具相信在以后的使用中都会有接触。

错误的认识

由于之前的实践比较少,也没有耐着性子仔细系统的学习mongodb,直到看了这本书才发现之前有很多理解是错的(并且我在用的时候还真的是这样干的),这部分错误认识主要集中在mongodb的副本集合分片那部分。

关于副本集

之前总是想着副本集是用以缓解mongodb读写压力,进行读写分离用的,primary节点负责数据写入,各个secondary节点则从primary实时获取最新数据,并提供给应用程序读取数据的能力。仔细看了这本书之后,感觉之前真是很傻很天真,而且深切觉得这种分布式存储在实现过程中要考虑的东西是那么多。

使用副本集可以将数据保存到多台服务器上,建议在所有的生产环境中都要使用。使用mongodb的副本集,即使一台或者多台服务器出错,也可以保证应用程序正常运行和数据安全。副本集是一组服务器,其中有一个主服务器primary,用于处理来自客户端的请求(当然是包括读和写),还有多个备份服务器secondary,用以保存主服务器的数据副本。如果主服务器崩溃,备份服务器会选举出一个成员将其升级为主服务器。

其中最大的误解在与副本集成员数据同步的原理。如上边所说,我一直以为从节点的数据都是实时从主节点上读取到的,所以主节点负责写入数据,在写入的同时从节点复制数据(这样会给主节点造成较大的负载),所以从节点就来负责数据的读取吧。我完全都没意识到数据一致性的问题(对于数据时效性要求较高的情况)还有如果在写入负载大的时候,从节点还从主节点读取数据,主节点不就崩了吗。 而实际上副本集实现数据同步是这样的:

副本集用以在多台服务器之间备份数据,mongodb的复制功能是通过操作日志oplog实现的,操作日志包含了主节点的每一次写操作。oplog是主节点的local数据库中的一个固定集合。备份节点通过查询这个集合就可以知道需要进行复制的操作。每个从节点都维护自己的oplog,记录每一次从主节点复制数据的操作,并且每个副本集成员都可以作为同步源提供给其他成员。

关于分片

由于之前用到的数据量还没有大到必须使用分片的程度,这次看书也是对分片有了重新的认识。 分片主要用于将数据拆分,将数据分散存放到不同的机器上,mongodb支持自动分片,可以使数据库架构对应用程序不可见,简化系统管理。mongodb的分片允许你创建一个包含许多台机器的集群,也就是说上边所说的副本集可以作为分片的一个节点,每个分片用于维护一个数据集的子集。 分片可以带来的优势:

  • 增加可用RAM
  • 增加可用磁盘空间
  • 减轻单台服务器的负载
  • 提升整个系统的吞吐量

一些其他的想法

到目前为止,最新的mongodb版本已经推出到3.4。mongodb的发展不能说不快,这款对开发者极其友好的数据库真的可以下功夫仔细学习、研究和使用。一些好的特性:除了相对高效的读写之外,Aggregation、map-reduce都可以应用在大数据处理上,官方提供的主流开发语言的驱动,和spark结合的connector在数据实时分析上也会有不错的效果。 而上边说到的这些东西,我想只有通过自己不断的实践练习才能在实际应用中游刃有余。