mongodb有一个write concern的设置,作用是保障write operation的可靠性。一般是在client driver里设置的,和db.getLastError()方法关系很大。

    一般来说,所有的mongo driver,在执行一个写操作(insert、update、delete)之后,都会立刻调用db.getLastError()方法。这样才有机会知道刚才的写操作是否成功,如果捕获到错误,就可以进行相应的处理。处理逻辑也是完全由client决定的,比如写入日志、抛出错误、等待一段时间再次尝试写入等。作为mongodb server并不关心,server只负责通知client发生了错误。

    这里有2点需要注意:

  • db.getLastError()方法是由driver负责调用的,所以业务代码不需要去显式调用。

  • driver一定会调用db.getLastError()函数,但是并不一定能捕获到错误。这主要取决于write concern的设置级别。


WriteConcern时序图:

  • write concern:0(Unacknowledged)

SouthEast

从图可见,driver调用执行Write之后会立刻调用getLastError(),然后立刻返回结果Response,然后才实际进行写操作。所以getLastError()的返回值一定是null,即使之后的Apply发生了错误,driver也不知道。使用这个级别的write concern,driver的写入调用立刻返回,所以性能是最好的,但是可靠性是最差的,因此并不推荐使用。在各平台最新版本的driver中,也不再以0作为默认级别。其实还有一个w:-1的级别,是error ignored,基本上和w:0差不多。区别在于,w:-1不会捕获任何错误,而w:0可以捕获network error


  • write concern:1(acknowledged)

SouthEast

和Unacknowledged的区别是,现在mongod只有在Apply(实际写入操作)完成之后,才会返回getLastError()的响应。所以如果写入时发生错误,driver就能捕获到,并进行处理。这个级别的write concern具备基本可靠性,也是目前mongodb的默认设置级别


  • write concern:1 & journal:true(Jounaled)

SouthEast

Acknowledged级别的write concern也不是绝对可靠的。因为mongodb的Apply操作,是将数据写入内存,定期通过fsync写入硬盘。如果在Apply之后,fsync之前mongod挂了,或者甚至server挂了,那持久化实际上是失败的。但是在w:1的级别下,driver无法捕获到这种情况下的error(因为response在apply之后就已经返回到driver)

mongod解决这个问题的办法是使用Journal机制,写操作在写入内存之后,还会写到journal文件中,这样如果mongod非正常down掉,重启以后就可以根据journal文件中的内容,来还原写操作。在64位的mongod下,journal默认是打开的。但是32位的版本,需要用--journal参数来启动

在driver层面,则是除了设置w:1之外,再设置journal:true或j:true,来捕获这个情况下的error。


  • write concern:2(Replica Acknowledged)

SouthEast

