ES 26 - 通過partial update區域性更新索引文件 (partial update增量修改原理)
目錄
- 1 什麼是partial update
- 1.1 全量修改文件的原理
- 1.2 修改指定field的思路
- 1.3 partial update的優勢
- 1.4 partial update的使用
- 2 通過指令碼進行partial update操作
- 2.1 內建painless指令碼修改文件
- 2.2 外接Groovy指令碼修改文件
- 2.3 內建painless指令碼upsert文件
- 2.4 外接Groovy指令碼delete文件
- 3 partial update的併發控制策略
- 3.1 控制方式
- 3.2 retry原理
1 什麼是partial update
1.1 全量修改文件的原理
全量修改文件的語法: PUT index/type/1
, 如果id=1的文件不存在, 則建立, 如果存在, 將發生替換原有文件的操作.
全量替換文件的效能比較低, 為了避免替換操作的發生, 引入partial update: 只修改指定的field, 不用全量修改資料.
1.2 修改指定field的思路
① 根據使用者請求, 獲得要修改的文件;
② 在記憶體中封裝使用者提交的新文件, 傳送PUT請求到ES內部;
③ 將要替換的舊文件標記為deleted;
④ 最後將封裝好的新文件存入索引中.
1.3 partial update的優勢
① 所有的查詢、修改和寫回操作, 都在同一個shard中進行, 避免了網路傳輸的開銷.
- 不需要: 從特定shard查詢文件 -> 返回到記憶體 -> 記憶體中修改 -> 將修改的文件傳送到原來的shard -> 寫索引 —— 這個複雜的操作, 顯著提升了效能:
② 減少了查詢和修改的時間間隔, 可以有效減少併發衝突.
1.4 partial update的使用
使用方法: 通過_update
// 新增測試資料:
PUT employee/developer/1
{
"name": "shou feng",
"sex": "male",
"age": 20
}
// partial update修改指定field:
POST employee/developer/1/_update
{
"doc": {
"age": 21
}
}
// 響應結果:
{
"_index": "employee",
"_type": "developer",
"_id": "1",
"_version": 5,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
// 檢視文件, 發現age已經從20變為21了.
GET employee/developer/1
如果不使用_update
, 則會直接覆蓋掉源文件, 導致原文件丟失部分資料:
// 不使用_update:
POST employee/developer/1
{
"doc": {
"age": 22
}
}
// 再次檢視, 發現id=1的該文件就只剩一個age欄位了:
GET employee/developer/1
2 通過指令碼進行partial update操作
ES提供了指令碼支援 —— 可以通過Groovy外接指令碼(已過時)、內建painless
指令碼實現各種複雜操作.
2.1 內建painless指令碼修改文件
插入文件:
PUT employee/developer/1 { "name": "shou feng", "age": 20, "salary": 10000 }
執行指令碼: —— 這裡使用的是更輕快簡短的painless指令碼, 就是直接由字串表示的指令碼:
POST employee/developer/1/_update // 傳送POST請求, 執行partial update { "script": "ctx._source.salary+=500" // 為salary自增500 }
檢視修改結果:
GET employee/developer/1 // 結果如下: { "_index": "employee", "_type": "developer", "_id": "1", "_version": 11, "found": true, "_source": { "name": "shou feng", "age": 20, "salary": 10500 // 自增500成功 } }
2.2 外接Groovy指令碼修改文件
將指令碼檔案存放在
${ES_HOME}/config/scripts
下, 檔名為xxx.groovy
, 內容為:ctx._source.salary+=bonus
—— 增加值為將近bonus的值, 示例如下:修改文件:
POST employee/developer/1/_update { "script": { "lang": "groovy", "file": "change_salary", "params": { "bonus": 500 } } } // 響應結果為: #! Deprecation: [groovy] scripts are deprecated, use [painless] scripts instead { "_index": "employee", "_type": "developer", "_id": "1", "_version": 12, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 } }
檢視修改結果:
GET employee/developer/1 // 結果如下: { "_index": "employee", "_type": "developer", "_id": "1", "_version": 12, "found": true, "_source": { "name": "shou feng", "age": 20, "salary": 9000 } }
說明: 在執行外接Groovy指令碼時, ES提示Groovy指令碼已經過時, 建議我們使用painless
—— 更輕快的表達方式, 即類似於ctx._source.salary+=bonus
的簡短表達方式.
Elasticsearch 5.6(具體開始版本不明確)版本中的預設指令碼使用方式就已經是painless
了.
關於指令碼的詳細使用, 請參見博文: ES 27 - Elasticsearch的painless指令碼使用實踐.
2.3 內建painless指令碼upsert文件
假設不知道id=1的文件被刪除了, 我們現在為其新增
"level": 1
的內容:POST employee/developer/1/_update { "doc": { "level": 1 } }
丟擲 [404 - 文件丟失] 的錯誤:
{ "error": { "root_cause": [ { "type": "document_missing_exception", "reason": "[developer][1]: document missing", "index_uuid": "rT6tChP2QISaVd2OzdCEMA", "shard": "3", "index": "employee" } ], "type": "document_missing_exception", "reason": "[developer][1]: document missing", "index_uuid": "rT6tChP2QISaVd2OzdCEMA", "shard": "3", "index": "employee" }, "status": 404 }
修改upsert策略: 如果指定的文件不存在, 就執行
upsert
中的初始化操作; 如果存在, 就執行doc
或script
中的partial update操作:POST employee/developer/1/_update { "script": "ctx.source.level+=1", "upsert": { "name": "heal", "age": 20 } }
2.4 外接Groovy指令碼delete文件
指令碼路徑: ${ES_HOME}/config/scripts/delete_doc.groovy
指令碼內容: ctx.op = ctx._source.age == age ? 'delete': 'none' ctx.op = ctx._source.age == param ? 'delete' : 'none'
使用示例:
POST employee/developer/1/_update { "script": { "lang": "groovy", "file": "delete_doc", "params": { "age": 20 // 如果年齡是20, 則刪除之 } } }
響應結果:
#! Deprecation: [groovy] scripts are deprecated, use [painless] scripts instead { "_index": "employee", "_type": "developer", "_id": "1", "_version": 13, "result": "deleted", "_shards": { "total": 2, "successful": 1, "failed": 0 } }
檢視文件是否被刪除:
GET employee/developer/1 // 響應結果 - 成功刪除: { "_index": "employee", "_type": "developer", "_id": "1", "found": false }
3 partial update的併發控制策略
partial update內部也是通過樂觀鎖進行併發控制的.
關於併發控制, 請參見博文: Elasticsearch的併發控制策略.
3.1 控制方式
POST index/type/id/_update?retry_on_conflict=5
POST index/type/id/_update?retry_on_conflict=5&version=5
3.2 retry原理
retry_on_conflict
: 發生衝突後的重試次數.
(1) 客戶端A、B幾乎同時獲取同一個文件, 一併獲得_version
版本資訊, 假設此時_version=1
;
(2) 客戶端A修改文件中的部分內容, 將修改寫入索引;
(3) Elasticsearch在寫入索引時, 檢查客戶端A提交的文件的版本資訊(這裡仍然是1) 和 現存的文件的版本資訊(這裡也是1), 發現相同後, 執行寫入操作, 並修改版本號_version=2
;
(4) 客戶端B也修改文件中的部分內容, 其操作寫回索引的速度稍慢. 此時同樣執行過程(3): ES發現客戶端B提交的文件的版本為1, 而現存文件的版本為2 ===> 發生衝突, 此次partial update將失敗;
(5) partial update操作失敗後, 將重複(1) - (3) 過程, 重複的次數, 就是retry_on_conflict
引數的值.
版權宣告
作者: 馬瘦風(https://healchow.com)
出處: 部落格園 馬瘦風的部落格(https://www.cnblogs.com/shoufeng)
感謝閱讀, 如果文章有幫助或啟發到你, 點個[好文要頂