1. 程式人生 > 實用技巧 >ElasticSearch 文件併發處理以及文件路由

ElasticSearch 文件併發處理以及文件路由

此文轉載自:https://my.oschina.net/u/3669799/blog/4753910
大咖揭祕Java人都栽在了哪?點選免費領取《大廠面試清單》,攻克面試難關~>>>

@[toc] ElasticSearch 系列教程我們前面已經連著發了兩篇了,今天第三篇,我們來聊一聊 Es 中的文件併發處理和文件路由問題。

本文是鬆哥所錄視訊教程的一個筆記,筆記簡明扼要,完整內容小夥伴們可以參考視訊,視訊下載連結:https://pan.baidu.com/s/1TwyOm2i28fDZh7rkNF-jww 提取碼: aee2

1. ElasticSearch 文件基本操作

1.1 新建文件

首先新建一個索引。

然後向索引中新增一個文件:

PUT blog/_doc/1
{
  "title":"6. ElasticSearch 文件基本操作",
  "date":"2020-11-05",
  "content":"微信公眾號**江南一點雨**後臺回覆 **elasticsearch06** 下載本筆記。首先新建一個索引。"
}

1 表示新建文件的 id。

新增成功後,響應的 json 如下:

{
  "_index" : "blog",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}
  • _index 表示文件索引。
  • _type 表示文件的型別。
  • _id 表示文件的 id。
  • _version 表示文件的版本(更新文件,版本會自動加 1,針對一個文件的)。
  • result 表示執行結果。
  • _shards 表示分片資訊。
  • _seq_no_primary_term 這兩個也是版本控制用的(針對當前 index)。

新增成功後,可以檢視新增的文件:

當然,新增文件時,也可以不指定 id,此時系統會預設給出一個 id,如果不指定 id,則需要使用 POST 請求,而不能使用 PUT 請求。

POST blog/_doc
{
  "title":"666",
  "date":"2020-11-05",
  "content":"微信公眾號**江南一點雨**後臺回覆 **elasticsearch06** 下載本筆記。首先新建一個索引。"
}

1.2 獲取文件

Es 中提供了 GET API 來檢視儲存在 es 中的文件。使用方式如下:

GET blog/_doc/RuWrl3UByGJWB5WucKtP

上面這個命令表示獲取一個 id 為 RuWrl3UByGJWB5WucKtP 的文件。

如果獲取不存在的文件,會返回如下資訊:

{
  "_index" : "blog",
  "_type" : "_doc",
  "_id" : "2",
  "found" : false
}

如果僅僅只是想探測某一個文件是否存在,可以使用 head 請求:

如果文件不存在,響應如下:

如果文件存在,響應如下:

當然也可以批量獲取文件。

GET blog/_mget
{
  "ids":["1","RuWrl3UByGJWB5WucKtP"]
}

這裡可能有小夥伴有疑問,GET 請求竟然可以攜帶請求體?

某些特定的語言,例如 JavaScript 的 HTTP 請求庫是不允許 GET 請求有請求體的,實際上在 RFC7231 文件中,並沒有規定 GET 請求的請求體該如何處理,這樣造成了一定程度的混亂,有的 HTTP 伺服器支援 GET 請求攜帶請求體,有的 HTTP 伺服器則不支援。雖然 es 工程師傾向於使用 GET 做查詢,但是為了保證相容性,es 同時也支援使用 POST 查詢。例如上面的批量查詢案例,也可以使用 POST 請求。

1.3 文件更新

1.3.1 普通更新

注意,文件更新一次,version 就會自增 1。

可以直接更新整個文件:

PUT blog/_doc/RuWrl3UByGJWB5WucKtP
{
  "title":"666"
}

這種方式,更新的文件會覆蓋掉原文件。

大多數時候,我們只是想更新文件欄位,這個可以通過指令碼來實現。

POST blog/_update/1
{
  "script": {
    "lang": "painless",
    "source":"ctx._source.title=params.title",
    "params": {
      "title":"666666"
    }
  }
}

