目前项目大量使用了mongodb,之前一直没有很好地系统学习和总结这块知识,最近打算整理项目中遇到的一些知识点,以便温习
什么是sharded cluster
为什么需要Sharded cluster?
MongoDB目前3大核心优势:『灵活模式』+ 『高可用性』 + 『可扩展性』,通过json文档来实现灵活模式,通过复制集来保证高可用,通过Sharded cluster来保证可扩展性。
当MongoDB复制集遇到下面的业务场景时,你就需要考虑使用Sharded cluster
- 存储容量需求超出单机磁盘容量
- 活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能
- 写IOPS超出单个MongoDB节点的写服务能力
如上图所示,Sharding Cluster使得集合的数据可以分散到多个Shard(复制集或者单个Mongod节点)存储,使得MongoDB具备了横向扩展(Scale out)的能力,丰富了MongoDB的应用场景。
Sharded Cluster
MongoDB shards data at the collection level, distributing the collection data across the shards in the cluster.
sharded cluster由以下三个组件组成:
- shard:每个shard包含切分数据的一个子集。每个shard都可以作为一个副本集部署
- mongos:mongos充当查询路由器,提供客户端应用程序和sharded cluster之间的接口
- config servers存储集群的元数据和配置设置。从MongoDB 3.4开始,配置服务器必须部署为一个副本集;
操作sharded cluster
客户端必须通过mongos来与sharded cluster中的collection交互(有可能是sharded collection,有可能不是),不能直接连接到一个shard上去操作
sharded cluster中的操作限制
- sharded collection中的单文档修改操作
指定multi: false
或justOne
选项的sharded collection的所有updateOne()
、removeOne()
和deleteOne()
操作的查询定义中必须包括shard键或_id字段。否则会返回一个error;
- sharded collections上的唯一索引
MongoDB不支持跨shards的唯一索引,除非唯一索引包含作为索引前缀的完整shard key。在这种情况下,MongoDB会在整个键上强制唯一性,而不是单个字段
- 非shard key进行查询、更新操作都会变成scatter-gather查询,影响效率
数据分布策略
shard key决定了集合的文档在sharded cluster中的分布。shard key是存在于集合中的每个文档中的索引字段或索引复合字段
Sharded cluster支持将单个集合的数据分散存储在多个shard上,用户可以指定根据集合内文档的某个字段即shard key来分布数据,目前主要支持2种数据分布的策略,范围分片(Range based sharding)或hash分片(Hash based sharding)。
范围分片
如上图所示,集合根据x字段来分片,x的取值范围为[minKey, maxKey](x为整型,这里的minKey、maxKey为整型的最小值和最大值),将整个取值范围划分为多个chunk,每个chunk(通常配置为64MB)包含其中一小段的数据。
Chunk1包含x的取值在[minKey, -75)的所有文档,而Chunk2包含x取值在[-75, 25)之间的所有文档... 每个chunk的数据都存储在同一个Shard上,每个Shard可以存储很多个chunk,chunk存储在哪个shard的信息会存储在Config server种,mongos也会根据各个shard上的chunk的数量来自动做负载均衡。
范围分片能很好的满足『范围查询』的需求,比如想查询x的值在[-30, 10]之间的所有文档,这时mongos直接能将请求路由到Chunk2,就能查询出所有符合条件的文档。
范围分片的缺点在于,如果shardkey有明显递增(或者递减)趋势,则新插入的文档多会分布到同一个chunk,无法扩展写的能力,比如使用_id作为shard key,而MongoDB自动生成的id高位是时间戳,是持续递增的。
Hash分片
Hash分片是根据用户的shard key计算hash值(64bit整型),根据hash值按照『范围分片』的策略将文档分布到不同的chunk。
Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围分片的不足,但不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。
合理的选择shard key
选择shard key时,要根据业务的需求及『范围分片』和『Hash分片』2种方式的优缺点合理选择,同时还要注意shard key的取值一定要足够多,否则会出现单个jumbo chunk,即单个chunk非常大并且无法分裂(split);比如某集合存储用户的信息,按照age字段分片,而age的取值非常有限,必定会导致单个chunk非常大。
shard key索引
- 所有sharded的集合必须有一个支持shard key的索引;也就是说,索引可以是shard key上的索引,也可以是复合索引,其中shard key是索引的前缀。
- 如果集合是空的,sh.shardCollection()将在shard key上创建索引(如果该索引不存在)。
- 如果集合不是空的,那么在使用sh.shardCollection()之前必须先创建索引。
如果删除shard key的最后一个有效索引,则通过仅在切分键上重新创建索引进行恢复。
sharded cluster唯一索引
不能在hash索引上指定唯一的约束。
对于范围分片集合,只有以下索引是唯一的:
- shard key上的索引
- 以shard key为前缀的复合索引
- 默认的_id索引;但是,如果_id字段不是shard key或shard key的前缀,则_id索引只对每个shard强制唯一性约束。
唯一性和_ID索引
如果_id字段不是分片键或分片键的前缀,则_id索引只对每个shard强制唯一性约束,而不是跨shard强制唯一性约束。
例如,考虑一个分片集合(用shard key{x: 1}),横跨两个shard A和B,因为_id key不是shard key的一部分,集合可能在shard A有_id值1的文档,在shard B有_id值1的另一个文档。
如果_id字段不是分片键,也不是分片键的前缀,MongoDB期望应用程序强制执行_id在shards的唯一性
唯一索引约束意味着:
- 对于要切分的集合,如果集合具有其他唯一索引,则不能对该集合进行切分。
- 对于已切分的集合,不能在其他字段上创建唯一索引。
通过对shard key上使用唯一索引,MongoDB可以对shard key值强制唯一性。MongoDB对整个key组合强制唯一性,而不是shard key的单个组件。为了增强shard key值的唯一性,将unique参数true传递给sh.shardCollection()方法:
- 如果集合是空的,sh.shardCollection()将在shard key上创建唯一索引(如果该索引不存在)。
- 如果集合不是空的,那么在使用sh.shardCollection()之前必须先创建索引。
虽然可以使用一个唯一的复合索引,其中shard key是一个前缀,但如果使用unique参数,则集合必须在shard key上有一个唯一索引。
易出错的操作
mongo语句练习借鉴自:https://segmentfault.com/a/11...
- 查看一年级二班的学生,年龄值有哪些
db.getCollection('grade_1_2').distinct('age')
- 查看一年级二班的学生,男生(`sex`为 0)年龄值有哪些
db.getCollection('grade_1_2').distinct('age', {"sex": 0})
- 查询结果中只显示某个字段
//只输出id和title字段,第一个参数为查询条件,空代表查询所有
db.news.find( {}, { id: 1, title: 1 } )
如果需要输出的字段比较多,不想要某个字段,可以用排除字段的方法
//不输出内容字段,其它字段都输出
db.news.find( {}, {content: 0 } )
- 一年级二班`grade_1_2`中,修改名为`zhangsan1`的学生,年龄为 8 岁,兴趣爱好为 跳舞和画画;
db.getCollection('grade_1_2').update({"name": "zhangsan1"}, {$set: {"age": 8, "hobby": ["dance", "drawing"]}})
- 一年级二班`grade_1_2`中,追加`zhangsan1`学生兴趣爱好吹牛和打篮球;
db.getCollection('grade_1_2').update({"name": "zhangsan1"}, {$push: {"hobby": {$each: ["brag", "play_basketball"]}}})
- 新学年,给一年级二班所有学生的年龄都增加一岁
db.getCollection('grade_1_2').update({}, {$inc: {"age": 1}}, {multi: true})
- 一年级二班`grade_1_2`中,删除`zhangsan1`学生的sex属性
db.getCollection('grade_1_2').update({"name": "zhangsan1"}, {$unset: {"sex": 1}})
注意unset只认key,value可以是任意的
- 一年级二班`grade_1_2`中,删除`zhangsan1`学生的hobby数组中的头元素
db.getCollection('grade_1_2').update({"name": "zhangsan1"}, {$pop: {"hobby": -1}})
- 一年级二班`grade_1_2`中,删除`zhangsan1`学生的`hobby`数组中的尾元素
db.getCollection('grade_1_2').update({"name": "zhangsan1"}, {$pop: {"hobby": 1}})
- 一年级二班`grade_1_2`中,删除`zhangsan1`学生的`hobby`数组中的`sing`元素
db.getCollection('grade_1_2').update({"name": "zhangsan1"}, {$pull: {"hobby": "sing"}})
- 查看一年级二班grade_1_2中所有年龄小于 4 岁并且大于 7 岁的学生
db.getCollection('grade_1_2').find({$or: [{"age": {$lt: 4}}, {"age": {$gt: 6}}]})
- 查看一年级二班grade_1_2中所有兴趣爱好有三项的学生
db.getCollection('grade_1_2').find({"hobby": {$size: 3}})
- 一年级二班`grade_1_2,给`zhangsan1`兴趣爱好添加跳舞
db.grade_1_3.update({name:"zhangsan1"},{$push:{hobby:"drawing"}})
- 一年级二班`grade_1_2,给`zhangsan1`兴趣爱好添加跳舞,不能重复
db.grade_1_3.update({name:"zhangsan1"},{$addToSet:{hobby:"drawing"}})
- $all为匹配数组中所有条件
db.users.find({age : {$all : [6, 8]}});
可以查询出:
{name: 'David', age: 26, age: [ 6, 8, 9 ] }
但查询不出:
{name: 'David', age: 26, age: [ 6, 7, 9 ] }
- 查询出包含1或者包含4的数据
db.user.find({ id : { $in : ["1","4"] } } );
满足任一即可
findAndModify和update的比较
根据findAndModify的官方文档总结,主要有以下区别
- 默认情况下,这两个操作都修改一个文档。但是,带有multi选项的update()方法可以修改多个匹配到query的文档。findAndModify没法修改多个文档;
- 如果多个文档符合update条件,对于db.collection.findAndModify(),可以指定一个sort来提供对要更新的文档的某种控制措施。使用update()方法不能指定在多个文档匹配时更新哪个文档。
- 默认情况下,db.collection.findAndModify()返回文档的预修改版本。要获取更新后的文档,请使用new选项。
- update方法返回一个包含操作状态的WriteResult对象。
- 当更新单个文档时,findAndModify和update方法都会原子性地更新文档(即先查询再更新是一个原子操作),
最重要的是,findAndModify和update都是原子操作
,且findAndModify可以返回更新后的文档对象
,update返回返回一个包含操作状态的WriteResult对象。
如何给内嵌文档更新值
假设我们的文档有个字段为coll,这个字段是一个object类型
{ "_id" : ObjectId("5e3cf2283737abd35ff5953d"), "name" : "jack", "age" : 18, "coll" : { "city" : "citizen", "city1" : "citizen" } }
我们想更新这个字段,为map里添加key,该怎么做呢?
可以用
db.market.update({name:"jack"},{$set:{"coll.school":"stu"}})
用coll."key"即可,不存在的话会追加写入,存在的话则覆盖
{ "_id" : ObjectId("5e3cf2283737abd35ff5953d"), "name" : "jack", "age" : 18, "coll" : { "city" : "citizen", "city1" : "citizen", "school" : "stu" } }
mgo操作mongo
当我们想更新name为jack1的数据,并将更新后的结果返回,可以按如下做法:
var nj Jack
demo := map[string]interface{}{
"coll.p1": "1",
"coll.p2": "2",
"coll.p3": "3",
}
updateMap := bson.M(demo)
_, err = mgoHelper.GetColl().Find(bson.M{"name": "jack1"}).Apply(
mgo.Change{
Update: bson.M{"$set": updateMap},
ReturnNew: true,
}, &nj)
这样没有问题,但如果我们定义
var nj *Jack
...
_, err = mgoHelper.GetColl().Find(bson.M{"name":"jack1"}).Apply(
mgo.Change{
Update: bson.M{"$set": updateMap},
ReturnNew: true,
}, nj)
这样会报panic
reflect: call of reflect.Value.Type on zero Value
可以看mgo这个Apply方法的源码,发现
func (q *Query) Apply(change Change, result interface{}) (info *ChangeInfo, err error) {
...
if doc.Value.Kind != 0x0A && result != nil {
err = doc.Value.Unmarshal(result)
if err != nil {
return nil, err
}
}
...
}
result为nil
时,直接会抛出err
;
这就要说到golang的一些基础知识,
第一种方式,var nj Jack
,这样会生成{Name: Age:0 Coll:map[]}
,各个属性会设成其对应类型的零值,之后取&nj
,则为&{Name: Age:0 Coll:map[]}
,不是nil
;
第二种方式,var nj *Jack
,这样会直接初始化一个nil
指针,因此报错了
参考文章
https://docs.mongodb.com/manu...
https://docs.mongodb.com/manu...
https://segmentfault.com/a/11...
https://yq.aliyun.com/article...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。