概要
本篇主要介紹增量更新(partial update,也叫區域更新)的核心原理,介紹6.3.1版本的Elasticsearch腳本使用實體和增量更新的優勢,
增量更新程序與原理
簡單回顧
前文我們有簡單介紹過增量的語法,簡單回顧一下請求示例:
POST /music/children/1/_update
{
"doc": {
"length": "76"
}
}
一般從客戶端到Elasticsearch,完整的應用請求流程基本是這樣的:
- 客戶端先發起GET請求,獲取到document資訊,展現到前端頁面上,供用戶進行編輯,
- 用戶編輯完資料后,點擊提交,
- 后臺系統處理修改后的資料,并組裝好完整的document報文,
- 發送PUT請求到ES,進行全量替換,
- ES將老的document標記為deleted,然后重新創建一個新的document,
Elasticsearch的document是基于不可變模式設計的,所有的document更新,其實都創建了一個新的document出來,再把老的document標記為deleted,增量更新也不例外,只是GET全量document資料,整合新的document,替換老的document這三步操作全在一個shard里完成,毫秒級完成,
增量更新分片之間的互動

增量更新document的步驟:
- Java客戶端向ES集群發送更新請求,
- Coodinate Node收到請求,但該document不在當前node上,它將請求轉發到Node2節點的P0 shard上,
- Node 2檢索document,修改_source下的JSON,并且重新索引該document,如果有其他執行緒修改過該document,有版本沖突的話,會重新嘗試更新document,最大重試retry_on_conflict次,超出重試次數后放棄,
- 如果步驟3操作成功,Node2會將該document的完整內容異步轉發到Node1和Node3的replica shard,重新建立索引,一旦所有replica都回傳成功,Node2回傳成功訊息給Coodinate Node,
- 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策略解決沖突的問題,
增量更新的優點
- 所有的查詢、修改和回寫操作,都是在ES內部完成的,減小了網路資料傳輸開銷(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,結果符合預期,
外部腳本
對剛剛那個自增需求做一些改動,支持批量更新播放量,自增的數量由引數傳入,腳本也可以通過匯入的方式,預先編譯存盤在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架構社區

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/2114.html
標籤:其他