这个级别只在replica set的部署模式下生效,只有secondary从primary完成了复制之后,getLastError()的结果才会返回。也可以同时设置journal:true或j:true,则还要等journal写入也成功后才会返回。但是注意,只要primary的journal写入就会返回,而不需要等待secondary的journal也写入。类似的也可以设置w:3,表示至少要有3个节点有数据;或者w:majority,表示>1/2的节点有数据。一般小规模的集群就是3节点部署,所以配置w:2就可以了。

    

  • mongodb数据写入的顺序:

  1. 写入到内存:mongodb执行写入的时候,是首先把数据写入到内存当中,如果在数据写入到日志文件journal或者刷新到硬盘之前mongod挂掉了,那么数据就会丢失。

  2. 写入到journal:如果打开journal,那么即使断电也只会丢失100ms的数据,这对大多数应用来说都可以容忍了。从1.9.2+,mongodb都会默认打开journal功能,以确保数据安全。而且journal的刷新时间是可以改变的,2-300ms的范围,使用 --journalCommitInterval 命令。

  3. 写入到硬盘:数据刷新到oplog和硬盘的时间间隔是60秒,对于复制来说,不用等到oplog刷新到硬盘,在内存中就可以直接复制到Secondary节点。


  • getLastError命令

  1. getLastError 是Mongodb的一个命令,从名字上看,它看起来好像是取得最后一个error,但其实它是Mongodb的一种客户端阻塞方式。用这个命令来获得当前线程上一个写操作是否成功的信息。

  2. getlastError有几个参数:j,w,fsync。在大多数的语言驱动中,这个命令是被包装成WriteConcern类,比如java。j表示是否需要等待写入完日志再返回成功通知,w表示至少写入到几个服务实例再返回成功通知,fsync表示是否需要等待刷新到硬盘再返回成功通知。

  3. journal无论如何都是建议打开的,设置j:true,只是说driver调用getLastError()之后是否要等待journal写入完成再返回。并不是说不设置j:true就关闭了server端的journal。


  • 什么时候使用getLastError命令

  1. Mongodb的写操作默认是没有任何返回值的,这减少了写操作的等待时间,也就是说,不管有没有写入到磁盘或者有没有遇到错误,它都不会报错。但一般我们是不放心这么做的,这时候就调用getlastError命令,得到返回值。

    以java为例,举个例子:当我们为字段建立了一个唯一索引,针对这个字段我们插入两条相同的数据,不设置WriterConcern或者设置WriterConcern.NORMAL模式,这时候即便抛出异常,也不会得到任何错误。insert()函数在java中的返回值是WriteResult类,这个类实际上包装了getlastError的返回值,但是这时候WriteResult的_lastErrorResult属性实际上是空的。因为dup key错误是server error,只有在WriterConcern.SAFE或更高级别的模式下,才会得到server error。

  2. 在多线程模式下读写Mongodb的时候,如果这些读写操作是有逻辑顺序的,那么这时候也有必要调用getlasterror命令,用以确保上个操作执行完下个操作才能执行,因为两次执行的连接有可能是不同的。在大多数情况下,我们都会使用连接池去连接mongodb,所以这是需要注意的。

  3. 在java等语言中,是不需要显示调用这个命令的,只需要设置WriterConcern即可。


  • getLastError最佳实践

  1. 如果没有特殊要求,最低级别也要使用WriterConcern.SAFE,即w=1。

  2. 对于不重要的数据,比如log日志,可以使用WriterConcern.NONE或者WriterConcern.NORMAL,即w=-1或者w=0,省去等待网络的时间。

  3. 对大量的不连续的数据写入,如果每次写入都调用getLastError会降低性能,因为等待网络的时间太长,这种情况下,可以每过N次调用一下getLastError。但是在Shard架构上,这种方式不一定确保之前的写入是成功的。

  4. 对连续的批量写入(batchs of write),要在批量写入结束的时候调用getlastError,这不仅能确保最后一次写入正确,而且也能确保所有的写入都能到达服务器。如果连续写入上万条记录而不调用getlastError,那么不能确保在同一个TCP socket里所有的写入都成功。这在并发的情况下可能就会有问题。避免这个并发问题,可以参考如何在一个链接(请求)里完成批量操作 http://mongodb.github.io/mongo-java-driver/3.0/driver/reference/crud/

  5. 对数据安全要求非常高的的配置:j=true,w="majority" db.runCommand({getlasterror:1,j:true,w:'majority',wtimeout:10000})

  6. java语言可以在MongoOption中设置,MongoOption中的这些设置是全局的,对于单独的一个(连接)操作,还可以分别设置。

  7. 总结:


  • spring-data-mongo中提供的预定义WriteConcern级别如下:

  1. NONE : 没有异常抛出

  2. NORMAL : 仅抛出网络异常, 没有服务器异常

  3. SAFE : 抛出网络异常 和 服务器异常, 并等待服务器完成写操作

  4. MAJORITY : 抛出网络异常 和 服务器异常, 并等待主服务器完成写操作

  5. FSYNC_SAFE : 抛出网络异常 和 服务器异常, 写操作等待服务器将数据刷新到磁盘

  6. JOURNAL_SAFE : 抛出网络异常 和 服务器异常, 写操作等待服务器提交到磁盘的日志文件

  7. REPLICAS_SAFE : 抛出网络异常 和 服务器异常, 并等待至少2台服务器完成写操作


  • spring-data-mongo预定义WriteConcern的构造如下,想查看详细情况请阅读源码

//w:-1, wtimeout:0, fsync:false, j:false
public static final WriteConcern NONE = new WriteConcern(-1);

//w:0, wtimeout:0, fsync:false, j:false
public static final WriteConcern NORMAL = new WriteConcern(0);

//w:1, wtimeout:0, fsync:false, j:false
public static final WriteConcern SAFE = new WriteConcern(1);

//w:"majority", wtimeout:0, fsync:false, j:false
public static final WriteConcern MAJORITY = new Majority();

//w:1, wtimeout:0, fsync:true, j:false
public static final WriteConcern FSYNC_SAFE = new WriteConcern(true);

//w:1, wtimeout:0, fsync:false, j:true
public static final WriteConcern JOURNAL_SAFE = new WriteConcern(1, 0, false, true);

//w:2, wtimeout:0, fsync:false, j:false
public static final WriteConcern REPLICAS_SAFE = new WriteConcern(2)