Elasticsearch系列---併發控制及樂觀鎖實現原理

概要

本篇主要介紹一下Elasticsearch的併發控制和樂觀鎖的實現原理,列舉常見的電商場景,關係型數據庫的併發控制、ES的併發控制實踐。redis

併發場景

不管是關係型數據庫的應用,仍是使用Elasticsearch作搜索加速的場景,只要有數據更新,併發控制是永恆的話題。數據庫

當咱們使用ES更新document的時候,先讀取原始文檔,作修改,而後把document從新索引,若是有多人同時在作相同的操做,不作併發控制的話,就極有可能會發生修改丟失的。可能有些場景,丟失一兩條數據沒關係(好比文章閱讀數量統計,評論數量統計),但有些場景對數據嚴謹性要求極高,丟失一條可能會致使很嚴重的生產問題,好比電商系統中商品的庫存數量,丟失一次更新,可能會致使超賣的現象。瀏覽器

咱們仍是以電商系統的下單環節舉例,某商品庫存100個,兩個用戶下單購買,都包含這件商品,常規下單扣庫存的實現步驟安全

  1. 客戶端完成訂單數據校驗,準備執行下單事務。
  2. 客戶端從ES中獲取商品的庫存數量。
  3. 客戶端提交訂單事務,並將庫存數量扣減。
  4. 客戶端將更新後的庫存數量寫回到ES。

示例流程圖以下:架構

庫存更新示例

若是沒有併發控制,這件商品的庫存就會更新成99(實際正確的值是98),這樣就會致使超賣現象。假定http-1比http-2先一步執行,出現這個問題的緣由是http-2在獲取庫存數據時,http-1還未完成下單扣減庫存後,更新到ES的環節,致使http-2獲取的數據已是過時數據,後續的更新確定也是錯的。併發

上述的場景,若是更新操做越是頻繁,併發數越多,讀取到更新這一段的耗時越長,數據出錯的機率就越大。異步

經常使用的鎖方案

併發控制尤其重要,有兩種通用的方案能夠確保數據在併發更新時的正確性。分佈式

悲觀併發控制

悲觀鎖的含義:我認爲每次更新都有衝突的可能,併發更新這種操做特別不靠譜,我只相信只有嚴格按我定義的粒度進行串行更新,纔是最安全的,一個線程更新時,其餘的線程等着,前一個線程更新完成後,下一個線程再上。ide

關係型數據庫中普遍使用該方案,常見的表鎖、行鎖、讀鎖、寫鎖,依賴redis或memcache等實現的分佈式鎖,都屬於悲觀鎖的範疇。明顯的特徵是後續的線程會被掛起等待,性能通常來講比較低,不過自行實現的分佈式鎖,粒度能夠自行控制(按行記錄、按客戶、按業務類型等),在數據正確性與併發性能方面也能找到很好的折衷點。高併發

樂觀併發控制

樂觀鎖的含義:我認爲衝突不常常發生,我想提升併發的性能,若是真有衝突,被衝突的線程從新再嘗試幾回就行了。

在使用關係型數據庫的應用,也常常會自行實現樂觀鎖的方案,有性能優點,方案實現也不難,仍是挺吸引人的。

Elasticsearch默認使用的是樂觀鎖方案,前面介紹的_version字段,記錄的就是每次更新的版本號,只有拿到最新版本號的更新操做,才能更新成功,其餘拿到過時數據的更新失敗,由客戶端程序決定失敗後的處理方案,通常是重試。

ES的樂觀鎖方案

咱們仍是以上面的案例爲背景,若http-2向ES提交更新數據時,ES會判斷提交過來的版本號與當前document版本號,document版本號單調遞增,若是提交過來的版本號比document版本號小,則說明是過時數據,更新請求將提示錯誤,過程圖以下:

有併發控制的庫存更新示例

使用內置_version實戰樂觀鎖控制效果

