5.2 MongoDB的standalone模式
本节首先介绍基本的数据写入过程,然后介绍在standalone模式下出现的异常。
5.2.1 MongoDB的写入过程
写操作是指插入文档、更新文档、删除文档。客户端将写请求发送给首要副本,首要副本向集合中插入该文档、修改集合中对应的文档,或者从集合中删除该文档,也就是会把写操作应用(apply)到对应的集合中。在standalone模式下,当把写操作应用到集合中后,写入过程就结束了。而在replica set模式下,还会有额外的复制过程(后面的5.3.1节会介绍复制过程)。
5.2.2 无确认导致的丢失更新异常
本节介绍在standalone模式下出现的一种异常。
场景:异步写入
当把写操作应用到集合中后,MongoDB并没有通知客户端这次写操作成功,客户端也不等待MongoDB的通知。
在这种情况下,客户端只向MongoDB发送写操作请求,即使MongoDB没有成功处理这次写操作,客户端也不知道,只要发送了写操作请求,客户端就会认为这次写操作是成功的。在出现故障的情况下(比如网络连接断开、MongoDB服务器节点重启等),会丢失大量客户端自认为写入成功的数据(请求可能没有到达MongoDB,还在网络路由中,或者MongoDB已经接收到请求,还没来得及处理就宕机了),这种行为被称为丢失更新(loss update)。之所以说“客户端自认为”,是因为在这种情况下,MongoDB并没有向客户端承诺写入成功。
在没有故障的情况下,是不会发生丢失更新异常的,并且客户端不需要等待MongoDB的确认(acknowledgement),这种异步写入可以获得非常高的写入效率。然而,一旦出现故障,虽然客户端会出现异常报错,但客户端处理这个异常报错的过程是非常复杂的。因为客户端不知道从哪一条数据开始,MongoDB没有进行正确的处理,往往要通过多次查询,确认哪些数据已经成功写入,哪些数据没有成功写入,然后重试失败的写操作。
原因:无确认
发生这种丢失更新异常的本质原因是无确认。
解决方法:写入确认
通常,可以通过让MongoDB给客户端返回一个写入确认(write acknowledgement,ack)来防止发生丢失更新异常。MongoDB客户端可以通过write concern:w选项,要求服务器端返回ack,开启了写入确认选项的客户端会等接收到ack后,再发送下一个写操作请求。
在开启了写入确认选项后,在异常情况下(比如网络断开、服务器重启等),受异常影响的请求不会接收到服务器端返回的ack,客户端据此就知道这些写入执行失败,可以简单地重试相应的写操作。客户端对这种异常的处理,比没有写入确认的异步场景处理要简单得多。
5.2.3 未持久化导致的丢失更新异常
在开启了写入确认选项后,并不能完全避免丢失更新。下面介绍另一种丢失更新异常。
场景:重启
这种丢失更新是因为MongoDB接收到写请求后,会先把数据应用到集合中,但是这并不等于这部分数据已经被写入磁盘中,它们可能仍然在内存中。出于性能的考虑,并不是每次被应用到集合中的写操作都会立即持久化到磁盘中,根据某种策略可能会采取定期或者异步的方式将数据写入磁盘中。采取定期或者异步方式,可以将多个写操作批量地持久化到磁盘中,以优化磁盘的写入性能。
然而,这时如果MongoDB服务器重启,那么还没有持久化到磁盘中的写操作就会丢失,但是客户端已经接收到了ack,所以这种丢失更新又被称为丢失确认更新(loss ack update),表明客户端已经接收到ack的写操作丢失了。这个更新可以是各种写操作,如插入、修改、删除等。
原因:未持久化
显而易见,发生这种丢失更新异常的本质原因是未持久化。
解决方法:写入日志
为了防止出现丢失确认更新,MongoDB采用了写入日志(journaling)技术。MongoDB可以通过write concern:j选项来开启写入日志。在开启了这个选项后,MongoDB每次把写操作应用到对应的集合中前,都会把这次写操作记录在一个日志(journal)文件中,这些记录被称为操作记录。操作记录描述了这次写操作对集合应用了哪些变化。
如果发生重启,那么MongoDB可以重新执行一遍日志中记录的操作,这样数据库中就包含了所有的数据。集合也会被定期持久化到磁盘中,并且在持久化到磁盘中后日志记录一个检查点,下次重启只需要执行这个检查点之后的记录,检查点之前的操作记录可以删除。MongoDB支持多种存储引擎(如MMapV1和WireTiger),每种存储引擎实现日志都有所不同。