1. 程式人生 > >ElasticSearch教程——併發問題與鎖機制

ElasticSearch教程——併發問題與鎖機制

併發衝突

舉個例子,比如在電商的場景下,假設我們有個程式,其工作流程為:

1.讀取商品資訊(包含庫存,以牙膏為例);

2.使用者下單購買;

3.更新商品庫存(庫存減一);

如果該程式是多執行緒的,那麼總有一個執行緒是先得到的,假設我們牙膏庫存一開始有100件,此時執行緒A先得到執行緒將牙膏的庫存設定為99件,然後執行緒B再將牙膏設定為99件,這個時候就已經錯了。

上面所述問題就是ES中的併發衝突問題,會導致資料不準確。

併發解決方案

在ES中如何解決這類併發衝突問題?

——通過_version版本號的方式進行樂觀鎖併發控制

在es內部第次一建立document的時候,它的_version預設會是1,之後進行的刪除和修改的操作_version都會增加1。可以看到刪除一個document之後,再進行同一個id的document新增操作,版本號是加1而不是初始化為1,從而可以說明document並不是正真地被物理刪除,它的一些版本號資訊一樣會存在,而是會在某個時刻一起被清除。

在es後臺,有很多類似於replica同步的請求,這些請求都是非同步多執行緒的,對於多個修改請求是亂序的,因此會使用_version樂觀鎖來控制這種併發的請求處理。當後續的修改請求先到達,對應修改成功之後_version會加1,然後檢測到之前的修改到達會直接丟棄掉該請求;而當後續的修改請求按照正常順序到達則會正常修改然後_version在前一次修改後的基礎上加1(此時_version可能就是3,會基於之前修改後的狀態)。

es提供了一個外部版本號的樂觀控制方案來替代內部的_version。例如:

?version=1&version_type=external

和內在的_version的區別在於。對於內在_version=1,只有在後續請求滿足?_version=1的時候才能夠更新成功;對於外部_version=1,只有在後續請求滿足?_version>1才能夠修改成功,即必須大於對應的版本

才可以進行修改。

replica同步圖示如下圖:

說明

樂觀鎖和悲觀鎖都是指對待併發控制的兩種思想,共享鎖(S鎖,也叫讀鎖)、排他鎖(X鎖,又稱寫鎖)、行鎖、表鎖、全域性鎖、文件鎖等是具體的鎖的實現,且都屬於悲觀鎖,樂觀鎖沒有鎖。

樂觀鎖

樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。

在ES中樂觀鎖主要通過版本號等資料來進行判定該資料是否有被修改過,如果發現版本version與自己不相同,那就說明資料是已經被修改過的,那麼它會重新去es中讀取最新的資料版本,然後再進行資料上的操作

測試

(1)先構造一條資料出來

PUT /test_index/test_type/7
{
  "test_field": "test test"
}

(2)模擬兩個客戶端,都獲取到了同一條資料

GET test_index/test_type/7

返回:
{
  "_index": "test_index",
  "_type": "test_type",
  "_id": "7",
  "_version": 1,
  "found": true,
  "_source": {
    "test_field": "test test"
  }
}

(3)其中一個客戶端,先更新了一下這個資料

同時帶上資料的版本號,確保說,es中的資料的版本號,跟客戶端中的資料的版本號是相同的,才能修改

PUT /test_index/test_type/7?version=1 
{
  "test_field": "test client 1"
}

返回:
{
  "_index": "test_index",
  "_type": "test_type",
  "_id": "7",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": false
}

(4)另外一個客戶端,嘗試基於version=1的資料去進行修改,同樣帶上version版本號,進行樂觀鎖的併發控制

PUT /test_index/test_type/7?version=1 
{
  "test_field": "test client 2"
}

返回:
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[test_type][7]: version conflict, current version [2] is different than the one provided [1]",
        "index_uuid": "6m0G7yx7R1KECWWGnfH1sw",
        "shard": "3",
        "index": "test_index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[test_type][7]: version conflict, current version [2] is different than the one provided [1]",
    "index_uuid": "6m0G7yx7R1KECWWGnfH1sw",
    "shard": "3",
    "index": "test_index"
  },
  "status": 409
}

(5)在樂觀鎖成功阻止併發問題之後,嘗試正確的完成更新

GET /test_index/test_type/7

返回:
{
  "_index": "test_index",
  "_type": "test_type",
  "_id": "7",
  "_version": 2,
  "found": true,
  "_source": {
    "test_field": "test client 1"
  }
}

基於最新的資料和版本號,去進行修改,修改後,帶上最新的版本號,可能這個步驟會需要反覆執行好幾次,才能成功,特別是在多執行緒併發更新同一條資料很頻繁的情況下

PUT /test_index/test_type/7?version=2 
{
  "test_field": "test client 2"
}

返回:
{
  "_index": "test_index",
  "_type": "test_type",
  "_id": "7",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": false
}

悲觀鎖

悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,上鎖之後就只有一個執行緒可以操作這條資料了,這樣別人想拿這個資料就會block直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

樂觀鎖和悲觀鎖對比

悲觀鎖

優點:方便,直接加鎖,對應用程式來說比較透明,不需要額外的操作;

缺點:併發能力低,同一時間只能有一條執行緒操作資料;

樂觀鎖

優點:併發能力高,不給資料加鎖,可大量執行緒併發操作;

缺點:麻煩,每次更新的時候,都要先比對版本號,然後可能需要重新載入資料,再次修改,再次更改……這個過程可能需要重複多次