elasticsearch的開發應用(3)

KerryWu發表於2022-11-27

前面的文章裡面主要講的是查詢的用法,還是延續之前的文章格式,這裡講講修改。

1. 單文件修改

1.1. insert

其實在資料準備階段已經有新增的例子了。

DSL
POST /operation_log/_doc
{
  "ip": "0.0.0.0",
  "module": "測試資料"
}
spring
        OperationLog operationLog=new OperationLog();
        operationLog.setIp("0.0.0.0");
        operationLog.setModule("測試資料");
        return esRestTemplate.save(operationLog);

1.2. update-(save)

新增時,springboot 用到的是 save 方法,更新時也一樣可以。不過得拿到文件的id,這裡id=13OkA4QBMgWicIn2wBwM。

DSL
PUT /operation_log/_doc/13OkA4QBMgWicIn2wBwM
{
  "ip": "0.0.0.0",
  "module": "測試資料1"
}
spring
esRestTemplate.save(operationLog);

1.3. update-(document)

DSL
POST /operation_log/_update/13OkA4QBMgWicIn2wBwM
{
  "doc": {
    "module":"測試資料1"
  }
}
spring
        Document document = Document.create();
        document.put("module", "測試資料1");
        UpdateQuery updateQuery = UpdateQuery
                .builder(id)
                .withDocument(document)
                .build();
        esRestTemplate.update(updateQuery,IndexCoordinates.of("operation_log"));

1.4. update-(script)

DSL
POST /operation_log/_update/13OkA4QBMgWicIn2wBwM
{
  "script": {
    "source": "ctx._source.module = params.module",
    "params": {
      "module": "測試資料1"
    }
  }
}
spring
        Map<String, Object> params = new HashMap<>();
        params.put("module", "測試資料1");
        UpdateQuery updateQuery = UpdateQuery
                .builder(id)
                .withScript("ctx._source.module = params.module")
                .withParams(params)
                .build();
        esRestTemplate.update(updateQuery, IndexCoordinates.of("operation_log"));

1.5. delete

DSL
DELETE /operation_log/_doc/13OkA4QBMgWicIn2wBwM
spring
        esRestTemplate.delete(id, OperationLog.class);

2. 批次修改 bulk

批次新增 DSL
POST /operation_log/_bulk
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"測試資料1"}
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"測試資料2"}
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"測試資料3"}
批次更新 DSL
POST /operation_log/_bulk
{"update":{"_id":"2HP9A4QBMgWicIn26BzR"}}
{"doc":{"module":"測試資料11"}}
{"update":{"_id":"2XP9A4QBMgWicIn26BzR"}}
{"script":{"source":"ctx._source.module = params.module","params":{"module":"測試資料22"}}}
批次刪除 DSL
POST /operation_log/_bulk
{"delete":{"_id":"2HP9A4QBMgWicIn26BzR"}}
{"delete":{"_id":"2XP9A4QBMgWicIn26BzR"}}
{"delete":{"_id":"2nP9A4QBMgWicIn26BzR"}}

不知是否注意到,在批次更新的語句中,支援同時 doc、script 兩種更新方式。實際上來說,_bulk 其實支援同時將上述的三種語句一起提交執行。
不過專案上一般不會如此應用,都是單獨分開來。像批次新增,save 方法就支援批次新增操作,雖然底層程式碼還是呼叫 bulkOperation

spring bulkUpdate
    @PatchMapping("bulk-update")
    public void bulkUpdate() {
        Map<String, Object> params = new HashMap<>();
        params.put("module", "測試資料2");
        String scriptStr = "ctx._source.module = params.module";
        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
                .build();
        List<UpdateQuery> updateQueryList = esRestTemplate.search(query, OperationLog.class)
                .stream()
                .map(SearchHit::getContent)
                .map(obj -> UpdateQuery.builder(obj.getId())
                        .withScript(scriptStr)
                        .withParams(params)
                        .build())
                .collect(Collectors.toList());
        esRestTemplate.bulkUpdate(updateQueryList, OperationLog.class);
    }

