1. 程式人生 > >ElasticSearch學習筆記十七 文件更新及版本控制

ElasticSearch學習筆記十七 文件更新及版本控制

文件更新

在 Elasticsearch 中文件是 不可改變 的,不能修改它們。相反,如果想要更新現有的文件,需要 重建索引。但是我們不需要自己來完成操作,Update API 會幫我們完成。 例如我們新插入一條紀錄

PUT /website/blog/1
{
  "title": "My first blog entry",
  "text":  "I am startinggggg to get the hang of this...",
  "date":  "2014/01/02"
}

檢視插入結果 插入結果

很明顯,我們對全新文件的操作結果是"created"文件,並且首次版本"_version"為1。

此時加入我們希望更新文件去掉之前錯誤的字元

PUT /website/blog/1
{
  "title": "My first blog entry",
  "text":  "I am starting to get the hang of this...",
  "date":  "2014/01/02"
}

結果如下 文件更新結果

我們對文件的操作結果是"updated"文件,並且版本"_version"更新為2。 實際上,Elasticsearch 已將舊文件標記為已刪除,並增加一個全新的文件。

下面我們嘗試刪除和新建文件:

DELETE  /website/blog/1

結果如下: 文件刪除結果

重新新增

PUT /website/blog/1
{
  "title":
"My first blog entry", "text": "I am starting to get the hang of this...", "date": "2014/01/02" }

再次插入紀錄

版本控制

當我們使用 Update API 更新文件 ,可以一次性讀取原始文件,做我們的修改,然後重新索引 整個文件 。 最近的索引請求將獲勝:無論最後哪一個文件被索引,都將被唯一儲存在 Elasticsearch 中。如果其他人同時更改這個文件,他們的更改將丟失。

很多時候這是沒有問題的。也許我們的主資料儲存是一個關係型資料庫,我們只是將資料複製到 Elasticsearch 中並使其可被搜尋。 也許兩個人同時更改相同的文件的機率很小。或者對於我們的業務來說偶爾丟失更改並不是很嚴重的問題。

但有時丟失了一個變更就是 非常嚴重的 。試想我們使用 Elasticsearch 儲存我們網上商城商品庫存的數量, 每次我們賣一個商品的時候,我們在 Elasticsearch 中將庫存數量減少。

有一天,管理層決定做一次促銷。突然地,我們一秒要賣好幾個商品。 假設有兩個 web 程式並行執行,每一個都同時處理所有商品的銷售,web_1 對 stock_count 所做的更改已經丟失,因為 web_2 不知道它的 stock_count 的拷貝已經過期。 結果我們會認為有超過商品的實際數量的庫存,因為賣給顧客的庫存商品並不存在,我們將讓他們非常失望。

變更越頻繁,讀資料和更新資料的間隙越長,也就越可能丟失變更。

在資料庫領域中,有兩種方法通常被用來確保併發更新時變更不會丟失:

悲觀併發控制

這種方法被關係型資料庫廣泛使用,它假定有變更衝突可能發生,因此阻塞訪問資源以防止衝突。 一個典型的例子是讀取一行資料之前先將其鎖住,確保只有放置鎖的執行緒能夠對這行資料進行修改。

樂觀併發控制

Elasticsearch 中使用的這種方法假定衝突是不可能發生的,並且不會阻塞正在嘗試的操作。 然而,如果源資料在讀寫當中被修改,更新將會失敗。應用程式接下來將決定該如何解決衝突。 例如,可以重試更新、使用新的資料、或者將相關情況報告給使用者。

Elasticsearch 是分散式的。當文件建立、更新或刪除時, 新版本的文件必須複製到叢集中的其他節點。Elasticsearch 也是非同步和併發的,這意味著這些複製請求被並行傳送,並且到達目的地時也許 順序是亂的 。 Elasticsearch 需要一種方法確保文件的舊版本不會覆蓋新的版本。

_version版本控制

當我們之前討論 index , GET 和 delete 請求時,我們指出每個文件都有一個 _version (版本)號,當文件被修改時版本號遞增。 Elasticsearch 使用這個 _version 號來確保變更以正確順序得到執行。如果舊版本的文件在新版本之後到達,它可以被簡單的忽略。

我們可以利用 _version 號來確保 應用中相互衝突的變更不會導致資料丟失。我們通過指定想要修改文件的 version 號來達到這個目的。 如果該版本不是當前版本號,我們的請求將會失敗。

讓我們建立一個新的部落格文章:

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

