分片,唯一索引和upsert,錶面上看似沒有直接聯繫的幾個東西,到底存在怎樣的瓜葛呢? ...
前言
分片,唯一索引和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