咱們在kibana平臺上模擬兩個線程修改同一條document數據,打開兩個瀏覽器標籤便可,咱們使用原有的案例數據:

{
  "_index": "music",
  "_type": "children",
  "_id": "2",
  "_version": 2,
  "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"
  }
}複製代碼

當前的version是2,咱們使用一個瀏覽器標籤頁,發出更新請求,把當前的version帶上:

POST /music/children/2?version=2
{
 "doc": {
   "length": 56
 }
}複製代碼

此時更新成功

{
  "_index": "music",
  "_type": "children",
  "_id": "2",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 2
}複製代碼

同時咱們在另外一個標籤頁上,也使用version=2進行更新,獲得的錯誤結果以下:

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[children][2]: version conflict, current version [3] is different than the one provided [2]",
        "index_uuid": "9759yb44TFuJSejo6boy4A",
        "shard": "2",
        "index": "music"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[children][2]: version conflict, current version [3] is different than the one provided [2]",
    "index_uuid": "9759yb44TFuJSejo6boy4A",
    "shard": "2",
    "index": "music"
  },
  "status": 409
}複製代碼

關鍵錯誤信息:versionconflictengine_exception,版本衝突,將version升到3,模擬失敗後重試,此時更新成功。

真實的場景,重試的次數跟線程併發數有關,線程越多,更新越頻繁,就可能須要重試屢次纔可能更新成功。

使用外部_version實戰樂觀鎖控制效果

ES容許不使用內置的version進行版本控制,能夠自定義使用外部的version,例如常見的使用Elasticsearch作數據查詢加速的經典方案,關係型數據庫做爲主數據庫,而後使用Elasticsearch作搜索數據,主數據會同步數據到Elasticsearch中,而主數據庫併發控制,自己就是使用的樂觀鎖機制,有本身的一套version生成機制,數據同步到ES那裏時,直接使用更方便。

請求語法上加上version_type參數便可:

POST /music/children/2?version=2&version_type=external
{
 "doc": {
   "length": 56
 }
}複製代碼

惟一的區別
  • 內置version,只有當你提供的version與es中的version徹底同樣的時候,才能夠進行更新,不然報錯;
  • 外部version,只有當你提供的version比es中的version大的時候,才能完成修改。

Replica Shard數據同步併發控制

在Elasticsearch內部,每當primary shard收到新的數據時,都須要向replica shard進行數據同步,這種同步請求特別多,而且是異步的。若是同一個document進行了屢次修改,Shard同步的請求是無序的,可能會出現"後發先至"的狀況,若是沒有任何的併發控制機制,那結果將沒法相像。

Shard的數據同步也是基於內置的_version進行樂觀鎖併發控制的。

例如Java客戶端向Elasticsearch某條document發起更新請求,共發出3次,Java端有嚴謹的併發請求控制,在ElasticSearch的primary shard中寫入的結果是正確的,但Elasticsearch內部數據啓動同步時,順序不能保證都是先到先得,狀況多是這樣,第三次更新請求比第二次更新請求先到,以下圖:

ES內部更新併發控制示例

若是Elasticsearch內部沒有併發的控制,這個document在replica的結果多是text2,而且與primary shard的值不一致,這樣確定錯了。

預期的更新順序應該是text1-->text2-->text3,最終的正確結果是text3。那Elasticsearch內部是如何作的呢?

Elasticsearch內部在更新document時,會比較一下version,若是請求的version與document的version相等,就作更新,若是document的version已經大於請求的version,說明此數據已經被後到的線程更新過了,此時會丟棄當前的請求,最終的結果爲text3。此時的更新順序爲text1-->text3,最終結果也是對的。

小結

本篇主要介紹併發場景出現數據錯亂的緣由,Elasticsearch樂觀鎖的實原理,以及ES內部數據同步時的併發控制,有不正確之處或未詳盡之處請知會修改,謝謝。

專一Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區Java架構社區

相關文章
相關標籤/搜索