MongoDB的并发
线上环境遇到MongoDB的性能瓶颈,为了解决性能瓶颈学习了一下MongoDB中的并发机制,记录如下。下文中主要是对比了MongoDB 2.2和3.0.7这两个版本的并发机制。
1. MongoDB锁的类型
在2.2版本中MongoDB用的是读写锁,允许并行的读但是只能互斥的写,当一个读锁存在的时候可以有多个读操作共享这个锁,但是当一个写锁存在的时候只能有一个写操作获得这个锁,其它的读或者写不能共享这个锁。
在2.2版本中写锁是"贪婪"的,意味着写比读有更大的优先权,当一个读和写操作正在等待一个锁的时候,MongoDB会优先满足写操作的锁要求。
在3.0版本中MongoDB的锁机制就有了比较大的改进,跟常见的数据库锁机制比较相似了。3.0还是使用读写锁机制,只是支持了多粒度的锁,支持全局、数据库、集合这几个粒度的锁(锁的粒度待到下面章节来详细了解)。MongoDB支持插件式的存储引擎,这样允许存储引擎自己来实现比集合粒度更细的并发控制(例如在3.0版本中引入了WiredTiger引擎,这个引擎支持文档粒度的锁)。
在3.0中除了"共享锁S"(我个人理解就是读锁)和"互斥锁X"(也就是写锁)以外还加入了"意向共享锁IS"和"意向互斥锁IX",这两种类型的锁预示我们后面需要更细粒度的锁来读写资源。当我们用某一粒度的锁以后所有比这个粒度大的地方都用"意向锁"来锁定。
例如,当锁定一个集合用于写(使用X锁)的时候,相应的数据库锁和全局锁必须用意向锁(IX锁)来锁定。一个数据库能够同时被IS、IX模式锁定,但是一个互斥锁X不能和其它的锁模式并存,一个共享锁S只能和意向共享锁IS同时存在。
在3.0版本中锁是公平的(不像2.2版本中写锁有更大的优先权),读和写顺序在队列中排队。然而,为了优化吞吐量,当一个请求被授权,其它兼容的请求都会在同一时刻被授权,这样就可能在遇到矛盾的请求之前就已经释放了这个请求。例如,刚刚一个X锁被释放,冲突队列中包含了如下的锁:
IS → IS → X → X → S → IS
如果按照严格的先进先出(FIFO),只有最开始的两个IS请求会被释放,但是MongoDB会把所有跟IS兼容的IS、S请求都同时释放。一旦这些请求被释放,MongoDB接着就会释放X,即使有新的IS、S请求到来,也就是说MongoDB只会释放队列中最前面的请求,这样就不会有请求被"饿死"。
2. 锁的粒度
从2.2版本开始,MongoDB实现了数据库级别的锁,对于大部分的读写操作都用数据库锁即可,但是一些全局操作,通常涉及到多个数据库操作的时候还是需要全局锁。在2.2版本之前MongoDB只有全局锁,例如,如果有6个数据库那么其中一个数据库的写操作不会影响其它5个数据库的读写操作的,但是这在2.2之前是不行的。
在MongoDB 3.0版本中锁的粒度就变得更细了,除了全局锁、数据库锁还加入了集合锁,而且对于WiredTiger存储引擎和MMAPv1存储引擎而言两者之间的锁机制也有不同。
WiredTiger:对于大部分的读写操作,WiredTiger使用乐观锁。WiredTiger对于全局、数据库、集合级别只会使用意向锁。当存储引擎检测到两个操作之间的冲突,一个写冲突导致MongoDB透明地重试写操作。一些全局操作,跟2.2版本一样还是会需要全局锁,例如,删除一个集合,那么仍然还是需要一个互斥的数据库锁的。
MMAPv1:3.0版本MMAPv1引擎用集合锁,相比之前的版本数据库锁是最细粒度的锁而言有了更进一步的改进。例如,在使用MMAPv1作为存储引擎的数据库中有6个集合,当其中一个集合写锁存在的时候,其它5个集合仍然可以自由的使用读锁、写锁来进行读写操作。
3. 如何查看当前MongoDB锁的状态
MongoDB提供了如下的命令来查看当前的锁状态:
楼主一般用currentOp来查看当前MongoDB的锁的状态,具体的可以参考文档。
如果遇到一个慢查询导致锁一直没释放的可以参考我这篇文章MongoDB一次性能问题处理。
4. 读写操作是否会主动让出锁?
在某些情况下,读写操作会主动让出自己拥有的锁。
长时间的读或者写操作,例如query、update、delete,都会在很多情况下主动让出锁。
MongoDB的MMAPv1引擎使用启发模式来预测要读取的数据是否在内存中,如果预测数据没有在内存中那么在把数据从硬盘加载到内存的过程中这个读锁就会主动让出,一旦数据加载完,这个读操作就会重新获得锁。
对于支持文档级别并发控制的存储引擎,如WiredTiger,当存在全局、数据库、集合级别的意向锁的时候没有必要主动让出锁,因为这些锁并不会完全阻塞读、写操作。
5. 常见操作对应的锁
MongoDB中的一些常见的操作对应的锁可以参考如下的两个链接:
- Which operations lock the database?
- Which administrative commands lock the database?
- Does a MongoDB operation ever lock more than one database?
6. sharding、replica set对并发的影响
在sharding模式下每一个mongod实例都是独立于分片集群中其它实例的包括它的锁,一个mongod实例中的锁不会影响其它实例。
在replica set模式下因为要保持primary、secondaries之间的同步,所以当在primary写入数据的时候MongoDB同步更新primary中的oplog(oplog是一个特殊的集合在local数据库中),因此MongoDB会同时锁住两个数据库以保证同步。
7. MongoDB支持事务吗?
MongoDB不支持多个文档的事务。
然而,MongoDB提供在单个文档中的原子操作。通常情况下文档级别的原子操作足够解决在关系数据库中要求ACID事务的大多数问题。
例如,在MongoDB中你可以在一个文档中把相关的数据嵌入嵌套数组或者嵌套文档中,然后在一次原子操作中更新整个文档。关系型数据库可能会通过相关的几个表或者行来达到这个目的,这样的话就会需要事务来保证数据的原子性。
SEE ALSO:Atomicity and Transactions
8. MongoDB提供了什么样的隔离保证?
MongoDB在并行读写的时候提供了如下的保证,MMAPv1或WiredTiger都会提供这些保证:
1. 在单一的文档中读写操作都是原子的,永远不会把一个文档置于不一致状态。这个意味着一个读者永远不会看到一个部分内容更新的文档,索引也会和集合内容一直保持一致。此外,在一个文档中的一系列读、写操作都是串行的。
2. 像db.collection.find()
这种查询谓词只会返回匹配的文档,db.collection.update()
也只会更新匹配的文档。(废话)
3. 对于一个排序的读操作(例如,db.collection.find()
、db.collection.aggregate()
),排序的顺序并不会因为并发的写入而会被打乱。(不太理解)
尽管在单个文档操作中MongoDB提供了这些隔离保证,但是在程序执行期间可能会读写任意数量的文档,对于这种多个文档的读写MongoDB是没有提供事务所以在并发写的时候是不保证隔离的。这个意味着如下的一些情况可能会出现,无论是在MMAPv1或WiredTiger引擎中。
1. Non-point-in-time read operations。这里直接把原文给贴出来了,因为我实在不知道该怎么翻译这个,我理解就是数据库事务隔离中的"不可重复读"。例如,在时间点t1开始读取文档(db.collection.find({"status_id":{$lt:20}})
),一个更新文档的操作在稍后的t2时刻发生(db.collection.update({"status_id":10},{"name":"andy"})
),status_id = 10
的文档最后查找出来的结果可能会是更新后的文档,也就是说MongoDB不能看到查找时刻的快照数据。这种情况在PG的"Repeatable reads (可重复读)"事务隔离级别中就不会出现,PG最后的结果会是事务(也就是查询开始之前)之前的快照数据,更新的内容不会出现在结果中。
2. 非串行操作。假设我们在t1时刻读取文档d1,在稍后的t3时刻更新文档d1,这种称为读-写依赖,如果我们的操作是串行,那么读操作就必须在写操作之前被处理。再假设如果在t2时刻更新文档t2,在稍后的t4时刻我们有一个对文档t2的读操作,这种称为写-读依赖,在串行调度中这就需要读操作在写操作之后。上面这两种情况组合在一起就会导致循环依赖,这样也就是导致了串行调度不可行。
3. Dropped results。MongoDB在读取文档的时候可能会存在有部分匹配的文档读取失败情况,因为在这个过程中可能会有文档被更新、删除。但是只要在查找过程中没有被修改那么肯定能读取到匹配的文档。说到底还是因为没有事务导致的。
9. 没有提交到磁盘的更改能读取到吗?
可以。对于那些还只存在于内存中没有被持久化的更新是能够被读操作看到的,无论写的关注级别和日志配置是怎么样的。应用程序可能会看到如下的一些现象:
1. 在一个写操作还未回复确认给客户端的时候MongoDB允许并发的读操作能够看到这些写操作的更新。关于不同写关注级别对于的确认回复可以参考:Write Concern。
2. 读操作可能会看到部分可能会被回滚回去的数据在一些极端情况下,例如replica set故障或者断电。但是这并不是意味着读操作可以看到部分更新的文档或者看到文档的不一致状态。这里只是说一个文档可能会被回滚回去,并不是说一个文档的部分内容会回滚。
其实这种情况就对应了数据库事务隔离中的"read uncommitted"级别。