更新的請求格式: POST {index}/_update/{id}

在指令碼中,lang 表示指令碼語言,painless 是 es 內建的一種指令碼語言。source 表示具體執行的指令碼,ctx 是一個上下文物件,通過 ctx 可以訪問到 _source_title 等。

也可以向文件中新增欄位:

POST blog/_update/1
{
  "script": {
    "lang": "painless",
    "source":"ctx._source.tags=[\"java\",\"php\"]"
  }
}

新增成功後的文件如下:

通過指令碼語言,也可以修改陣列。例如再增加一個 tag:

POST blog/_update/1
{
  "script":{
    "lang": "painless",
    "source":"ctx._source.tags.add(\"js\")"
  }
}

當然,也可以使用 if else 構造稍微複雜一點的邏輯。

POST blog/_update/1
{
  "script": {
    "lang": "painless",
    "source": "if (ctx._source.tags.contains(\"java\")){ctx.op=\"delete\"}else{ctx.op=\"none\"}"
  }
}

1.3.2 查詢更新

通過條件查詢找到文件,然後再去更新。

例如將 title 中包含 666 的文件的 content 修改為 888。

POST blog/_update_by_query
{
  "script": {
    "source": "ctx._source.content=\"888\"",
    "lang": "painless"
  },
  "query": {
    "term": {
      "title":"666"
    }
  }
}

1.4 刪除文件

1.4.1 根據 id 刪除

從索引中刪除一個文件。

刪除一個 id 為 TuUpmHUByGJWB5WuMasV 的文件。

DELETE blog/_doc/TuUpmHUByGJWB5WuMasV

如果在新增文件時指定了路由,則刪除文件時也需要指定路由,否則刪除失敗。

1.4.2 查詢刪除

查詢刪除是 POST 請求。

例如刪除 title 中包含 666 的文件:

POST blog/_delete_by_query
{
  "query":{
    "term":{
      "title":"666"
    }
  }
}

也可以刪除某一個索引下的所有文件:

POST blog/_delete_by_query
{
  "query":{
    "match_all":{
      
    }
  }
}

1.5 批量操作

es 中通過 Bulk API 可以執行批量索引、批量刪除、批量更新等操作。

首先需要將所有的批量操作寫入一個 JSON 檔案中,然後通過 POST 請求將該 JSON 檔案上傳並執行。

例如新建一個名為 aaa.json 的檔案,內容如下:

首先第一行:index 表示要執行一個索引操作(這個表示一個 action,其他的 action 還有 create,delete,update)。_index 定義了索引名稱,這裡表示要建立一個名為 user 的索引,_id 表示新建文件的 id 為 666。

第二行是第一行操作的引數。

第三行的 update 則表示要更新。

第四行是第三行的引數。

注意,結尾要空出一行。

aaa.json 檔案建立成功後,在該目錄下,執行請求命令,如下:

curl -XPOST "http://localhost:9200/user/_bulk" -H "content-type:application/json" --data-binary @aaa.json

執行完成後,就會建立一個名為 user 的索引,同時向該索引中新增一條記錄,再修改該記錄,最終結果如下:

2. ElasticSearch 文件路由

es 是一個分散式系統,當我們儲存一個文件到 es 上之後,這個文件實際上是被儲存到 master 節點中的某一個主分片上。

例如新建一個索引,該索引有兩個分片,0個副本,如下:

接下來,向該索引中儲存一個文件:

PUT blog/_doc/a
{
  "title":"a"
}

文件儲存成功後,可以檢視該文件被儲存到哪個分片中去了:

GET _cat/shards/blog?v

檢視結果如下:

index shard prirep state   docs store ip        node
blog  1     p      STARTED    0  208b 127.0.0.1 slave01
blog  0     p      STARTED    1 3.6kb 127.0.0.1 master

從這個結果中,可以看出,文件被儲存到分片 0 中。

那麼 es 中到底是按照什麼樣的規則去分配分片的?

es 中的路由機制是通過雜湊演算法,將具有相同雜湊值的文件放到一個主分片中,分片位置的計算方式如下:

shard=hash(routing) % number_of_primary_shards

routing 可以是一個任意字串,es 預設是將文件的 id 作為 routing 值,通過雜湊函式根據 routing 生成一個數字,然後將該數字和分片數取餘,取餘的結果就是分片的位置。

預設的這種路由模式,最大的優勢在於負載均衡,這種方式可以保證資料平均分配在不同的分片上。但是他有一個很大的劣勢,就是查詢時候無法確定文件的位置,此時它會將請求廣播到所有的分片上去執行。另一方面,使用預設的路由模式,後期修改分片數量不方便。

當然開發者也可以自定義 routing 的值,方式如下:

PUT blog/_doc/d?routing=javaboy
{
  "title":"d"
}

如果文件在新增時指定了 routing,則查詢、刪除、更新時也需要指定 routing。

GET blog/_doc/d?routing=javaboy

自定義 routing 有可能會導致負載不均衡,這個還是要結合實際情況選擇。

典型場景:

對於使用者資料,我們可以將 userid 作為 routing,這樣就能保證同一個使用者的資料儲存在同一個分片中,檢索時,同樣使用 userid 作為 routing,這樣就可以精準的從某一個分片中獲取資料。

3. ElasticSearch 版本控制

當我們使用 es 的 API 去進行文件更新時,它首先讀取原文件出來,然後對原文件進行更新,更新完成後再重新索引整個文件。不論你執行多少次更新,最終儲存在 es 中的是最後一次更新的文件。但是如果有兩個執行緒同時去更新,就有可能出問題。

要解決問題,就是鎖。

3.1 鎖

悲觀鎖

很悲觀,每一次去讀取資料的時候,都認為別人可能會修改資料,所以遮蔽一切可能破壞資料完整性的操作。關係型資料庫中,悲觀鎖使用較多,例如行鎖、表鎖等等。

樂觀鎖

很樂觀,每次讀取資料時,都認為別人不會修改資料,因此也不鎖定資料,只有在提交資料時,才會檢查資料完整性。這種方式可以省去鎖的開銷,進而提高吞吐量。

在 es 中,實際上使用的就是樂觀鎖。

3.2 版本控制

es6.7之前

在 es6.7 之前,使用 version+version_type 來進行樂觀併發控制。根據前面的介紹,文件每被修改一個,version 就會自增一次,es 通過 version 欄位來確保所有的操作都有序進行。

version 分為內部版本控制和外部版本控制。

3.2.1 內部版本

es 自己維護的就是內部版本,當建立一個文件時,es 會給文件的版本賦值為 1。

每當使用者修改一次文件,版本號就回自增 1。

如果使用內部版本,es 要求 version 引數的值必須和 es 文件中 version 的值相當,才能操作成功。

3.2.2 外部版本

也可以維護外部版本。

在新增文件時,就指定版本號:

PUT blog/_doc/1?version=200&version_type=external
{
  "title":"2222"
}

以後更新的時候,版本要大於已有的版本號。

  • vertion_type=external 或者 vertion_type=external_gt 表示以後更新的時候,版本要大於已有的版本號。
  • vertion_type=external_gte 表示以後更新的時候,版本要大於等於已有的版本號。

3.2.3 最新方案(Es6.7 之後)

現在使用 if_seq_noif_primary_term 兩個引數來做併發控制。

seq_no 不屬於某一個文件,它是屬於整個索引的(version 則是屬於某一個文件的,每個文件的 version 互不影響)。現在更新文件時,使用 seq_no 來做併發。由於 seq_no 是屬於整個 index 的,所以任何文件的修改或者新增,seq_no 都會自增。

現在就可以通過 seq_noprimary_term 來做樂觀併發控制。

PUT blog/_doc/2?if_seq_no=5&if_primary_term=1
{
  "title":"6666"
}

最後,鬆哥還蒐集了 50+ 個專案需求文件,想做個專案練練手的小夥伴不妨看看哦~

需求文件地址:https://github.com/lenve/javadoc