有關更詳細、更好使用 bulk的部分,建議檢視 es官網資料

3. 修改ByQuery

3.1. updateByQuery

DSL
POST /operation_log/_update_by_query
{
  "script": {
    "source": "ctx._source.module = params.module",
    "params": {
      "module": "測試資料1"
    }
  },
  "query": {
    "term": {
      "ip": "0.0.0.0"
    }
  }
}
spring
    @PatchMapping("update-by-query")
    public void updateByQuery() {
        Map<String, Object> params = new HashMap<>();
        params.put("module", "測試資料2");
        String scriptStr = "ctx._source.module = params.module";
        UpdateQuery updateQuery = UpdateQuery
                .builder(new NativeSearchQueryBuilder()
                        .withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
                        .build())
                .withScript(scriptStr)
                .withScriptType(ScriptType.INLINE)
                .withLang("painless")
                .withParams(params)
                .build();
        esRestTemplate.updateByQuery(updateQuery, IndexCoordinates.of("operation_log"));
    }

可以對比一下上面的 bulkUpdate 方法,發現有些不同:

  • updateByQuery 只支援Script,不支援 Document 的方式更新。
  • updateByQuery 使用 Script 方式更新時,必須傳遞 scriptTypeLang 這些輔助引數。原本 bulkUpdate 中也是要傳的,只不過底層方法封裝了,但是沒有給 updateByQuery 封裝。(實際踩過坑,看封裝方法才得知)

3.2. deleteByQuery

DSL
POST /operation_log/_delete_by_query
{
  "query": {
    "term": {
      "ip": "0.0.0.0"
    }
  }
}
spring
        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
                .build();
        esRestTemplate.delete(query, OperationLog.class);

delete_by_query並不是真正意義上物理文件刪除,而是隻是版本變化並且對文件增加了刪除標記。當我們再次搜尋的時候,會搜尋全部然後過濾掉有刪除標記的文件。因此,該索引所佔的空間並不會隨著該API的操作磁碟空間會馬上釋放掉,只有等到下一次段合併的時候才真正被物理刪除,這個時候磁碟空間才會釋放。相反,在被查詢到的文件標記刪除過程同樣需要佔用磁碟空間,這個時候,你會發現觸發該API操作的時候磁碟不但沒有被釋放,反而磁碟使用率上升了。

3.3. 調優引數

可參考es官網 ElasticSearch API guide,在批次修改文件時,有很多引數可以配合調優。

這裡先列舉幾個常用的,剩下詳細的請看官方文件:

1. refresh

ES的索引資料是寫入到磁碟上的。但這個過程是分階段實現的,因為IO的操作是比較費時的。

  • 先寫到記憶體中,此時不可搜尋。
  • 預設經過 1s 之後會被寫入 lucene 的底層檔案 segment 中 ,此時可以搜尋到。
  • refresh 之後才會寫入磁碟

以上過程由於隨時可能被中斷導致資料丟失,所以每一個過程都會有 translog 記錄,如果中間有任何一步失敗了,等伺服器重啟之後就會重試,保證資料寫入。translog也是先存在記憶體裡的,然後預設5秒刷一次寫到硬碟裡。

在 index ,Update , Delete , Bulk 等操作中,可以設定 refresh 的值。如下:

  • false:預設值。不要重新整理相關的動作。在請求返回後,此請求所做的更改將在某個時刻顯示。如:

    建立一個文件,而不做任何使其可以搜尋的事情:
    PUT /test/test/1
    PUT /test/test/2?refresh=false
  • true或空字串:更新資料之後,立刻對相關的分片(包括副本) 重新整理,這個重新整理操作保證了資料更新的結果可以立刻被搜尋到。

    建立一個文件並立即重新整理索引,使其可見:
    PUT /test/test/1?refresh
    PUT /test/test/2?refresh=true
  • wait_for:等待請求所做的更改在返回之前透過沖刷顯示。這不會強制立即重新整理,而是等待重新整理發生。 Elasticsearch會自動每隔index.refresh_interval重新整理已經更改的分片,預設為1秒。該設定是動態的。

    建立一個文件並等待它成為搜尋可見:
    PUT /test/test/1?refresh=wait_for
