一般我們在更新文檔時,主要的操作流程時:讀取文檔->修改->提交保存。數據中心等保存的都是最新一次提交的內容。
大部分時候,這都沒有什么問題。但是如果兩個或更多的請求同時修改一個文檔時,非常容易產生沖突,因為上述的流程無法保證原子性,也不可能保證。
沖突解決常用的兩種策略:
- 悲觀鎖並發策略
在關系性數據庫中,通過阻塞並排隊的方式,來避免發生沖突,例如在讀取數據行時阻塞,來保證正在修改行數據的請求完成正常操作后,以讀取到最新的數據。這種方式的前提假設是數據沖突更有可能發生。 - 樂觀鎖並發策略
Elasticsearch中采用的是樂觀鎖的並發策略,這種方式的前期假設是數據沖突一般不會發生,從而避免阻塞數據請求。然而,在讀和寫之間,如果數據發生改變,更新就失敗了,然后由程序決定如果進行后續的處理。
Elasticsearch是分布式的,文檔的創建/變更等都會同步到其他節點。由於其異步性和並發的特點,這些同步請求都是並行的,因此並不能保證數據的是按照修改順序依次到達的。Elasticsearch保證了一個老版本的數據永遠無法重寫或覆蓋更新版本的數據。在 index
get
和 delete
請求中,都存在一個 _version
字段。數據的變更均會導致_version
的值增大。Elasticsearch通過該字段來保證小於該值的數據會被忽略掉。通過數字版本的方式也可以避免ABA的數據問題,即數據A修改為B而后又修改為A,對於應用端來說,數據是沒有任何變化的。
創建文檔:
1 PUT /website/blog/1/_create 2 { 3 "title": "My first blog entry", 4 "text": "Just trying this out..." 5 }
獲取文檔:
1 GET /website/blog/1 2 結果: 3 { 4 "_index" : "website", 5 "_type" : "blog", 6 "_id" : "1", 7 "_version" : 1, 8 "found" : true, 9 "_source" : { 10 "title": "My first blog entry", 11 "text": "Just trying this out..." 12 } 13 }
此時 _version
為1
修改數據:
1 PUT /website/blog/1?version=1 2 { 3 "title": "My first blog entry", 4 "text": "Starting to get the hang of this..." 5 }
1 { 2 "_index": "website", 3 "_type": "blog", 4 "_id": "1", 5 "_version": 2 6 "created": false 7 }
此時操作成功。
如果在_version為1的基礎我們再次提交修改:
1 { 2 "error": { 3 "root_cause": [ 4 { 5 "type": "version_conflict_engine_exception", 6 "reason": "[blog][1]: version conflict, current [2], provided [1]", 7 "index": "website", 8 "shard": "3" 9 } 10 ], 11 "type": "version_conflict_engine_exception", 12 "reason": "[blog][1]: version conflict, current [2], provided [1]", 13 "index": "website", 14 "shard": "3" 15 }, 16 "status": 409 17 }
此時,顯示存在版本沖突。此時應用可以據此作出相應的處理,獲取最新數據Merge處理亦或其他處理方式。
問題解決:在請求上加上retry_on_conflict的參數,在沖突發生時,重試提交數據:
1 POST /website/pageviews/1/_update?retry_on_conflict=5 2 { 3 //some update 4 }