Elasticsearch - 處理衝突

當你使用 索引 API來更新一個文檔時,我們先看到了原始文檔,然後修改它,最後一次性地將整個新文檔進行再次索引處理。Elasticsearch會根據請求發出的順序來選擇出最新的一個文檔進行保存。但是,如果在你修改文檔的同時其他人也發出了指
令,那麼他們的修改將會丟失。

但是有些時候如果我們丟失了數據就會出大問題。想象一下,如果我們使用Elasticsearch來存儲一個網店的商品數量。每當我們賣出一件,我們就會將這個數量減少一個。突然有一天,老闆決定來個大促銷。瞬間,每秒就產生了多筆交易。並行處理,多個進程來處理交易:
web_1 中 庫存量 的變化丟失的原因是 web_2 並不知道它所得到的 庫存量 數據是是過期的。這樣就會導致我們誤認爲還有很多貨存,最終顧客就會對我們的行爲感到失望。
當我們對數據修改得越頻繁,或者在讀取和更新數據間有越長的空閒時間,我們就越容易丟失掉我們的數據。


有兩種能避免在併發更新時丟失數據的方法:

悲觀併發控制(PCC)

這一點在關係數據庫中被廣泛使用。假設這種情況很容易發生,我們就可以組織對這一資源的訪問。典型的例子就是當我們在讀取一個數據前先鎖定這一行,然後確保只有讀取到數據的這個線程可以修改這一行數據。

樂觀併發控制(OCC)

Elasticsearch所使用的。假設這種情況並不會經常發生,也不會去組織某一數據的訪問。然而,如果基礎數據在我們讀取和寫入的間隔中發生了變化,更新就會失敗。這時候就由程序來決定如何處理這個衝突。例如,它可以重新讀取新數據來進行更新,又或者它可以將這一情況直接反饋給用戶。

Elasticsearch是分佈式的。當文檔被創建、更新或者刪除時,新版本的文檔就會被複制到集羣中的其他節點上。Elasticsearch即是同步的又是異步的,也就是說複製的請求被平行發送出去,然後可能會混亂地到達目的地。這就需要一種方法能夠保證新的數據不會被舊數據所覆蓋。
我們在上文提到每當有 索引 、 put 和 刪除 的操作時,無論文檔有沒有變化,它的 _version 都會增加。Elasticsearch使用 _version 來確保所有的改變操作都被正確排序。如果一箇舊的版本出現在新版本之後,它就會被忽略掉。
我們可以利用 _version 的優點來確保我們程序修改的數據衝突不會造成數據丟失。我們可以按照我們的想法來指定 _version 的數字。如果數字錯誤,請求就是失敗。


我們來創建一個新的博文:

PUT /website/blog/1/_create
{
"title": "My first blog entry",
"text": "Just trying this out..."
}

反饋告訴我們這是一個新建的文檔,它的 _version 是 1 。假設我們要編輯它,把這個數據加載到網頁表單中,修改完畢然後。

首先我們先要得到文檔:

GET /website/blog/1
返回結果顯示 _version 爲 1 :
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out..."
}
}
現在,我們試着重新索引文檔以保存變化,我們這樣指定了 version 的數字:
PUT /website/blog/1?version=1 <1>
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
1. 我們只希望當索引中文檔的 _version 是 1 時,更新才生效。請求成功相應,返回內容告訴我們 _version 已經變成了 2 :
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2
"created": false
}
然而,當我們再執行同樣的索引請求,並依舊指定 version=1 時,Elasticsearch就會返回一個 409 Conflict 的響應碼,返回內容如下:
{
"error" : "VersionConflictEngineException[[website][2] [blog][1]:
version conflict, current [2], provided [1]]",
"status" : 409
}

這裏面指出了文檔當前的 _version 數字是 2 ,而我們要求的數字是 1 。我們需要做什麼取決於我們程序的需求。比如我們可以告知用戶已經有其它人修改了這個文檔,你應該再保存之前看一下變化。而對於“倉庫庫存量“問題,我們可能需要重新讀取一下最新的文檔,然後顯示新的數據。所有的有關於更新或者刪除文檔的API都支持 version 這個參數,有了它你就通過修改你的程序來使用樂觀併發控制。

使用外部系統的版本
還有一種常見的情況就是我們還是使用其他的數據庫來存儲數據,而Elasticsearch只是幫我們檢索數據。這也就意味着主數據庫只要發生的變更,就需要將其拷貝到Elasticsearch中。如果多個進程同時發生,就會產生上文提到的那些併發問題。
如果你的數據庫已經存在了版本號碼,或者也可以代表版本的 時間戳 。這是你就可以在Elasticsearch的查詢字符串後面添加 version_type=external 來使用這些號碼。版本號碼必須要是大於零小於 9.2e+18 (Java中long的最大正值)的整數。
Elasticsearch在處理外部版本號時會與對內部版本號的處理有些不同。它不再是檢查 _version 是否與請求中指定的數值相同,而是檢查當前的 _version 是否比指定的數值小。如果請求成功,那麼外部的版本號就會被存儲到文檔中的 _version 中。

例如,創建一篇使用外部版本號爲 5 的博文,我們可以這樣操作:

PUT /website/blog/2?version=5&version_type=external
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
在返回結果中,我們可以發現 _version 是 5 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"created": true
}
現在我們更新這個文檔,並指定 version 爲 10 :
PUT /website/blog/2?version=10&version_type=external
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
請求被成功執行並且 version 也變成了 10 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"created": false
}
如果你再次執行這個命令,你會得到之前的錯誤提示信息,因爲你所指定的版本號並沒有大於當前Elasticsearch中的版本號。

更新文檔中的一部分

文檔不能被修改,它們只能被替換掉。 更新API(update)也必須遵循這一法則。從表邊看來,貌似是文檔被替換了。對內而言,它必須按照找回-修改-索引的流程來進行操作與管理。不同之處在於這個流程是在一個片(shard) 中完成的,因此可以節省多個請求所帶來的網絡開銷。除了節省了步驟,同時我們也能減少多個進程造成衝突的可能性。
使用 更新 請求最簡單的一種用途就是添加新數據。新的數據會被合併到現有數據中,而如果存在相同的字段,就會被新的數據所替換。

更新和衝突

在本節的開篇我們提到了當取回與重新索引兩個步驟間的時間越少,發生改變衝突的可能性就越小。但它並不能被完全消除,在 更新 的過程中還能可能存在另一個進程進行重新索引的可能性。
爲了避免丟失數據, 更新 API會在獲取步驟中獲取當前文檔中的 _version ,然後將其傳遞給重新索引步驟中的 索引 請求。如果其他的進程在這兩步之間修改了這個文檔,那麼 _version 就會不同,這樣更新就會失敗。
對於很多的局部更新來說,文檔有沒有發生變化實際上是不重要的。例如,兩個進程都要增加頁面瀏覽的計數器,誰先誰後其實並不重要 —— 發生衝突時只需要重新來過即可。你可以通過設定 retry_on_conflict 參數來設置自動完成這項請求的次數,它的默認值是 0 。

POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}

From:LearnElasticSearch


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章