MongoDB 基本上可以算是 Node.js 的缺省数据库。好多主流框架都用它,比如 Mean.io (Mean.js), Meteor, Keystone 等等。(如果要了解其他的存储方式,可以看这里 http://jsnoder.com/blog/chang-yong-shu-ju-ku-xuan-yong/.) 怎样安装就不介绍了,网上文档很多。这里主要介绍如何使用的基本知识。
为什么要 NoSQL
相比较 SQL 的关系数据库,NoSQL 对数据模型的限制更少,设计简单,横向扩展(horizontal scale)更容易,对已有数据控制更好。这些特点正好符合了现在的 web app 开发特点,快速开发,中途不停修改迭代,一旦用户访问上量再增加硬件横向扩展。MongoDB 满足这些要求而同时又在数据存取的准确性,速度和可靠性几方面平衡做得比较好。估计这是 MongoDB 流行的原因之一吧。
另外就是 NoSQL 数据库相比 SQL 数据库一般管理要简单很多,不需要太多 DBA 的帮助就能使用。不过简单的代价就是功能会少些。
理解 MongoDB
MongoDB 是基于文档模型的(同样是 NoSQL, Redis 基于 key/value 结构,HBase 是 column 结构)。
MongoDB 的结构就是数据库 (db)-> 集合(collection) -> 文档(document). 其中 collection 类似 SQL 里的 table,不同之处是 collection 没有一个严格的 schema,在同一个 collection 里的 document 可以有些不同 (如果很多不同,最好还是设计另外一个 collection)。这就比 SQL 灵活多了,不用动不动就搞另外一个 table。
Document 有点类似 SQL 的行 row,但是又有很大的不同。row 是平的,每列 column 只能存一个值。如果一个 column 需要保存稍微复杂点的数据,比如员工的信息,有姓名,年龄,职位等,在 SQL 里一般就是把这些数据保存在另外一张表(table)里,然后用表里对应数据的 key 来引用它;而 MongoDB 的 Document 是立体的,它的 column 由一对 field/value 构成,其中 value 可以直接嵌入子文档 subdocument, 例如
{department:"rd", office:"b8f7s45", employee:{name:"Tom", age:25, position:"pm"}}
这里可以看到 document 的结构和 JSON 一模一样,实际叫BSON,是一种轻的二进制 JSON,而且 MongoDB 的 field/value 正好对应 JavaScript 的 property/value,所以 Node.js 存取 MongoDB 数据非常方便,而且几乎没有转换的开销。
我之前有时在 SQL 里的 NTEXT 直接保存 JSON string,临时之计,感觉这样的 data flow 有点怪,而且可读性差,外人不能直观理解。
注意:Field 不能包含 null、点 .、$ 三个字符。_id 是保留字符串,也不能用于 field name。
MongoDB 的文档 document 的最大容量是 16MB. 如果大于 16MB,可以使用 MongoDB 的 GridFS。
MongoDB 的数据类型也和 JavaScript 非常接近,不像 SQL 来自另外一个世界。
模型
设计数据模型前首先先要了解将要保存的数据结构,这样才能更好地优化数据库性能。
每个应用场景都是不同的,设计之前问问以下几个问题:
- 这个应用存取的基本对象是那些?
- 这些对象的关系?一对一,一对多,多对多?
- 添加新对象的频率
- 删除对象的频率
- 读取的频率
- 对象如何被读取?使用 ID, field/value 还是比较?
- 如何读取一组对象?
对这些问题有了清晰的认识才能更好的设计你的数据库。
数据规范化 Data Normalization
数据的规范化就是把集合 collection 和文档 document 的冗余和依赖最小化;把子对象子文档保存到其他集合的单独文档。通常数据规范化适合一对多和多对多的关系。
规范化的优点是可以节省数据库的存储空间,因为避免了在多处重复保存同一个子对象。另外,如果需要修改一个子对象,只需修改一处。
不足之处就是需要多次查找才能得到完整的对象。所以对经常读取数据的情景有性能影响。
举一个例子,什么情况下适合规范数据。
一个系统有一个用户 Users 对象集合,每个用户有姓名 name, 电话 phone 和喜欢的商店 favoriteStore 属性。这个 favoriteStore 是一个子对象,包含有 name, street, city 和 zip 属性。
可能成千上万的用户拥有同一个 favoriteStore,这里就是一个高频的一对多关系。因此,如果每个用户都保存这个 favoriteStore 就显得不合理了,因为那样就会有无数的重复数据。替代方案,新建一个 FavoriteStore 集合来保存用户的 favoriteStore,然后用户对象只用引用 favoriteStore 的 _id 就行了。
数据逆规范化 Data Denormalizing
数据逆规范化就是把子对象作为子文档 subdocument 直接嵌入主对象。适合一对一的关系和小的更新不频繁的子对象。
逆规范化的主要好处就是所有信息在一个文档里,只需要一次查找,提升性能。缺点就是前面提到的一对多关系情况下,需要在多个文档里保存相同的子对象,插入操作会变慢一点,而且会占用更多存储空间。
用例。
假设用户 user 有家庭和工作两个地址,每个 user 文档都有 name, home, work 三个属性,其中 home, work 又有 phone, street, city 和 zip 属性。 这些属性都是几乎不会改变的。可能有的用户会有相同的 home 地址,但是重复率非常低。另外地址属于小巧的对象。所以home 属性就很适合直接嵌入到用户对象。
对于 work 属性可能有些不同。如果像 home 一样,没有多少用户的 work 地址是一样的,那么就和前面一样,直接存入用户对象。如何是大公司,很多人用同一个地址,那么就可以使用规范化。
另外一种情景,如果工作地址或者家庭地址很少被用到,那么也可以把它规范到单独的集合里。所以要提高性能就得了解实用使用情况,做好平衡。
Capped Collections
Capped collections 是一种固定长度的集合。就像一个先进先出 FIFO 队列一样,如果一个新文档被插入到一个已经写满的集合,那么最先写入的文档会被删除。 Capped collections 适合经常进行插入 insertion, 获取 retrieval 和删除 deletion 操作的应用场景。
优点:
- 保证插入顺序。可以省掉索引 index 的开销。
- 通过禁止增加单个文档的容量来确保插入的顺序和数据在磁盘上的顺序一致。这样就省却了定位的开销。
- 自动删除最早数据,不用实现删除代码。
限制:
- 文档一旦插入,可以更新,但是容量只能不变或者小于之前,不能变大。
- 不能删除中间的文档。所以就算有些文档不再需要也不能删除,始终占用磁盘空间。
Capped collections 的一大用处就是用来保存系统日志。
原子写操作 Atomic Write Operation
MongoDB 的写操作是原子级的,就是在文档级同时只能有一个写操作。这意味着在某个时刻,只有一个进程可以更新某个文档。
逆规范化的文档的写操作也是原子级的,因为子对象是直接嵌入在主对象,构成一个文档。然而,规范化的文档的写入需要对其他集合进行写操作,因此对于整个大对象的写操作不是原子的。
如果你不得不确保一个大对象的写操作是原子的,那么你得使用逆规范化的设计。
文档更新
MongoDB 会为文档提供一些适当的填充空间,这样更新文档时就有地方可以存放增加的容量,而不用把文档挪位。如果更新后增加的容量比较大,那么 MongoDB 就不得不寻找一个新的磁盘位置来存放这个文档,造成性能下降。而且太多的移动位置容易造成磁盘碎片。
所以如果可能,可以把新增加的部分放入规范化的对象单独保存,老对象只用保存一个引用 id。 例如代替一个购物车 Cart 对象里的所购物品数组,你可以新建一个已购物品 CartItem 集合,然后把新选择的物品 item 作为一个文档 document 存入 CartItem 集合,最后在 Cart 对象里只用引用 item 的 id 就行了。
Indexing,Sharding
- Indexing: 排好序的索引可以提高数据库 query 的性能。_id 属性会被自动索引。
- sharding: 把数据分配到不同的 MongoDB 服务器(cluster)。每个 MongoDB 服务器是一个 shard. 这就是横向扩展(horizontal scaling),便于速度上支持高并发请求和容量上大数据。
单个大集合 vs 多个集合
另外一个要考虑的问题就是设计一个集合包括大量的文档还是设计许多小的集合来分散这些分档。 一般来说,有大量的小集合不会怎么影响数据库性能,但是如果一个集合的文档太多就可能会。你应该考虑如何把一个超大集合分成小的集合。这个有点类似用独一个的超大数组还是分散的数组段。
数据的生命周期
一个容易被忽视的数据库设计方面是数据的生命周期。一个集合到底需要保存多久? 一些数据需要无限期保存,有些可能就不需要。对于不需要的 MongoDB 提供了 TTL(Time-To-Live)属性来自动删除。