Elasticsearch系列---增量更新原理及優點

概要

本篇主要介紹增量更新(partial update,也叫局部更新)的核心原理,介紹6.3.1版本的Elasticsearch腳本使用實例和增量更新的優點。前端

增量更新過程與原理

簡單回顧

前文咱們有簡單介紹過增量的語法,簡單回顧一下請求示例:java

POST /music/children/1/_update
{
  "doc": {
    "length": "76"
  }
}

通常從客戶端到Elasticsearch,完整的應用請求流程基本是這樣的:node

  • 客戶端先發起GET請求,獲取到document信息,展示到前端頁面上,供用戶進行編輯。
  • 用戶編輯完數據後,點擊提交。
  • 後臺系統處理修改後的數據,並組裝好完整的document報文。
  • 發送PUT請求到ES,進行全量替換。
  • ES將老的document標記爲deleted,而後從新建立一個新的document。

Elasticsearch的document是基於不可變模式設計的,全部的document更新,其實都建立了一個新的document出來,再把老的document標記爲deleted,增量更新也不例外,只是GET全量document數據,整合新的document,替換老的document這三步操做全在一個shard裏完成,毫秒級完成。緩存

增量更新分片之間的交互

12

增量更新document的步驟:安全

  1. Java客戶端向ES集羣發送更新請求。
  2. Coodinate Node收到請求,但該document不在當前node上,它將請求轉發到Node2節點的P0 shard上。
  3. Node 2檢索document,修改_source下的JSON,而且從新索引該document,若是有其餘線程修改過該document,有版本衝突的話,會從新嘗試更新document,最大重試retry_on_conflict次,超出重試次數後放棄。
  4. 若是步驟3操做成功,Node2會將該document的完整內容異步轉發到Node1和Node3的replica shard,從新創建索引。一旦全部replica都返回成功,Node2返回成功消息給Coodinate Node。
  5. Coodinate Node響應更新成功消息給客戶端,此時ES集羣內primary shard和replica shard都已經更新完成。
注意幾點:
  • primary shard向replica shard進行document數據同步時,發送的是document的完整信息,由於異步請求不保證有序,若是發增量信息的話,順序錯亂會致使document內容錯誤。
  • 只要Coodinate Node向Java客戶端響應成功,就表示全部的primary shard向replica shard都完成了更新操做,此時ES集羣內的數據是一致的,更新是安全的。
  • retry策略,ES再次獲取document數據和最新版本號,成功就更新,失敗再試,最大次數能夠設置,如5次:retry_on_conflict=5
  • retry策略在增量操做無關順序的場景更適用,好比說計數操做,誰先執行誰後執行,關係不大,最終結果是對的就行。其餘的一些場景,如庫存的變化,帳戶餘額的變化,直接更新成指定數值的,確定不能使用retry策略,但能夠轉化成加減法,以下單時由直接更新庫存數量的邏輯改爲「當前可用庫存數量=庫存數量-訂單商品數量」,帳戶餘額的更新加減變化的金額,這樣能夠在必定程度上,把順序有關轉化成順序無關,就能夠更方便的使用retry策略解決衝突的問題。

增量更新的優勢

  1. 全部的查詢、修改和回寫操做,都是在ES內部完成的,減少了網絡數據傳輸開銷(2次),提高了性能。
  2. 相比全量替換的時間間隔(秒級以上),縮短了查詢和修改的時間間隔(毫秒級),能夠有效下降併發衝突的狀況。

使用腳本實現增量更新

Elasticsearch支持使用腳本實現更爲靈活的邏輯,6.0版本之後,默認支持的腳本是painless,而且再也不支持Groovy,由於Groovy編譯有必定機率會出現內存不釋放,最終致使Full GC的問題。網絡

咱們以英文兒歌的案例爲背景,假設document的數據是這樣:架構

{
  "_index": "music",
  "_type": "children",
  "_id": "2",
  "_version": 6,
  "found": true,
  "_source": {
    "name": "wake me, shark me",
    "content": "don't let me sleep too late, gonna get up brightly early in the morning",
    "language": "english",
    "length": "55",
    "likes": 0
  }
}

內置腳本

如今有這樣一個需求:每當有人點擊播放一次歌曲時,該document的likes field就自增1,咱們能夠用簡單的腳原本實現:併發

POST /music/children/2/_update
{
   "script" : "ctx._source.likes++"
}

執行一次後,再查詢該document,發現likes變成1,每執行一次,likes都自增1,結果符合預期。less

外部腳本

對剛剛那個自增需求作一些改動,支持批量更新播放量,自增的數量由參數傳入,腳本也能夠經過導入的方式,預先編譯存儲在ES中,使用的時候調用便可。異步

建立腳本
POST _scripts/music-likes
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.likes += params.new_likes" 
  }
}

腳本ID爲music-likes,參數爲new_likes,是能夠在調用時傳入的。

使用腳本

咱們更新時,執行以下請求,就能夠調用剛剛建立的腳本

POST /music/children/2/_update
{
  "script": {
    "id": "music-likes", 
    "params": {
      "new_likes": 2
    }
  }
}

id即建立腳本時的music-likes,params是固定寫法,裏面的參數爲new_likes,執行後再查看document信息,能夠看到likes field的值按傳入的值進行累加,結果符合預期。

查看腳本

命令:

GET _scripts/music-likes

斜槓後面的參數即腳本ID

刪除腳本

命令:

DELETE _scripts/music-likes

斜槓後面的參數即腳本ID

腳本注意事項

  • ES檢測到新腳本時,會執行腳本編譯,並將它存儲在緩存中,編譯比較耗時。
  • 腳本的編寫能參數化的,就不要硬編碼,提升腳本的複用性。
  • 短期內太多的腳本編譯,若是超出了ES的承受範圍,ES直接報circuit_breaking_exception錯誤,這個範圍默認是15條/分鐘。
  • 腳本緩存默認100條,默認不設過時時間,每一個腳本最大字符65535字節,想自行配置的話能夠改script.cache.expire、script.cache.max_size和script.max_size_in_bytes參數。

一句話,提升腳本的複用性。

upsert語法

像剛剛的案例,實現的是一個播放計數器的功能,目前這個計數器是與內容存儲在一塊兒,若是計數器單獨存儲,可能會出現新上架的一首歌,但計數器的document可能還不存在,試圖對它作更新操做會報document_missing_exception錯誤,這種場景咱們須要使用upsert語法:

POST /music/children/3/_update
{
   "script" : "ctx._source.likes++",
   "upsert": {
     "likes": 0
   }
}

若是id爲3的記錄不存在,第一次請求時,執行upsert裏面的JSON內容,初始化一個新文檔,ID爲3,likes值爲0;第二次請求時,文檔已經存在,此時作script腳本的更新操做,likes自增。

小結

本篇簡單介紹了增量更新的過程與原理,並與全量替換作了簡單的對比,針對一些簡單的計數場景,引入腳本的實現方式案例,腳本能夠實現很豐富的功能,具體能夠查看官網對Painless的介紹。

專一Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區
Java架構社區

相關文章
相關標籤/搜索