響應體告訴我們,這個新建立的文件 _version 版本號是 1 。現在假設我們想編輯這個文件:我們載入其資料到 web 表單中, 做一些修改,然後儲存新的版本。

首先我們檢索文件:

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
{
  "title": "My first blog entry",
  "text":  "Starting to get the hang of this..."
}

我們想這個在我們索引中的文件只有現在的 _version 為 1 時,本次更新才能成功。

此請求成功,並且響應體告訴我們 _version 已經遞增到 2 :

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

然而,如果我們重新執行相同的索引請求,仍然指定 version=1 , Elasticsearch 返回 409 Conflict HTTP 響應碼,和一個如下所示的響應體:

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
        "index_uuid": "FQEnqqIUTx2xHDFFgN6Wew",
        "shard": "3",
        "index": "website"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
    "index_uuid": "FQEnqqIUTx2xHDFFgN6Wew",
    "shard": "3",
    "index": "website"
  },
  "status": 409
}

這告訴我們在 Elasticsearch 中這個文件的當前 _version 號是 2 ,但我們指定的更新版本號為 1 。

我們現在怎麼做取決於我們的應用需求。我們可以告訴使用者說其他人已經修改了文件,並且在再次儲存之前檢查這些修改內容。 或者,在之前的商品 stock_count 場景,我們可以獲取到最新的文件並嘗試重新應用這些修改。

所有文件的更新或刪除 API,都可以接受 version 引數,這允許你在程式碼中使用樂觀的併發控制,這是一種明智的做法。

通過外部系統使用版本控制

一個常見的設定是使用其它資料庫作為主要的資料儲存,使用 Elasticsearch 做資料檢索, 這意味著主資料庫的所有更改發生時都需要被複制到 Elasticsearch ,如果多個程序負責這一資料同步,你可能遇到類似於之前描述的併發問題。

如果你的主資料庫已經有了版本號 — 或一個能作為版本號的欄位值比如 timestamp — 那麼你就可以在 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 的當前版本號。

部分文件更新

文件是不可變的:他們不能被修改,只能被替換。 Update API 必須遵循同樣的規則。 從外部來看,我們在一個文件的某個位置進行部分更新。然而在內部, Update API 簡單使用與之前描述相同的 檢索-修改-重建索引 的處理過程。 區別在於這個過程發生在分片內部,這樣就避免了多次請求的網路開銷。通過減少檢索和重建索引步驟之間的時間,我們也減少了其他程序的變更帶來衝突的可能性。

update 請求最簡單的一種形式是接收文件的一部分作為 doc 的引數, 它只是與現有的文件進行合併。物件被合併到一起,覆蓋現有的欄位,增加新的欄位。 例如,我們增加欄位 tags 和 views 到我們的部落格文章,如下所示:

POST /website/blog/1/_update
{
   "doc" : {
      "tags" : [ "testing" ],
      "views": 0
   }
}

如果請求成功,我們看到類似於 index 請求的響應:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}

檢視新文件

GET /website/blog/1

結果

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "title": "My first blog entry",
    "text": "Starting to get the hang of this...",
    "views": 0,
    "tags": [
      "testing"
    ]
  }
}

指令碼更新文件部分

指令碼可以在 update API中用來改變 _source 的欄位內容, 它在更新指令碼中稱為 ctx._source 。 例如,我們可以使用指令碼來增加部落格文章中 views 的數量:

POST /website/blog/1/_update
{
   "script" : "ctx._source.views+=1"
}

結果

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}

失敗重試

檢索 和 重建索引 步驟的間隔越小,變更衝突的機會越小。 但是它並不能完全消除衝突的可能性。 還是有可能在 update 設法重新索引之前,來自另一程序的請求修改了文件。

為了避免資料丟失, Update API 在 檢索 步驟時檢索得到文件當前的 version 號,並傳遞版本號到 _重建索引 步驟的 index 請求。 如果另一個程序修改了處於檢索和重新索引步驟之間的文件,那麼 _version 號將不匹配,更新請求將會失敗。

對於部分更新的很多使用場景,文件已經被改變也沒有關係。 例如,如果兩個程序都對頁面訪問量計數器進行遞增操作,它們發生的先後順序其實不太重要; 如果衝突發生了,我們唯一需要做的就是嘗試再次更新。

這可以通過設定引數 retry_on_conflict 來自動完成, 這個引數規定了失敗之前 update 應該重試的次數,它的預設值為 0 。

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