2. scroll_size

這個引數是執行刪除的時候,每次每個執行緒會查詢的資料量,然後進行刪除。預設值是100,就是說每個執行緒每次都會查詢出100條資料然後再刪除。

3. slices

可以理解為,預設值是一個執行緒在進行查詢資料並刪除,當設定這個slices值時,會將es下的資料進行切分,啟動多個task去做刪除,理解為多執行緒執行操作。

但是就像不建議濫用多執行緒一樣,不建議設定slices值太大,否則會導致es出問題。建議設為索引分片數量的倍數(如:1倍、2倍),有助於基於每個分片的資料做切分。

4. conflicts

如果按查詢刪除遇到版本衝突,該怎麼辦,有兩個值:

  • abort:預設值,衝突時中止。
  • proceed:衝突時繼續。

舉前面updateByQuery的例子。_update_by_query 在啟動時獲取索引的快照,並使用內部版本控制對其進行索引。這意味著如果文件在拍攝快照和處理索引請求之間發生變化,則會發生版本衝突。當版本匹配文件被更新並且版本號增加。

所有更新和查詢失敗導致 _update_by_query 中止並在響應失敗中返回。已執行的更新仍然堅持。換句話說,程式沒有回滾,只會中止。當第一個故障導致中止時,失敗批次請求返回的所有故障都會返回到故障元素中;因此,有可能會有不少失敗的實體。

如果你想簡單地計算版本衝突,不會導致 _update_by_query中止,你可以在url設定conflicts=proceed 或在請求體設定"conflicts": "proceed"。如上例中改成:

POST /operation_log/_update_by_query?conflicts=proceed

4. 鎖

Elasticsearch和資料庫一樣,在多執行緒併發訪問修改的情況下,會有一個鎖機制來控制每次修改的均為最新的文件,核心是使用樂觀鎖的機制。

_version

在 Elasticsearch 透過 _version 來記錄文件的版本。第一次建立一個document的時候,它的_version內部版本號就是1;以後,每次對這個document執行修改或者刪除操作,都會對這個_version版本號自動加1;哪怕是刪除,也會對這條資料的版本號加1

由於 segment 時不能被修改的,所以當對一個文件執行 DELETE 之後,在插入相同id的文件,version 版本不會是0,而是在 DELETE 操作的version上遞增。

在對文件進行修改和刪除時,version 會遞增,也可以由使用者指定。只有當版本號大於當前版本時,才會修改刪除成功,否則失敗。當併發請求時,先修改成功的,version 會增加,這個時候其他請求就會猶豫 version 不匹配從而修改失敗。

external version

es提供了一個feature,就是說,你可以不用它提供的內部_version版本號來進行併發控制,可以基於你自己維護的一個版本號來進行併發控制。

舉個例子,假如你的資料在mysql裡也有一份,然後你的應用系統本身就維護了一個版本號,無論是什麼自己生成的或程式控制的。這個時候,你進行樂觀鎖併發控制的時候,可能並不是想要用es內部的_version來進行控制,而是用你自己維護的那個version來進行控制。

?version=1   基於_version

?version=1&version_type=external   基於external version

_version與version_type=external唯一的區別在於:

  • _version,只有當你提供的version與es中的_version一模一樣的時候,才可以進行修改,只要不一樣,就報錯。
  • 當version_type=external的時候,只有當你提供的version比es中的_version大的時候,才能完成修改。
es,_version=1,    ?version=1,才能更新成功

es,_version=1,    ?version>1&version_type=external,才能成功,
比如說:?version=2&version_type=external

引用:

相關文章