后台创建索引,遍历Collection表的所有数据之前,会先把数据库的锁从MODE_X变成MODE_IX, (关于数据库多级锁的概念,https://en.wikipedia.org/wiki/Multiple_granularity_locking), 从而允许其他客户端对数据库读写操作.一面做全表扫面,一面做数据的更新,如果保证索引和数据能对的上呢?
事实上Mongodb是不提供事务保证的,只对单个文档的操作提供了原子操作保证,所以如果执行一个update多个值的操作,不能保证要更新的多个值的原子性.假如现在更新操作要更改两个值(key1,value1),(key2,value2),你在更新(key1,value1)时候,有可能另一个人更改了(key2,value2),等你再去更新(key2,value2)的时候,由于你获取的是原来的版本,所以更新时候会产生WCE(写冲突异常),你需要重新获取(key2,value2)的新版本,在新版本基础上再执行更新操作.事实上,Mongodb在对每个值做操作之前都会重新获取一次这个值的最新版本.就WiredTiger存储引擎而言,每次获取一个新版本,相当于创建了一个新的快照,在这个快照基础上做一次事务.快照隔离级别对应ANSI SQL定义的可重复读隔离级别.了解了这些我们再说后台创建索引的问题.
我们将创建索引和对数据的读写操作的竞争分为几种场景来描述:
a 对key已经创建了索引,对key做修改操作
b 对key还没创建索引,对key做修改操作
c 对key做创建索引的同时,对key做修改操作
从上面的三个场景来看,a,b是不会有竞争条件出现的,两个操作是一前一后执行.
a 的情况key已经做了索引的创建,对key的修改操作会先删除key的旧值索引,再对key的新值做索引.
b 的情况key还没有创建索引,对key的修改操作先删除老的索引,因为还没有索引,所以删除返回WT_NOTFOUND,不做处理.再对key的新值做索引.等后台创建索引操作为这个key创建索引的时候拿的是key的新版本创建索引,因为索引已经创建所以会产生写入冲突,跳过为这key创建索引,继续为下一个key创建索引
对于问题c是我们要讨论的竞争条件,这个问题曾经引发Mongodb索引创建的bug,目前已经修改,我们来描述这个bug的产生条件
During an index build, the collection is locked in mode IX. Consider a background index build occurring with concurrent updates. Then, the following race scenario could occur:
- Both the updater and the index builder read the same document.
- To perform the update, the updater unindexes the document's old value.
- Because the document has yet to be indexed, the WiredTiger returns WT_NOTFOUND. No WCE is generated.
- The update proceeds to index the new value.
- The indexer in the background, unaware of any WCEs, indexes the value it has read. The result is that the index has one too many keys.
We can fix this by having the update path trigger a write conflict exception even in the "no-op" case when unindexing returns WT_NOTFOUND.
(An alternative is to simply lock the collection in X mode during an index build, but this will probably have a large impact on performance.)
以上描述的场景,最终产生的结果是,更新操作为document的新版本创建一个索引,后台创建索引操作会为document老版本创建一个索引.显然这是错误的,错误产生的原因是更新操作为新值创建了索引,后台创建索引操作为老值创建了索引,两个操作读取的数据集合相同,但是更新的数据集合没有交集,所以两个快照同时操作不会产生写冲突.更新操作修改的数据集合是(NewValue),后台创建索引修改的数据集合是(OldValue).
但是明明是操作的同一个key,这个冲突应该产生才对,为了解决这个问题,Mongodb的开发者在步骤2)上删除老索引返回WT_NOTFOUND的时候,故意插入老版本数据的索引,再删除它,这样更新操作修改的数据集合是(OldValue,NewValue),后台创建索引修改的数据集合是(OldValue),因为对修改了OldValue就使更新操作和后台创建索引操作产生写冲突,后台创建索引操作会跳过为这key创建索引,继续为下一个key创建索引