Mongodb操作与基本特性
本文主要介绍Mongodb中CRUD常用操作、存储引擎、数据模型,以及如何使用java Driver。
一、BSON
Mongodb中数据存储格式为BSON,和JSON非常类似,可以说在整体的结构几乎一样,只不过BSON定义了更多的数据类型,这对面向对象编程语言非常友好。
{ "_id": { "$oid": "55f6c87fdefdd10de72fc024" }, "name": "zhangsan", "age": 30, "is_alive": true, "addresses": [ "beijing", "shanghai" ], "created": { "$date": 1442236543707 } }
文档格式大概如上所示,如果数据类型时JSON不支持的,那么数据类型也将写入文档中,比如date。BSON中有2个比较特殊的类型,其中date对应java中的Date,array对于java中List。其他类型比如boolean,string,int等与java都一一对应。
在mongodb中,一条数据称为一个document,其API类为org.bson.Document,当然Document也是Bson的子类,同时也实现了Map接口,其内部有一个LinkedHashMap作为数据支撑(所以mongodb中字段是有顺序的)。
Document user = new Document() .append("name", "zhangsan") .append("age", 30) .append("is_alive",true) .append("addresses", Arrays.asList("beijing","shanghai")) .append("created", new Date());
上述就是创建一个Document的过程,这和操作一个Map其实并没有太大区别。
二、java driver示例
如下简单展示如何使用mongodb java客户端开发,因为下面的例子都是基于JAVA的。具体参见:“mongodb java”
<dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver</artifactId> <version>3.0.3</version> </dependency>
创建MongoClient的方式有多种,可以参看MongoClient的构造方法即可。最终归结来由2个主要方式,一个是MongClient + MongoClientOptions,另一个是MongClient + conenctionString。例子如下,如果想在spring环境中使用,请你继续封装。关于授权登陆,SSL方面,稍后会专门介绍,此处仅为示例:
MongoClient mongoClient = new MongoClient("127.0.0.1", 27017); //MongoClient mongoClient = new MongoClient(new MongoClientURI("mongodb://127.0.0.1:27017")); MongoDatabase db = mongoClient.getDatabase("test");//获取DB MongoCollection<Document> collection = db.getCollection("user");//获取collection(表) //通过collection进行CRUD操作
CRUD的例子将会在下文中列举。
三、Read操作
即从mongodb中读取一条或者多条数据,可以在query中使用多种复合查询条件,这种查询条件的组合类似于SQL,我们也可以使用“projection”过滤器来指定需要返回的结果中包含(不包含)哪些字段,以提升IO效率,因为Mongodb可以使用众多的类似于SQL的查询操作,而且支持多种indexes,这或许就是mongodb的亮点所在。(Mongodb之上的SQL引擎:Drill,即使用sql查询mongodb数据)
MongoCollection<Document> collection = db.getCollection("user");//获取collection(表) //通过collection进行CRUD操作 Bson parent = Filters.eq("name", "zhangsan"); parent = Filters.and(parent,Filters.gt("age", 10)); MongoCursor<Document> cursor = collection.find(parent).sort(new Document("age", 1)).skip(10).limit(5).iterator(); try { while (cursor.hasNext()) { Document item = cursor.next(); System.out.println(item.toJson()); } } finally { cursor.close();//must be }
上面例子就是一个典型的query,我们基本上可以通过多种组合,即可完成复杂的查询。我们使用了Filters API来构建查询条件,这是一个便捷的方式。当然开发者也可以自己使用mongodb的“比较指令”来自己封装:
Document filter = new Document("name",new Document("$eq","zhangsan"));
自己封装不仅繁琐而且易于出错,建议使用Filters。
1、比较符
mongodb支持如下几种比较操作,它们均可以在query的查询条件中:$eq(相等,逐字节比较),$gt(大于),$gte(大于等于),$lt(小于),$lte(小于等于),$ne(不等于),$in(值是否在指定的数组中),$nin(值不在制定的数组中)。
2、逻辑判断符
$or:逻辑or,如果document匹配多个条件判断中的一个或者多个,则将此document返回。
$and:逻辑and,document必须匹配全部的判断条件。
$not:逻辑!,$and取反,如果document不匹配条件,则返回此document。比如:"age":{$not:{$gt:16}}表示“获取age不大于16的document”,它的内部检测过程就是“如果此document存在age字段,且大于16的,一律不返回”,言外之意是:如果document中不包含age字段,或者age的值小于等于16的均会返回。
$nor:not or。
3、元素
$exists:检测文档中是否存在此字段,比如: "name":{$exists:true}表示如果name字段存在,则返回此文档。
$type:检测文档中字段的类型,比如:"age":{$type:16}表示如果age字段的类型为int则返回此文档,参见BSON Types
4、数组
$all:如果字段的值是数组,$all表示此数组中包含指(不一定必须等于)定的所有元素。比如:"addresses":{$all:["beijing","shanghai"]},首先addresses字段值是一个数组,且同时包含“beijing”、“shanghai”两个元素。
$elemMatch:数组中至少包含一个元素同时满足条件,比如:"item":{$elemMatch:{$gt:25,$lt:60}},这表示数组item至少包含一个元素大于25且小于60,item:[15,26,70]这个文档就符合要求,而item:[16,62]就不符合要求。
$size:数组的元素个数满足条件,比如"item":{$size:2}则会返回item中元素个数为2的文档。
此外,我们可以在表达式中指定index位置来获取数组中的元素:
collection.find(new Document("addresses.0","beijing"))
表示获取addresses这个数组中第0个位置的元素值为“beijing”的文档。
5、运算与表达式
$mod:取模运算,比如"price":{$mod:[4,0]}即获取price与4取模为0的文档。
$regex:正则匹配,基于PCRE,首先参与匹配的field必须建立索引,否则性能受限。语法样例为:{<field>:{$regex:/pattern/,$options:'<options>'}},比如{"name":{$regex:/^liu/,$option:i}}表示查询去name字段值以“liu”开头的文档。其中$regex表示正则表达式,表达式需要以“/”开头和结尾,$option表示辅助选项,它有4个可选值:“i”表示不区分大小写;“m”表示“多行匹配”,其中行符为"\n";“s”表示“.”符号可以匹配所有字符包括换行符,“x”表示忽略空白符。
表达式可以简写为{<field>:{$regex:/pattern/options}}。如果表达式中不包含正则匹配符(比如$,^,*等,参见PCRE),表示字符串完全匹配,而且正则表达式可以在字符串比较操作中直接使用。
比如:"name":/zhangsan/,表示name中包含完整字符串"zhangsan"的文档,"name":/^zhang/,表示以zhang开头的文档。
"name"/^zhang/i,这是正则的简写形势,表示以zhang开头且不区分大小写。
"description":"/^S/m",我们指定了选项值为m,它可以匹配多行字符串,只要每行为S开头即可。比如文档"description":"Fist line \nSecond line"将匹配成功。
6、projection
$:这个符号,很特殊,对于query而言,用于获数组中匹配比较表达的第一个元素。这是一个“projection”操作,稍后我们会详解。比如文档"number":[16,31,6,88],对于描述“如果number中存在30~100的元素,则获取第一个匹配的元素”,我们可以使用$与elemMatch:
Document filter = new Document() .append("$gt",30) .append("$lt",100); MongoCursor<Document> cursor = collection.find(Filters.elemMatch("number",filter)).projection(new Document("number.$",1)).iterator();
结果将输出:{"number":[31]}。projection表示需要在结果中包含(不包含)那些字段。
projection是mongodb提供了用于过滤返回字段的操作,1表示包含字段,0表示不包含。
$slice:限定返回结果中数组元素的数量,可以只返回数据的起止位置之间的元素。
Document document = new Document("number",new Document("$slice",Arrays.asList(2,5))); MongoCursor<Document> cursor = collection.find().projection(document).iterator();
如上例,对于number数组,只需返回位置2之后的5个元素,默认起始位置为0,语法为$slice:[skip,limit]。"number":{$slice:2}则表示返回位置2之后的元素,其中skip可以为负数,表示从尾部计数,比如$slice:[-20,5]表示从尾部计数第20个开始取5个元素。
7、find()方法:返回一个FindIterable对象,我们通常根据FindIterable获取一个Cursor,我们就可以遍历此cursor,依次读取结果集,FindIterable中有很多方法有用的方法:
1)batchSize(int size):每次网络请求返回的document条数,比如你需要查询500条数据,mongodb不会一次性全部load并返回给client,而是每次返回batchSize条,遍历完之后后再通过网路IO获取直到cursor耗尽。默认情况下,首次批量获取101个document或者1M的数据,此后每次4M,当然我们可以通过此方法来覆盖默认值,如果文档尺寸较小,则建议batchSize可以大一些。
2)skip(int number)、limit(int number):同SQL中的limit字句,即表示在符合匹配规则的结果集中skip一定数量的document,并最终返回limit条数据。可以实现分页查询。
3)maxTime(int time,TimeUnit unit):表示此次操作保持的最长时间,即server端保持cursor状态的最长时间,如果超时server端将移除此cursor,即在此通过此cursor遍历数据将会error。
4)sort(Bson bson):根据指定field排序,参与排序的字段最好是索引,如果不是,将会在内存中排序,如果参与排序的数据尺寸大于32M,将会抛出error。1表示正序,-1表示倒叙,比如"age":1表示按照age正序排序。
5)noCursorTimeout(boolean timeout):如果cursor空闲一定时间后(10分钟),server端是否将其移除,默认为false,即server会将空闲10分钟的cursor移除以节约内存。如果为true,则表示server端不需要移除空闲的cursor,而是等待用户手动关闭。无论如何,开发者都需要注意,手动关闭cursor。
6)partial(boolean partial):对于sharding集群,如果一个或者多个shard不可达,是否允许返回部分数据(只从正常的shard中获取数据)。
7)cursorType():指定cursor类型,当cursor遍历完毕后是否关闭cursor,默认是关闭,无论何时都建议手动关闭cursor(不管是否耗尽curosr);当然有些开发场景可能需要保持cursor的活性,遍历到cursor的最后一条后,不关闭cursor,继续等待,此后一段时间内如果有新数据插入到cursor之后,则可以继续遍历,这就是Tailable Cursor,通常对于Capped Collection中使用。目前支持支持3种类型的Cursor:NonTailable、Tailable、TailableAwait。
8)projection(Bson bson):限定返回结果中需要包含的filed或者数组元素。在6)中我们已经看到相关的几个例子。默认情况下,将会返回document的所有字段,1表示包含,0表示不包含。
MongoCursor<Document> cursor = collection.find().projection(new Document("name",0)).iterator();
表示文档中不包含name字段。
MongoCursor<Document> cursor = collection.find().projection(new Document("address",1)).iterator();
表示结果中只包含address字段。通过例子比较,我们会发现“包含”和“不包含”互为排斥,通常只会在projection使用一种。
8、内嵌文档:内嵌文档可以通过"."来表达路径,比如文档:
{ "_id": 1, "addressess":{ "zip_code":100010, "city":"beijing" } }
可以通过查询内部文档:
collection.find(new Document("addresses.zip_code",10010));
9、查询优化:和其他数据库一样,mongodb也支持查询优化,也支持辅助索引,同时_id字段是唯一索引,通常我们需要根据查询条件,来创建合适的辅助索引,比如在单字段索引或者多个字段的组合索引,索引的作用最终是避免对整个collection(磁盘IO)全量扫描。合适的索引将会匹配较少比例的数据,否则不当的索引事实上并不能有效的筛选数据,比如一个字段status,最多有3个不同的值,那么对status建立索引并不能提高查询性能,因为query根据status不能过滤掉大量数据,这就是“基准”。
覆盖查询(Cover query)表示查询使用了索引,且所有需要返回的字段都是索引的一部分,这意味着只需要索引就可以筛选并获得结果内容,这一过程均会在内存中进行,无需进行磁盘查询整个document,这是性能比较高的。不过,如果对数组做索引,即“multi-key index”将不支持覆盖查询;如果查询条件或者projection包含了内置document字段,则也不支持覆盖查询。比如文档:
{"_id":1,user:{login:"testuser"},如果对"user.login"建立索引,那么如下查询仍将不会使用查询覆盖:
collection.find(Filters.eq("user.login","testuser")).projection(new Document("userlogin",1))
不过这个查询仍然会使用到user.login索引,但不会覆盖查询。
在sharding环境中,如果index中不包含shard key,将不支持覆盖查询;_id除外,如果查询中仅用_id查询且返回结果中只包含_id字段,仍会使用覆盖查询。
mongodb也提供了explain,用来分析query的查询计划,通常我们可以使用explain来判定此query的性能。具体请参看explain结果分析以及explain介绍。
9、分布式查询:mongodb的分布式模型分为replica set和sharded cluster。
sharded集群中将read根据sharding key(分片键)转发到指定的shard节点,read操作非常高效;当然如果query中没有包含sharding key,那么此次read将会被转发到所有的shard节点上,并有mongos server负责merge结果(包括排序),所以这种情况性能较差(俗称scatter、gather),对于大型集群,这种查询通常是不可行的。
对于replica set而言,只是涉及到将read操作路由到哪个secondary上,默认情况下,read请求总是在primary上发生,我们可以通过指定“read preference mode”来调整这一行为。
collection.withReadPreference(ReadPreference.secondaryPreferred()).find();
上述代码表示尽可能从 secondary上读取,如果所有的secondary都失效则从primary上读取。可以实现“读写分离”。目前支持的read模式:
1)primary:读操作只发生在primary上,默认的read模式。如果primary失效,则返回error。
2)primaryPreferred:读操作发生在primary上,如果primary失效,则由secondary接收read请求。这是一种比较良好的模式,
3)secondary:读操作只发生在secondary上,如果所欲哦的secondary都失效,则返回error。
4)secondaryPreferred:读操作通常发生在secondary上,如果所有的secondary都失效,则由primary接收read请求。
5)nearest:读取“最近”的节点,mongodb客户端将评估与每个节点的网络延迟,有限选择延迟最小的节点,primary和secondary都有可能接收到read请求(不同的Client或许延迟不同)。
四、Write操作
write操作包括insert、update,replace,remove四种类型,需要清楚的是mongodb并不支持事务,所以如果write操作影响多条document,那么它们之间的变更并非原子性的,即有可能几条document修改(插入)成功但是其他的或许失败;对于一个document而言,是原子性的,不可能存在一个documnet被部分更新的情况。
Document document = new Document() .append("number", Arrays.asList(15, 20, 44, 60, 80)); collection.insertOne(document);
对于insert操作,mongodb会检测文档中是否指定了_id,如果没有指定,server端将会根据一定的算法(ObjectId方法)生成一个并保存,如果开发者自己指定_id,需要确保全局唯一。
update操作可以根据查询条件,更新相应的一条或者多条documents。java Driver中提供了updateOne、updateMany、findAndModify等方法。可以在UpdateOptions中指定upsert来实现“如果存在就更新,否则就插入”的语义:
UpdateResult result = collection.updateOne(Filters.eq("name", "zhangsan"), new Document("$set",new Document("modified", new Date())), new UpdateOptions().upsert(true)); if(result.getModifiedCount() > 0) { System.out.println("found and modified!"); }
replace就是“替换”,将整个文档,全部替换为指定的document。
1、$isolated:隔离,这涉及到mongodb lock机制,mongodb允许同时有多个read、write操作交错执行,当write操作影响到多条记录时,我们可以使用"$islolated"避免这种情况,此时write操作将持有全局锁,直到此操作涉及到的所有的document都变更完毕,此期间其他client将不会看到变更的数据,直到此write操作结束或者error退出。isolated将会较长时间的持有lock,会对整体的并发能力带来负面影响;此外,它并不表达事务中的“all-or-nothing”语义,即如果write操作更新一部分数据之后,其他document失败了,并不会“回滚”操作,那些成功的document将仍会被保留。isolated在sharded集群中不生效。
collection.updateMany(new Document("name", "zhangsan").append("$isolated",1),new Document("$set",new Document("modified", new Date()));
2、Fields更新:
1)$inc:对字段的值自增,语法格式:{$inc:{<field1>:amount1,<field2>:amount2}},正负数值都可以,负值表示自减,如果指定的filed不存在,则增加此字段且设定为指定的值。如果字段的值为null,使用inc操作将会抛出错误。
2)$mul:乘法。
3)$rename:更改字段的名称,语法:{$rename:{<field1>:<newName1>,<field2>:<newName2>}},新字段不能与原字段名一样,对于内嵌文档,可以使用“.”表示路径。如果文档中已经存在newName,则首先移除那个字段,然后再将指定的Field重命名。如果指定的字段不存在,则什么都不做。
4)$set:update操作,更新指定字段的值,如果字段不存在则添加此字段和值。
5)$unset:将指定字段从文档中移除,语法:{$unset:{<field1>:"",....}}。
6)$min:如果指定的值比字段的当前值要小,那么将字段的值修改为指定的值。比如{$min:{"score":60}},如果文档中score的值 > 60,那么$min将会把score的值修改位60。
7)$max:语义同$min。
8)$currentDate:比较有用的指令,将指定字段的值修改为当前时间,类型可以为Date或者Timestamp,默认为Date,语法为:{$currentDate:{<field1>:<type>,....}},需要使用到$type。
##使用Date类型,只需要字段值为true即可,当然也可以使用$type:date ##如果是timestamp类型,需要使用$type {$currentDate:{ "modified_date":true, "modified_timestamp":{$type:"timestamp"} } }
3、Array操作:
1)$addToSet:将元素添加到指定的数组中,如果数组中已经存在此元素值则不添加。如果此字段不是数组,则操作失败。如果此字段不存在,则创建此字段且将设定为指定的数组元素。语法:{$addToSet:{<field>:<item>}},其中item需要是一个单值,如果是数组,需要借助$each修饰符来拆解。比如:{$addToSet:{<field>:{$each:[item1,item2]}}},此时item1、item2均会分别添加到<field>的数组中。
2)$pop:从数组的头或者尾部移除一个元素,就像操作双端队列一样。-1表示移除第一个元素,1表示移除最后一个元素。语法:{$pop:{<field>: -1 | 1}}。
3)$pull:从数据中移除符合条件的所有元素,即有条件的移除。语法:{$pull:{<field1}: <value | condition>,<field2>:<value | condition>....},如果指定的是condition,那么只有某个元素同时满足所有条件才能被删除。比如数组{"item"[{"size":10,"color":"read"},[{"size":30,"color":"black"}]},那么{$pull:{"item":{"size":{$gt:20},"color":"black"}}}将会移除第二个元素。详细参见“pull”。
4)$push:将元素追加到数组的尾端。语法:{$push:{<field1>:<value1>,<field2>:<value2>,...}},如果需要对一个数组添加多个元素(子数组),那么需要使用$each修饰符。
4、modifiers(修饰符):
1)$each:可以在$addToSet和$push操作中配合使用,例子参见上。
2)$slice:我们在read操作中已经了解了slice可以用来限制返回的数组元素个数,那么在write操作中,也可以用过更新后,保留最新的num个元素,它需要配合$push和$each一起使用。
#语法 { $push: { "field" : { $each:[item1,item2,item3], $slice:<num> }
其中slice的值为0表示将数组置为空,正数表示保留最后num个元素,负数表示保留最前的num个元素。
3)$sort:同$slice一样,必须配合$push和$each一起使用,表示push之后根据指定的方式对数组进行重排序。1表示正序,-1表示倒序,如果数组元素为内嵌文档,可以使用内嵌文档的字段排序,语法和$slice一样,只是sort部分可以为:$sort: <1 | -1>或者$sort: {<field>: <1 | -1>}
4)$position:配合$push和$each一起使用,表示push时将元素列表添加到哪个位置,默认为原数组的尾部。$position:0表示添加到第0个元素位置(即首个元素之前)。
5、writeConcern:
Write connern是mongodb提供了一种保障机制,当write操作成功后可以获得通知。mongodb有多个level的担保,这个有点类似于事务级别。当write操作使用较弱的concern,那么操作可以更快的返回(阻塞更少的时间),但有可能数据并没有持久化(写入磁盘),有丢失的风险;较强的concern需要等待较长的时间但是数据持久能力更强,更不容易丢失。详细请参考“write concern”
每个write concern包含三个参数:
1)w选项:默认值为1,即{w:1},即当客户端发送write操作后等待server返回确认信息,write操作只需要在primary上写入成功即可;“0”表示不需要向客户端返回确认信息,即当write操作发送后,客户端不需要等待server的执行结果,但是网络异常这种error仍然可以抛出;“majority”表示当write操作在primary执行成功后,并传播给“大多数”的secondary且执行成功后才返回确认信息,对于replica set架构而言,这种方式是最佳的,可以有效的避免write数据丢失的问题;如果w的值设置成任何>1的值,适合在replica set架构中使用,表示write操作在指定个数的nodes上执行成功后才返回确认信息(包括primary),如果集群中没有足够的nodes在线,则等待足额的节点加入且执行成功后返回。
2)、j选项
j选项用来控制mongod实例将数据写入磁盘上的journal日志(注意,是写入磁盘,直接flush),这可以确保在mongod异常关闭后数据不会丢失,如果此选项设置为true,则mongod必须开启journal功能,否则将会报错。当write操作写入journal日志且flush到磁盘后,才向客户端发送确认消息。
3)、wtimeout选项
指定write操作的耗时,毫秒,仅当w选项的值 > 1时适用。当write操作超时后,将会返回error,或许此write操作最终成功了(比如primary等待secondary的返回信息,此时超时,则返回给客户端error,不过最终此write可能会在secondary上成功执行,只是还没有来得及向primary返回成功消息),不过mongodb在timeout之后不会撤销已经成功的数据更新。如果设置为0或者没有设置此值,那么write操作将会等待直到满足条件。(可能导致数据不一致)
collection.withWriteConcern(WriteConcern.MAJORITY).updateMany(...)
6、分布式写入操作:
对于sharded集群中的一个shared collection(即collection允许分片存储),write操作将会由相应的shard节点负责执行,这由config server来决定。参见“分布式写入”。
对于replica set而言,write操作只能有primary负责接收,并异步的方式传播给secondary,大量的写入操作可能导致secondary不能及时的跟进,那么发生在secondary上的read操作可能会读取到旧的数据,如果primary失效,将会failover其中一个secondary成为primary,为了保证读取一致性也有可能触发rollbacks(即在primary上写入成功,但是在secondary上没有,但是primary失效了,此后primary再次加入,则需要rollback并与secondary保持一致,这也意味着此前的某些write操作丢失)。为了避免这个问题,开发者需要指定合适“write concern”,以确保足够多的secondary能够跟进primary,通过降低write的吞吐量来提高数据一致性(避免网络分区,CAP),此外还需要server端的一些额外的支持,比如oplog、journal等。
7、write操作性能
如果write操作影响到了索引,那么还需要对相应的索引进行变更。比如insert操作将会导致索引文件中新增条目,update操作中修改了索引的值也会触发索引的调整。所以这也是write操作对性能的影响之一。(高效索引设计有个要求,就是最好不要修改索引的值,是一种低效的操作)
有些update可能会导致document的尺寸增长,对于MMAPV1引擎而言,如果文档大小超过了原来分配的size,那么mongodb将会重新分配一个连续的磁盘空间来保存此文档(废弃以前的文档空间,可以被其他document重用),这也会需要额外消耗一些性能。在mongodb 3.0之后,MMAPV1引擎默认使用了“Power of 2 Size Allocations”以及自动padding,即为每个document分配的size为2的次幂(稍后详解),剩余空间被padding,这可以帮助mongodb高效的重用那些删除文档而产生的空闲空间,以及在很多情况下可以减少了重新分配空间的可能。
毕竟Mongodb是一个存储系统,高效的磁盘对提升性能有极大的帮助,影响存储性能的因素包括:磁盘介质(SSD,HDD)、磁盘缓存、预读以及RAID配置等。
mongodb通过使用一种称为journal的预写日志来保证write操作的可靠性,在数据写入实际的数据文件之前,首先将变更写入journal(journal日志是append操作,性能较高;但对应实际的数据文件是随机写,性能较低,所以对于数据文件的修改通常是批量 + 延迟写入)。在write concern中开启journal选项,这可以保证write操作的持久性,但是会大大降低整体的write吞吐量。我们可以修改mongod的“commitIntervalMs”,即journal日志flush到磁盘的间隔时间,此值越小,flush的频率越高,持久能力越强,即丢失数据的可能越低,但是对磁盘的IO却越大;此值越大,所来带的问题就是当异常crash时,mongodb丢失数据的风险就越高,因为上次一次flush之后的write尚没有被持久写入journal文件,将可能会丢失。这是一个在性能与可靠性之间的权衡。
8、Bulk write
mongodb支持批量write操作,即多个write操作批量发送到mongod,同样mongod不会保证这些操作原子性的执行(事务性),只是提高了IO交互的性能。可以将insert、update、remove、replace等多种类型的write批量提交到mongd。
List<WriteModel<Document>> bulks = new ArrayList<WriteModel<Document>>(); bulks.add(new InsertOneModel<Document>(new Document("name","zhangsan"))); bulks.add(new UpdateOneModel<Document>(new Document("name","lisi"),new Document("modified",new Date()))); collection.withWriteConcern(WriteConcern.MAJORITY).bulkWrite(bulks,new BulkWriteOptions().ordered(false));
Bulk write可以分为ordered或者unordered,通过BulkWriteOptions指定。如果设定了有序性,那么mongod将会依次执行列表中的write操作,如果出错,mongodb将会返回那些尚未执行的操作列表。对于unordered,mongodb则会将它们并行执行,如果出错,也将返回没有执行的操作。由此可见,ordered执行相对较慢,只有其中一个执行成功,才会执行下一个。
五、Document
mongodb中每条document的最大尺寸为16MB,对于超过16MB的数据建议使用Gridfs,一个高效的文件系统。我们稍后再介绍GridFS。
上文中我们已经提到,Document是BSON结构,key-value对,java中实现为LinkedHashMap,mongodb在保存document时,将尽可能保持字段的顺序;但是_id字段总是在第一个位置,插入时如果没有指定_id则由mongodb生成,有些操作可能会影响字段的顺序,比如$rename。
六、其他
Replica set:复制集,mongodb的架构方式之一 ,通常是三个对等的节点构成一个“复制集”集群,有“primary”和secondary等多中角色(稍后详细介绍),其中primary负责读写请求,secondary可以负责读请求,这有配置决定,其中secondary紧跟primary并应用write操作;如果primay失效,则集群进行“多数派”选举,选举出新的primary,即failover机制,即HA架构。复制集解决了单点故障问题,也是mongodb垂直扩展的最小部署单位,当然sharding cluster中每个shard节点也可以使用Replica set提高数据可用性。
Sharding cluster:分片集群,数据水平扩展的手段之一;replica set这种架构的缺点就是“集群数据容量”受限于单个节点的磁盘大小,如果数据量不断增加,对它进行扩容将时非常苦难的事情,所以我们需要采用Sharding模式来解决这个问题。将整个collection的数据将根据sharding key被sharding到多个mongod节点上,即每个节点持有collection的一部分数据,这个集群持有全部数据,原则上sharding可以支撑数TB的数据。
系统配置:1)建议mongodb部署在linux系统上,较高版本,选择合适的底层文件系统(ext4),开启合适的swap空间 2)无论是MMAPV1或者wiredTiger引擎,较大的内存总能带来直接收益。3)对数据存储文件关闭“atime”(文件每次access都会更改这个时间值,表示文件最近被访问的时间),可以提升文件访问效率。 4)ulimit参数调整,这个在基于网络IO或者磁盘IO操作的应用中,通常都会调整,上调系统允许打开的文件个数(ulimit -n 65535)。