前言
分片,唯一索引和upsert
,表面上看似没有直接联系的几个东西,到底存在怎样的瓜葛呢?
分片
为了保持水平扩展的有效性,分片功能必须保证各个片之间没有直接关联,不需要与其他分片交互就可以独立做出决策。如果不能满足这一点,随着分片数量不断增加,需要交互的分片越来越多,势必会越来越慢,那么就违背了分片的初衷了。比如JOIN
就是一种典型的破坏分片独立性的功能。在一个n
个分片的集群中,为了得到笛卡尔积,每个分片必须与其他n-1
个分片交互来得到结果。虽然不见得是线性的延迟增长(因为n-1
个请求可以并行),但是可想而知对资源将是极大的消耗,并且随着分片数量的增长影响会越来越显著,最终会到达“增加一个分片可能对性能完全没有帮助”,或者“增加一个分片反而降低性能”的地步。
唯一索引
唯一索引是另外一个显著破坏分片独立性的特性。前面对JOIN
的分析完全适用于唯一索引,并且更糟的情况是唯一索引还有有更进一步的恶劣影响,那就是在写入数据的时候必须占用一个跨分片的全局锁,否则无法保证其唯一性,可想而知对性能有怎样的影响。这也是MongoDB为什么不打算去实现全局唯一索引的原因。
有一种特殊情况却可以改变这种不利状况,那就是唯一索引的键正好是片键的时候。片键一旦确定,文档该去哪个分片就确定了,那么只要保证该键在这一个片上唯一就可以了,不再需要去与其他分片协商。
upsert
从语义上讲,我们使用upsert
一般是希望一个键只出现一次的(不然每次insert
就好了)。这一点恰恰是唯一索引要干的事情,而唯一索引又存在上面的所说的问题,因此唯一有意义的情况则是upsert
使用的条件正好是片键,且片键唯一。
满足了上面这些条件就高枕无忧了吗?并不是。在决定一个键是不是存在,到执行update
/insert
之间,是存在空隙的。即,检测和执行并不在一个原子操作中,也不可能在一个原子操作中,否则将是一个很大粒度的锁。再说,MongoDB对文档级别并没有真正通过加锁来控制,而是通过“乐观并发控制”(optimistic concurrency control)来进行的。
因此,出于效率考虑,不是原子操作是正确的选择,而解决这个问题也不是特别麻烦的事情,实际上只需要在遇到duplicate key异常的时候重试该操作就可以了,因为重试的时候理论上就应该变成update
而不再是insert
,自然避免了问题。或者,在4.2中直接实现了这类错误的自动重试(SERVER-37124)。
参考资料
- Unique Indexes: https://docs.mongochina.com/core/index-unique.html
- Retry full upsert path when duplicate key exception matches exact query predicate: https://jira.mongodb.org/browse/SERVER-37124