在MongoDB发展的早期阶段,确实有一些数据持久化的问题没有处理好,特别是一些默认值的选定上。随着mongoDB的发展和社区的不断贡献,journal和replicaSet等功能也是愈来愈完善,到version 2.6以后,就已经可以使用在企业级的生产环境,也有大量的成功案例。尤为是version 3.2默认采用新的存储引擎WiredTiger,性能更是明显提高。java
在早期版本中,坊间有不少传说MongoDB会丢数据,也常常会据说mongoDB丢数据的案例。这其中有mongoDB早期版本的不完善的缘由,也有很多是因为用户对mongoDB的安全机制的不了解致使。mongodb
在MongoDB 2.0以前,Journal没有被支持或者不是一个默认开的选项。在MySQL,PostgreSQL,Oracle等关系型数据库里都有一个Write Ahead Log(RedoLog)的机制用来解决由于系统掉电或者崩溃时致使内存数据丢失问题。MongoDB的journal就是实现这个目的的一种WAL日志。在MongoDB2.0以前,Journal没有被支持或者不是一个默认开的选项。因此当你进行写入操做时。在没有Journal的状况下,数据在写入内存以后即刻返回给应用程序。而数据刷盘动做则在后台由操做系统来进行。MongoDB会每隔60秒强制把数据刷到磁盘上。那么你们能够想象获得,若是这个时候发生了系统崩溃或者掉电,那么未刷盘的数据就会完全丢失了。在大数据量的应用中,出现数据丢失的几率也就很是大了。自从2.0开始,MongoDB已经把Journal日志设为默认开启。shell
Mongodb的写操做默认是没有任何返回值的,这减小了写操做的等待时间,同时也带来了安全隐患。无论有没有写入到磁盘或者有没有遇到网络错误错误,它都不会报错,客户端发送写命令以后就返回,不会等服务器返回操做的结果。 以java为例,举个例子:当咱们为字段创建了一个惟一索引,针对这个字段咱们插入两条相同的数据,不设置WriterConcern或者设置WriterConcern.NORMAL模式,这时候即使抛出异常,也不会获得任何错误。insert()函数在java中的返回值是WriteResult类,数据库
WriteResult( CommandResult o , WriteConcern concern ){ _lastErrorResult = o; _lastConcern = concern; _lazy = false; _port = null; _db = null; }
这个类实际上包装了getlastError的返回值,可是这时候WriteResult的_lastErrorResult属性其实是空的。由于dup key错误是server error,只有在WriterConcern.SAFE或更高级别的模式下,才会获得server error。 MongoDB的读写操做是没有事务,也不彻底有顺序的。在多线程模式下读写Mongodb的时候,若是这些读写操做是有逻辑顺序的,那么这时候也有必要调用getlasterror命令,用以确保上个操做执行完下个操做才能执行。在实际开发中,咱们都是经过驱动程序操做MongoDB,而大多数驱动程序都是使用链接池去链接mongodb,这致使在多线程的程序中,颇有可能采用的不是同一个链接。经过不一样链接发送的操做命令,MongoDB服务器并不彻底按照接收的顺序来执行。这就有可能致使前面插入的数据,后面查询不到的现象,尤为是高并发程序中。安全
数据库会为每个MongoDB数据库链接建立一个队列,存放这个链接的请求。当客户端发送一个请求,会被放到队列的末尾。只有队列中的请求都执行完毕,后续的请求才会执行。因此从单个链接就能够了解整个数据库,而且它老是能读到本身写的东西。 注意,每一个链接都有独立的队列,要是打开两个shell,就有两个数据库链接。在一个shell中执行插入,以后在另外一个shell中进行查询不必定能获得插入的文档。然而,在同一个shell中,插入后再进行查询是必定能查到的。手动复现这个行为并不容易,可是在繁忙的服务器上,交错的插入/查找就显得稀松日常了。当开发者用一个线程插入数据,用另外一个线程检查是否成功插入时,就会常常遇到这种问题。有那么一两秒钟时间,好像根本就没插入数据,但随后数据又忽然冒出来。 使用Ruby、Python和Java驱动程序时要特别注意这种行为,由于这几个语言的驱动程序都使用了链接池。为了提升效率,这些驱动程序都和服务器创建了多个链接(一个链接池),并将请求分散到这些链接中去。好在它们都提供一些机制来确保一系列的请求都由一个链接来处理。MongoDBwiki(http;//dochub.mongodb.org/drivers/connections)上有不一样语言链接池的详细信息。服务器
随着MongoDB的发展和开源社区的贡献,到当前3.2版本,数据安全的问题早已不复存在。Journal日志早在2.0版本就为默认开启,在大多数的语言驱动中,插入、更新和删除操做都会等待Server返回的确认信息。网络
By default, all write operations will wait for acknowledgment by the server, as the default write concern is WriteConcern.ACKNOWLEDGED.多线程
那么在开发中,MongoDB如何来确保咱们的数据是安全的呢?在google.groupuser上,mongo的开发者有一段这样的解释:并发
By default: Collection data (including oplog) is fsynced to disk every 60 seconds. Write operations are fsynced to journal file every 100 milliseconds. Note, oplog is available right away in memory for slaves to read. Oplog is a capped collection so a new oplog is never created, old data just rolls off. GetLastError with params: (no params) = return after data updated in memory. fsync: true: with --journal = wait for next fsync to journal file (up to 100 milliseconds); without --journal = force fsync of collection data to disk then return. w: 2 = wait for data to be updated in memory on at least two replicas.app
咱们能够看到:
以MongoDB Java Driver中的GetLastError命令为例,咱们从驱动程序接口开始分析MongoDB对数据安全的实现机制。
WriteProtocol.java private BsonDocument createGetLastErrorCommandDocument() { BsonDocument command = new BsonDocument("getlasterror", new BsonInt32(1)); command.putAll(writeConcern.asDocument()); return command; } WriteConcern.java /** * Gets this write concern as a document. * * @return The write concern as a BsonDocument, even if {@code w <= 0} */ public BsonDocument asDocument() { BsonDocument document = new BsonDocument(); addW(document); addWTimeout(document); addFSync(document); addJ(document); return document; }
全部的插入、更新和删除操做底层都会调用到createGetLastErrorCommandDocument方法获得getlasterror命令的Document,该document包含Object w, Integer wTimeoutMS, Boolean fsync, Boolean journal四个属性。因此经过分析Driver的接口实现,咱们知道客户端就是经过这四个参数来调用MongoDB Server实现数据安全。 这里,咱们先看一下WriteProtocol的构造函数。
WriteProtocol.java // Private constructor for creating the "default" unacknowledged write concern. Necessary because there already a no-args // constructor that means something else. private WriteConcern(final Object w, final Integer wTimeoutMS, final Boolean fsync, final Boolean journal) { if (w instanceof Integer) { isTrueArgument("w >= 0", ((Integer) w) >= 0); } else if (w != null) { isTrueArgument("w must be String or int", w instanceof String); } isTrueArgument("wtimeout >= 0", wTimeoutMS == null || wTimeoutMS >= 0); this.w = w; this.wTimeoutMS = wTimeoutMS; this.fsync = fsync; this.journal = journal; }
下面,咱们就分析一下这四个参数分别表示什么。
When running with replication, this is the number of servers to replicate to before returning. A w value of 1 indicates the primary only. A w value of 2 includes the primary and at least one secondary, etc. In place of a number, you may also set w to majority to indicate that the command should wait until the latest write propagates to a majority of the voting replica set members. 在复制集环境中,w表示数据须要复制的服务器数量。1表示只写入primary Server就返回,2表示写入primary Server和至少一个secondary Server,其余数字同理。除了数字以外,w的值也能够被设置成“majority”,表示须要等到写操做被同步到大多数的Server以后才返回。
w参数表示的是在复制集环境中,最后一个写操做返回以前,须要等待写命令同步到几个Server(默认值为1)。w默认值为1,就兼顾了基本的数据安全和性能。
w也能够被设置为0或者-1,表示每个写入操做,MongoDB都不会返回一个是否成功的状态值。这个级别是写入性能最好但也是最不安全的级别。有很多时候MongoDB用来保存一些监控和程序日志数据,这个时候若是你有一、2条数据丢失,是不会对应用程序有什么影响的。这是version 2.2以前的默认设置,也是不少人以为MongoDB数据不安全的主要缘由。
w=1的意思就是对每个写入MongoDB的操做都会确认操做的完成状态,无论是成功仍是失败。固然这个确认只是基于主节点的内存写入。这样,就能够侦测到重复主键,网络错误,系统故障,惟一索引或者是无效数据等大多数错误,这也是当前版本的默认设置。在这种状况下,出现由于系统故障掉电缘由而致使的数据丢失只会是咱们以前提到的日志没有及时写入磁盘的状况。若是你不能接受由于停电或系统崩溃而引发的可能的100ms的数据损失,那么你能够选用更安全的设置。
w=2,3,4...,此时表示除了能够确保写操做写入了主节点的内存,还能够确保写操做被同步到了其余2-1,3-1,4-1个Server上。假如环境配置了3台Server,1主2从,w=2就能够保证数据被写入保证1台primary Server和1台secondary Server的内存。若是要是担忧这时候primary Server不可用致使数据不一致,还能够设置w=3来确保数据被同步到全部的Server。
还有一种状况,假如Server数量不肯定或者可能有变化,咱们能够设置w值为“majority”。“majority” 指的是“大多数节点”,使用这个写安全级别,MongoDB只有在数据已经被复制到多数个节点的状况下才会向客户端返回确认。
通常来讲,MongoDB建议在集群中使用 {w: “majority”} 设置。在一个集群是健壮的部署的状况下(如:足够网络带宽,机器没有满负荷),这个能够知足绝大部分数据安全的要求,由于MongoDB的复制在正常状况下是毫秒级别的,每每在Journal刷盘以前已经复制到从节点了。 为何要说绝大多数呢? 假如集群配置1主2从,w=3 数据还在primary Server内存,写操做的journal日志也在内存中,写操做也被同步到其中一台secondary Server。这时primary Server瞬间停电,数据没有入盘,journal日志也还没来得及入盘,剩下两台secondary Server一台有数据,一台无数据。此时以前的primary Server恢复可用,状况变成了两台server无数据,一台有数据,就致使了以前写入的数据被回退了。若是写入数据的时候,可以确保journal日志入盘,这种丢数据的状况就彻底能够避免了。
Specify a value in milliseconds to control how long to wait for write propagation to complete. If replication does not complete in the given timeframe, the getLastError command will return with an error status. 以毫秒为单位指定一个值,用来控制等待写传播来完成传播的时间。若是复制没有在给定的时间内完成,则GetLastError函数命令将错误状态返回。
等待超时时间就比较好理解,若是写操做在指定的时间内尚未执行完或同步完,则将返回错误的状态。wTimeoutMS的值要根据集群的Server数量和所处的网络环境来决定,wTimeoutMS值过高也有可能会致使链接数太高。
If true, wait for the next journal commit before returning, rather than waiting for a full disk flush. If mongod does not have journaling enabled, this option has no effect. If this option is enabled for a write operation, mongod will wait no more than 1/3 of the current commitIntervalMs before writing data to the journal. 若是参数j的值为true,则只要等到下一次journal日志提交(写磁盘)就会返回,而不须要等待一个完整的磁盘刷新。若是的mongod未启用日志记录,此选项没有任何效果。若是这个选项被用于写入操做,在最终写入到日志以前,mongod将最多等待commitIntervalMs(提交间隔)值的1/3。
使用这种方式意味着每一次的写操做会在MongoDB实实在在的把journal入盘之后才会返回。固然这并不意味着每个写操做就等于一个IO。MongoDB并不会对每个操做都当即入盘,而是会等最多30ms,把30ms内的写操做集中到一块儿,采用顺序追加的方式写入到盘里。在这30ms内客户端线程会处于等待状态。这样对于单个操做的整体响应时间将有所延长,但对于高并发的场景,综合下来平均吞吐能力和响应时间不会有太大的影响。特别是给journal部署一个对顺序写有优化而且IO带宽足够的专门存储系统的话,这个对性能的影响能够降到最低。
The primary use of fsync is to flush and lock the database for backups. The fsync operation blocks all other write operations for a while it runs. 主服务器使用fsync 来强制写入磁盘和备份时候的锁住整个数据库。 fsync运行时会阻塞数据库的其余写操做。
严格来讲,FSYNC是一个管理员命令,它迫使全部的数据刷新到磁盘。你不该该在你的代码中使用它,至少不是常用,它经常使用于锁定备份数据库。MongoDB中的数据安全经过复制/分片/日志来实现,而不是强制写入,不然就违背了MongoDB的原始初衷了。在Java驱动程序包中的WriteConcern类,我基本上历来不使用WriteConcern.FSYNC_SAFE和WriteConcern.FSYNCED。
总的来讲,对于数据安全,MongoDB2.6+ 就有了很是完善的机制,并且还很灵活。从不返回结果、确认主服务器写内存、确认主服务器写journal日志、确认同步到大多数服务器到强制写入磁盘,MongoDB数据安全级别逐渐提升。开发者能够根据不一样的应用场景选择适合的安全级别,在数据安全和写操做的性能之间找到平衡。
根据以往踩过的坑和同行经验,总结出如下几点建议。
传说中MongoDB 丢数据的事情,已经成为了历史。