ElasticSearch——version 版本號(hào)控制

前言

在分析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

https://blog.csdn.net/sangyongjia/article/details/79995550

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末悔详,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惹挟,更是在濱河造成了極大的恐慌茄螃,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件连锯,死亡現(xiàn)場(chǎng)離奇詭異归苍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)运怖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門拼弃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人摇展,你說(shuō)我怎么就攤上這事吻氧。” “怎么了咏连?”我有些...
    開(kāi)封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵盯孙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我祟滴,道長(zhǎng)振惰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任垄懂,我火速辦了婚禮骑晶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埠偿。我一直安慰自己透罢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布冠蒋。 她就那樣靜靜地躺著羽圃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上朽寞,一...
    開(kāi)封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天识窿,我揣著相機(jī)與錄音,去河邊找鬼脑融。 笑死喻频,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肘迎。 我是一名探鬼主播甥温,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼妓布!你這毒婦竟也來(lái)了姻蚓?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤匣沼,失蹤者是張志新(化名)和其女友劉穎狰挡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體释涛,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡加叁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唇撬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片它匕。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖局荚,靈堂內(nèi)的尸體忽然破棺而出超凳,到底是詐尸還是另有隱情愈污,我是刑警寧澤耀态,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站暂雹,受9級(jí)特大地震影響首装,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杭跪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一仙逻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧涧尿,春花似錦系奉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至桥言,卻和暖如春萌踱,著一層夾襖步出監(jiān)牢的瞬間葵礼,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工并鸵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸳粉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓园担,卻偏偏與公主長(zhǎng)得像届谈,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子弯汰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容