1、为什么要NoSQL:nosql能解决sql中那些解决不了的问题
NoSQL是什么:Not Only SQL,本质上还是数据库,但它不会遵循传统数据库的规则(如:SQL标准、ACID属性[事务]、表结构等)。
优点:
- 处理大量数据时性能高。
- 对磁盘读写要求不高,可以运行在便宜的PC机上,降低服务器成本。
缺点:
- 对事务的支持不够友好
- 复杂的关联查询难以实现
传统SQL |
1、吞吐量小,无法支持高并发读写 2、结构要求严谨(增改一个字段麻烦),复杂系统中难以维护此关系 |
NoSQL |
1、吞吐量大,支持海量数据的快速读写(基于内存操作数据) 2、增改字段非常容易 |
2、MongoDB简介
MongoDB是NoSQL的一种,它是一个文档型数据库。
MySQL | MongoDB |
db | database(数据库) |
table | collection(集合) |
row | document(文档) |
column | field(字段) |
index | index(索引) |
join | 无关联(可以用DBRef实现) |
primaryKey | primaryKey(主键,客户端默认使用_id,ObjectId) |
特性:
- 数据存储方式:面向集合文档存储数据,以独有的bson格式存储
- 可扩展性:可扩展性好,修改数据后不会影响生产环境的程序运行
- 语言特性:强大且面向对象的查询语言,基本覆盖了sql语言所有能力
- 索引和查询计划:完整的索引支持和查询计划
- 集群、分片、内部故障支持:支持集群之间的数据复制、自动故障转移、支持数据的分片,提升系统扩展性
- 数据操作方式:使用内存映射存储引擎,把IO操作转换成内存操作(不是只用内存,而是通过内存提高读写性能)
3、MongoDB应用场景
只要满足以上两点,选择MongoDB就绝对不会错!!!
但系统需要高一致的事务性,不推荐使用(如:银行、财务系统)。
系统结构固定且有复杂的关联查询系统,不推荐使用。
4、MongoDB数据结构
5、增删改查
a、新增,insert():
db.collectionName.insert(document)
document有两种方式:
1、直接放入数据:
db.collectionName.insert({
......
})
2、新定义变量再放入数据:
var user = {
......
}
db.collectionName.insert(user)
b、查询,find()、aggregate():
db.collectionName.find(
query(查询的条件),
<projection>(可用自参数指定返回字段,0=不展示,1=展示;0和1不能同时出现)
)
查询选择器:
常用的查询方法:
1、sort():排序,1=升序,-1=降序。
2、skip(count)、limit(count):跳过和限制。
3、distinct('fieldName'):查询唯一值。
关联查询:
DBRef插入数据: {'$ref':'collectionName', '$id':'所在集合的_id值', '$db':'dbName(可选)'}
DBRef查询数据: db.collectionName.findOne({'name':'zd'}).userId.fetch()
聚合查询:
1、分组: $group,将数据分组后再统计结果。
- $sum、$avg、$min、$max:获取分组集合中的总和、平均值、最大值、最小值
- $push:将指定表达式添加到一个数组中
- $addToSet:将指定表达式添加到集合中(无重复)
- $first:返回每组第一个文档,如有排序按照排序返回,没有则按照文档的默认顺序
- $last:同$first,但返回最后一个文档
2、投影: $project,输出指定字段(0=不显示,1显示)。
3、过滤: $match,只输出指定条件(使用MongoDB标准查询选择器)。
4、限制: $limit,只输出指定个数。
5、跳过: $skip,跳过输出指定个数。
6、排序: $sort,排序(1=升序,-1=降序)。
7、拆分: $unwind,将文档数组类型字段拆分成多条。
c、删改,update():
db.collectionName.update( <query>, // 类似于sql的where <updateSelector> // 类似于sql的set { upsert:<boolean>, // true=query不存在插入新的,false=不插入新的(默认) multi:<boolean>, // true=只更新找到的第一条记录,false=全部更新(默认) writeCocern:<document> // 写入的安全配置 } )
修改选择器(uupdateSelector):
修改的原子性:
日常开发中,你可能会经常使用以下操作:
int row = update(); if (row > 0) { findByName(); }
正常清下下的确没有问题,但高并发场景下这样写便会有问题,因为一但update()完毕后就可以拿到锁了,而这时可能就会有其他操作把此用户的数据修改;
故findByName()可能并能拿到update()之后的数据,而是拿到了最终的结果,这样可能会影响业务。
而MongoDB提供了原子性的更新:
db.collectionName.findAndModify({ query:{}, // 查询选择器 update:{}, // 需要更新的值,不能与remove同事出现 remove:true|false, // 删除符合条件的文档,不能与update同时出现 new:true|false, // true=返回更新后的文档,false=旧的文档 sort:{}, // 排序 fields:{}, // 显示或隐藏指定的值 upsert:true|false // 同update的upsert })
6、ObjectId()
因新增数据返回的是受影响的函数,无法返回ObjectId;这里有一个小技巧,通过update代替insert获取ObjectId
db.collectionName.update( {'':''}, // 永远不满足的查询条件 {'username':'zd'}, // 这里写需要新增的数据 {'upsert':true} // 因为查询条件永不满足,所以upsert=true便可新增数据 )
7、MongoDB存储引擎
a、WiredTiger:
WiredTiger引擎是文档并发级别,其数据写入方式是以检查点为单位,每1min创建一个检查点将数据快照写入磁盘。
因检查点的特性所以服务器宕机的话最大可能会丢失1min左右的数据,针对这一情况MongoDB采用journal来进一步的优化。
journal会记录检查点之间所有的操作日志,用于数据同步;其日志文件最小128B,当日志文件小于128B则不启用压缩算法,反之则启用snappy算法压缩。
压缩算法:以消耗CPU资源换取磁盘空间,分块压缩算法压缩集合,snappy压缩算法压缩索引(前缀压缩算法)。
内存使用情况:(RAM - 1GB) * 0.5 或 256M ,默认使用较大的那一个。
b、MMAPv1:
MMAPv1存储引擎的数据都是连续存储在磁盘上,当document需要更大的空间时,MongoDB必须重新分配空间;此间还涉及到数据的移动和索引的更新,不仅比直接更新更耗费时间,而且还会导致磁盘碎片。
因MMAPv1的特性MongoDB会为每个document分配两倍的空间。
内存使用情况:使用全部内存。
c、InMemory:
基于内存的一种存储引擎,它是文档级别的并发,默认使用内存为 RAM * 0.5 - 1GB。
8、Journal原理分析
Journal流程分析:首先找到数据文件中最后一个检查点,然后在Journal中检索与其相匹配的记录,最后将Journal中未匹配的数据恢复。
工作原理:https://www.processon.com/view/5c349f6ee4b048f108c78a52
9、MongoDB索引
索引主要用于排序和检索(1=升序,-1=降序),其分为4种:
- 单键索引:db.collectionName.createIndex({'name':-1})
- 复合索引:db.collectionName.createIndex({'name':-1, 'age':1})
- 多键索引:db.collectionName.createIndex({'address.city':-1})
- 哈希索引:db.collectionName.createIndex({'name':'hashed'})
db.collectionName.createIndex( {'name':1}, // 索引名称 { 'background':true, // 是否后台构建索引 'unique':true, // 是否是唯一索引 'sparse':true // 是否为稀疏索引 } )
索引的删除:
- 删除指定姓名:db.collectionName.dropIndex('indexName')
- 删除集合上的索引(_id删不掉):db.collectionName.dropIndexs()
- 重建集合上的索引:db.collectionName.reIndex()
- 查询集合上的索引:db.collectionName.getIndexs()
关于索引的建议:
- 根据需求建立索引,它有用但也有成本,不要对那些写多读少的建立索引。
- 尽量保证每个查询的stage都为IXSCAN,追求扫描文档数(totalDocsExamined) = 返回文档数(nReturned)。
- MongoDB在一次查询中只使用一个索引,如果多条件查询的尽量使用复合索引。
- 在数据量多的时候建立索引是非常消耗资源的,所以尽量在数据量小的时候就把索引建好。
10、可复制集
在MongoDB中可复制集是服务器分布和维护数据的方法,其实就是主从复制的升级。
优点:
- 它可以尽可能的避免数据丢失,保障数据的安全性,提高系统安全性。(最少3个节点,最大50个)
- 它具有自动化灾备机制,在主节点宕机后会选举出一个新的主节点,提高系统的健壮性。(7个选举节点上限)
- 读写分离,提高系统性能。
原理:
a、oplog:保存操作记录及时间戳。
b、数据同步:主从保持长轮询。
- 从节点查看本机oplog最新的时间戳
- 查看主节点oplog中晚于此时间戳的文档
- 加载这些文档,并根据log执行写操作
c、心跳机制:每2秒进行一次心跳检测,发现故障后会进行选举和故障转移。
d、选举制度:当主节点故障后,其余节点根据优先级和bully算法选举出新的主节点,在此之间集群服务是只读的。
11、读写分离
MongoDB读数据的方式分为5种:
- PRIMARY(默认):读操作都在主节点,若主节点不可用则报错。
- PRIMARY_PREFERRED:首选主节点,若主节点不可用则转移到其它从节点。
- SECONDARY:读从节点,不可用则报错。
- SECONDARY_PREFERRED(推荐):首选从节点,若是特殊情况则在主节点读(但主节点架构)。
- NEAREST:最邻近主节点。
12、分片
分片架构的三个主要角色:
- 分片:分片架构中唯一存储数据的角色,它可以是单台服务器也可以是一个可复制集(生成环境推荐使用可复制集),每个分区上只存储部分数据。
- 路由:由于分片只存储部分数据,所以需要一个工具(工具为mongos)来将请求处理到对应的分片中,而路由就充当这一角色。
- 配置服务器:存储集群的元数据(数据库、集合、分片的位置范围等日志信息),配置服务器最低3台。
分片键选择的一些建议:
a、不推荐点:
- 不要使用自增长的字段作为分片键,避免热点问题。
- 不能使用粗粒度的分片键,避免数据块无法分割。
- 不能使用完全随机的分片键值,这样会造成查询性能低下。
b、推荐点:
- 使用与常用查询相关的字段作为分片键,且包含唯一字段(如业务主键,id等)。
- 索引对于分区同样重要,每个分片集合上要有同样的索引,分片键默认成为索引。
- 分片集合只允许在id和分片键上创建唯一索引。
13、MongoDB最佳实践
尽量选取稳定新版本64位的MongoDB。
数据模式设计;提倡单文档设计,将关联关系作为内嵌文档或者内嵌数组;当关联数据量较大时,考虑通过表关联实现,dbref或者自定义实现关联。
避免使用skip跳过大量数据
- 通过查询条件尽量缩小数据范围。
- 利用上一次的结果作为条件来查询下一页的结果。
避免单独使用不适用索引的查询符($ne、$nin、$where等)。
根据业务场景选择合适的写入策略,在数据安全和性能之间找到平衡点。
建立索引很重要。
生产环境中建议打开profile,便于优化系统性能。
生产环境中建议打开auth模式,保障系统安全。
不要将MongoDB和其他服务部署在同一台机器上(虽然MongoDB 占用的最大内存是可以配置的)。
单机一定要开启journal日志,数据量不太大的业务场景中,推荐多机器使用副本集,并开启读写分离。
分片键的注意事项。