前言
在分析ES的索引的創(chuàng)建過(guò)程中看到了些和version相關(guān)的變量(例如:versionForIndexing)。這些個(gè)變量是用于沖突處理的偏序。
在ES的應(yīng)用場(chǎng)景中,使用index API更新文檔胖替,可以一次性讀取原始文檔研儒,做修改豫缨,然后重新索引整個(gè)文檔,最近的索引請(qǐng)求將獲勝:無(wú)論最后哪一個(gè)文檔被索引殉摔,都將唯一存儲(chǔ)在ElasticSearch中,如果其他人同時(shí)更改了這個(gè)文檔记焊,他們的更改將丟失逸月。
很多時(shí)候丟失信息是沒(méi)問(wèn)題的。也許我們的主數(shù)據(jù)存儲(chǔ)是一個(gè)關(guān)系型數(shù)據(jù)庫(kù)遍膜,我們只是將數(shù)據(jù)復(fù)制到ElasticSearch中并使其可被搜索碗硬;也許兩個(gè)人同時(shí)更改文檔的幾率很小 ∑奥或者對(duì)于某些業(yè)務(wù)來(lái)說(shuō)偶爾丟失更改并不是很嚴(yán)重的問(wèn)題恩尾。
但,有時(shí)候丟失一個(gè)變更就是非常嚴(yán)重的挽懦。試想我們使用ElasticSearch存儲(chǔ)我們網(wǎng)上商城商品庫(kù)存的數(shù)量翰意,每次我們賣一個(gè)商品的時(shí)候,在ElasticSearch中將庫(kù)存數(shù)量減少(促銷時(shí)一個(gè)時(shí)間點(diǎn)賣很多商品)信柿〖脚迹或是金融系統(tǒng)兩個(gè)人同時(shí)對(duì)一個(gè)賬戶進(jìn)行取錢操作。都存在如下圖中存在的問(wèn)題:
在數(shù)據(jù)庫(kù)領(lǐng)域中渔嚷,有兩種方法通常備用來(lái)確保并發(fā)更新時(shí)變更不會(huì)丟失:
1进鸠、悲觀并發(fā)控制(Pessimistic concurrency control)
這種方法被關(guān)系型數(shù)據(jù)庫(kù)廣泛使用,它假定有變更沖突可能發(fā)生形病,因此阻塞訪問(wèn)資源以防止沖突客年。一個(gè)典型的例子是讀取一行數(shù)據(jù)之前先將其鎖住,確保只有放置鎖的線程能夠?qū)@行數(shù)據(jù)進(jìn)行修改漠吻。
2量瓜、樂(lè)觀并發(fā)控制(Optimistic concurrency control)
Elasticsearch 中使用的這種方法,它假定沖突是不可能發(fā)生的途乃,所以不會(huì)阻塞正在嘗試的操作榔至。 然而,如果源數(shù)據(jù)在讀寫當(dāng)中被修改欺劳,更新將會(huì)失敗唧取。應(yīng)用程序接下來(lái)將決定該如何解決沖突。 例如划提,可以獲取新的數(shù)據(jù)枫弟,重試更新、或者將相關(guān)情況報(bào)告給用戶鹏往。
ElasticSearch是分布式的淡诗,當(dāng)文檔創(chuàng)建骇塘、更新、刪除時(shí)韩容,新版本的文檔必須復(fù)制到集群中其他節(jié)點(diǎn)款违,同時(shí),ElasticSearch也是異步和并發(fā)的群凶。這就意味著這些復(fù)制請(qǐng)求被并行發(fā)送插爹,并且到達(dá)目的地時(shí)也許順序是亂的(老版本可能在新版本之后到達(dá))。ElasticSaerch需要一種方法確保文檔的舊版本不會(huì)覆蓋新的版本:ES利用_version (版本號(hào))的方式來(lái)確保應(yīng)用中相互沖突的變更不會(huì)導(dǎo)致數(shù)據(jù)丟失请梢。需要修改數(shù)據(jù)時(shí)赠尾,需要指定想要修改文檔的version號(hào),如果該版本不是當(dāng)前版本號(hào)毅弧,請(qǐng)求將會(huì)失敗气嫁。
ElasticSearch中有內(nèi)部版本號(hào)和外部版本號(hào)之分。使用內(nèi)部版本號(hào)是要求指定的version字段和當(dāng)前的version號(hào)相同够坐。但在使用外部版本號(hào)時(shí)要求當(dāng)前version號(hào)小于指定的版本號(hào)寸宵。如果請(qǐng)求成功,外部版本號(hào)作為文檔新的version號(hào)進(jìn)行存儲(chǔ)元咙。
外部版本號(hào)命令:
PUT /website/blog/2?version=5&version_type=external
內(nèi)部版本號(hào)命令:
PUT /website/blog/1?version=1
樂(lè)觀并發(fā)控制
Elasticsearch是分布式的邓馒。當(dāng)文檔被創(chuàng)建、更新或刪除蛾坯,文檔的新版本會(huì)被復(fù)制到集群的其它節(jié)點(diǎn)光酣。Elasticsearch即是同步的又是異步的,意思是這些復(fù)制請(qǐng)求都是平行發(fā)送的脉课,并無(wú)序(out of sequence)的到達(dá)目的地救军。這就需要一種方法確保老版本的文檔永遠(yuǎn)不會(huì)覆蓋新的版本。
Elasticsearch在index倘零、put唱遭、get、delete請(qǐng)求時(shí)呈驶,我們指出每個(gè)文檔都有一個(gè)_version號(hào)碼拷泽,這個(gè)號(hào)碼在文檔被改變時(shí)加一。Elasticsearch使用這個(gè)_version保證所有修改都被正確排序袖瞻。當(dāng)一個(gè)舊版本出現(xiàn)在新版本之后司致,它會(huì)被簡(jiǎn)單的忽略。
我們利用_version的這一優(yōu)點(diǎn)確保數(shù)據(jù)不會(huì)因?yàn)樾薷臎_突而丟失聋迎。我們可以指定文檔的version來(lái)做想要的更改脂矫。如果那個(gè)版本號(hào)不是現(xiàn)在的,我們的請(qǐng)求就失敗了霉晕。
使用內(nèi)部版本控制系統(tǒng)
Let's create a new blog post: 讓我們創(chuàng)建一個(gè)新的博文:
PUT /website/blog/1/_create
{
"title": "My first blog entry",
"text": "Just trying this out..."
}
響應(yīng)體告訴我們這是一個(gè)新建的文檔庭再,它的_version是1±剔龋現(xiàn)在假設(shè)我們要編輯這個(gè)文檔:把數(shù)據(jù)加載到web表單中,修改拄轻,然后保存成新版本颅围。
首先我們檢索文檔:
GET /website/blog/1
響應(yīng)體包含相同的_version是1
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out..."
}
}
現(xiàn)在,當(dāng)我們通過(guò)重新索引文檔保存修改時(shí)恨搓,我們這樣指定了version參數(shù):
PUT /website/blog/1?version=1 <1>
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
<1> 我們只希望文檔的_version是1時(shí)更新才生效院促。
This request succeeds, and the response body tells us that the _version has been incremented to 2:
請(qǐng)求成功,響應(yīng)體告訴我們_version已經(jīng)增加到2:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2
"created": false
}
然而奶卓,如果我們重新運(yùn)行相同的索引請(qǐng)求一疯,依舊指定version=1撼玄,Elasticsearch將返回409 Conflict狀態(tài)的HTTP響應(yīng)夺姑。響應(yīng)體類似這樣:
{
"error" : "VersionConflictEngineException[[website][2] [blog][1]:
version conflict, current [2], provided [1]]",
"status" : 409
}
這告訴我們當(dāng)前_version是2,但是我們指定想要更新的版本是1掌猛。
我們需要做什么取決于程序的需求盏浙。我們可以告知用戶其他人修改了文檔,你應(yīng)該在保存前再看一下荔茬。而對(duì)于上文提到的商品stock_count废膘,我們需要重新檢索最新文檔然后申請(qǐng)新的更改操作。
所有更新和刪除文檔的請(qǐng)求都接受version參數(shù)慕蔚,它可以允許在你的代碼中增加樂(lè)觀鎖控制丐黄。
使用外部版本控制系統(tǒng)
一種常見(jiàn)的結(jié)構(gòu)是使用一些其他的數(shù)據(jù)庫(kù)做為主數(shù)據(jù)庫(kù),然后使用Elasticsearch搜索數(shù)據(jù)孔飒,這意味著所有主數(shù)據(jù)庫(kù)發(fā)生變化灌闺,就要將其拷貝到Elasticsearch中。如果有多個(gè)進(jìn)程負(fù)責(zé)這些數(shù)據(jù)的同步坏瞄,就會(huì)遇到上面提到的并發(fā)問(wèn)題桂对。
如果主數(shù)據(jù)庫(kù)有版本字段——或一些類似于timestamp等可以用于版本控制的字段——是你就可以在Elasticsearch的查詢字符串后面添加version_type=external來(lái)使用這些版本號(hào)。版本號(hào)必須是整數(shù)鸠匀,大于零小于9.2e+18——Java中的正的long蕉斜。
外部版本號(hào)與之前說(shuō)的內(nèi)部版本號(hào)在處理的時(shí)候有些不同。它不再檢查_(kāi)version是否與請(qǐng)求中指定的一致缀棍,而是檢查是否小于指定的版本宅此。如果請(qǐng)求成功,外部版本號(hào)就會(huì)被存儲(chǔ)到_version中爬范。
外部版本號(hào)不僅在索引和刪除請(qǐng)求中指定诽凌,也可以在創(chuàng)建(create)新文檔中指定。
例如坦敌,創(chuàng)建一個(gè)包含外部版本號(hào)5的新博客侣诵,我們可以這樣做:
PUT /website/blog/2?version=5&version_type=external
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
在響應(yīng)中痢法,我們能看到當(dāng)前的_version號(hào)碼是5:
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"created": true
}
現(xiàn)在我們更新這個(gè)文檔,指定一個(gè)新version號(hào)碼為10:
PUT /website/blog/2?version=10&version_type=external
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
請(qǐng)求成功的設(shè)置了當(dāng)前_version為10:
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"created": false
}
如果你重新運(yùn)行這個(gè)請(qǐng)求杜顺,就會(huì)返回一個(gè)像之前一樣的沖突錯(cuò)誤财搁,因?yàn)橹付ǖ耐獠堪姹咎?hào)不大于當(dāng)前在Elasticsearch中的版本。
文檔局部更新
在上面我們說(shuō)了一種通過(guò)檢索躬络,修改尖奔,然后重建整文檔的索引方法來(lái)更新文檔。這是對(duì)的穷当。然而提茁,使用update API,我們可以使用一個(gè)請(qǐng)求來(lái)實(shí)現(xiàn)局部更新馁菜,例如增加數(shù)量的操作茴扁。
我們也說(shuō)過(guò)文檔是不可變的——它們不能被更改,只能被替換汪疮。update API必須遵循相同的規(guī)則峭火。表面看來(lái),我們似乎是局部更新了文檔的位置智嚷,內(nèi)部卻是像我們之前說(shuō)的一樣簡(jiǎn)單的使用update API處理相同的檢索-修改-重建索引流程卖丸,我們也減少了其他進(jìn)程可能導(dǎo)致沖突的修改。
最簡(jiǎn)單的update請(qǐng)求表單接受一個(gè)局部文檔參數(shù)doc盏道,它會(huì)合并到現(xiàn)有文檔中——對(duì)象合并在一起稍浆,存在的標(biāo)量字段被覆蓋,新字段被添加猜嘱。舉個(gè)例子衅枫,我們可以使用以下請(qǐng)求為博客添加一個(gè)tags字段和一個(gè)views字段:
POST /website/blog/1/_update
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}
如果請(qǐng)求成功,我們將看到類似index請(qǐng)求的響應(yīng)結(jié)果:
{
"_index" : "website",
"_id" : "1",
"_type" : "blog",
"_version" : 3
}
檢索文檔文檔顯示被更新的_source字段:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 3,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Starting to get the hang of this...",
"tags": [ "testing" ], <1>
"views": 0 <1>
}
}
<1> 我們新添加的字段已經(jīng)被添加到_source字段中泉坐。
使用腳本局部更新
使用Groovy腳本
這時(shí)候當(dāng)API不能滿足要求時(shí)为鳄,Elasticsearch允許你使用腳本實(shí)現(xiàn)自己的邏輯。腳本支持非常多的API腕让,例如搜索孤钦、排序、聚合和文檔更新纯丸。腳本可以通過(guò)請(qǐng)求的一部分偏形、檢索特殊的.scripts
索引或者從磁盤加載方式執(zhí)行。
默認(rèn)的腳本語(yǔ)言是Groovy觉鼻,一個(gè)快速且功能豐富的腳本語(yǔ)言俊扭,語(yǔ)法類似于Javascript。它在一個(gè)沙盒(sandbox)中運(yùn)行坠陈,以防止惡意用戶毀壞Elasticsearch或攻擊服務(wù)器萨惑。
你可以在《腳本參考文檔》中獲得更多信息捐康。
腳本能夠使用update API改變_source字段的內(nèi)容,它在腳本內(nèi)部以ctx._source表示庸蔼。例如解总,我們可以使用腳本增加博客的views數(shù)量:
POST /website/blog/1/_update
{
"script" : "ctx._source.views+=1"
}
我們還可以使用腳本增加一個(gè)新標(biāo)簽到tags數(shù)組中。在這個(gè)例子中姐仅,我們定義了一個(gè)新標(biāo)簽做為參數(shù)而不是硬編碼在腳本里花枫。這允許Elasticsearch未來(lái)可以重復(fù)利用腳本,而不是在想要增加新標(biāo)簽時(shí)必須每次編譯新腳本:
POST /website/blog/1/_update
{
"script" : "ctx._source.tags+=new_tag",
"params" : {
"new_tag" : "search"
}
}
獲取最后兩個(gè)有效請(qǐng)求的文檔:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 5,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Starting to get the hang of this...",
"tags": ["testing", "search"], <1>
"views": 1 <2>
}
}
<1> search標(biāo)簽已經(jīng)被添加到tags數(shù)組掏膏。
<2> views字段已經(jīng)被增加劳翰。
通過(guò)設(shè)置ctx.op為delete我們可以根據(jù)內(nèi)容刪除文檔:
POST /website/blog/1/_update
{
"script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'",
"params" : {
"count": 1
}
}
更新可能不存在的文檔
想象我們要在Elasticsearch中存儲(chǔ)瀏覽量計(jì)數(shù)器。每當(dāng)有用戶訪問(wèn)頁(yè)面馒疹,我們?cè)黾舆@個(gè)頁(yè)面的瀏覽量佳簸。但如果這是個(gè)新頁(yè)面,我們并不確定這個(gè)計(jì)數(shù)器存在與否行冰。當(dāng)我們?cè)噲D更新一個(gè)不存在的文檔溺蕉,更新將失敗伶丐。
在這種情況下悼做,我們可以使用upsert參數(shù)定義文檔來(lái)使其不存在時(shí)被創(chuàng)建。
POST /website/pageviews/1/_update
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 1
}
}
第一次執(zhí)行這個(gè)請(qǐng)求哗魂,upsert值被索引為一個(gè)新文檔肛走,初始化views字段為1.接下來(lái)文檔已經(jīng)存在,所以script被更新代替录别,增加views數(shù)量朽色。
更新和沖突
這這一節(jié)的介紹中,我們介紹了如何在檢索(retrieve)和重建索引(reindex)中保持更小的窗口组题,如何減少?zèng)_突性變更發(fā)生的概率葫男,不過(guò)這些無(wú)法被完全避免,像一個(gè)其他進(jìn)程在update進(jìn)行重建索引時(shí)修改了文檔這種情況依舊可能發(fā)生崔列。
為了避免丟失數(shù)據(jù)梢褐,update API在檢索(retrieve)階段檢索文檔的當(dāng)前_version,然后在重建索引(reindex)階段通過(guò)index請(qǐng)求提交赵讯。如果其他進(jìn)程在檢索(retrieve)和重加索引(reindex)階段修改了文檔盈咳,_version將不能被匹配,然后更新失敗边翼。
對(duì)于多用戶的局部更新鱼响,文檔被修改了并不要緊。例如组底,兩個(gè)進(jìn)程都要增加頁(yè)面瀏覽量丈积,增加的順序我們并不關(guān)心——如果沖突發(fā)生筐骇,我們唯一要做的僅僅是重新嘗試更新既可。
這些可以通過(guò)retry_on_conflict參數(shù)設(shè)置重試次數(shù)來(lái)自動(dòng)完成江滨,這樣update操作將會(huì)在發(fā)生錯(cuò)誤前重試——這個(gè)值默認(rèn)為0拥褂。
POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}
<1> 在錯(cuò)誤發(fā)生前重試更新5次
這適用于像增加計(jì)數(shù)這種順序無(wú)關(guān)的操作,但是還有一種順序非常重要的情況牙寞。例如index API饺鹃,使用“保留最后更新(last-write-wins)”的update API,但它依舊接受一個(gè)version參數(shù)以允許你使用樂(lè)觀并發(fā)控制(optimistic concurrency control)來(lái)指定你要更細(xì)文檔的版本间雀。
參考:
https://blog.csdn.net/huakai_sun/article/details/79170170