1. 程式人生 > >Elasticsearch系列---併發控制及樂觀鎖實現原理

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

概要

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

併發場景

不論是關係型資料庫的應用,還是使用Elasticsearch做搜尋加速的場景,只要有資料更新,併發控制是永恆的話題。

當我們使用ES更新document的時候,先讀取原始文件,做修改,然後把document重新索引,如果有多人同時在做相同的操作,不做併發控制的話,就極有可能會發生修改丟失的。可能有些場景,丟失一兩條資料不要緊(比如文章閱讀數量統計,評論數量統計),但有些場景對資料嚴謹性要求極高,丟失一條可能會導致很嚴重的生產問題,比如電商系統中商品的庫存數量,丟失一次更新,可能會導致超賣的現象。

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

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

示例流程圖如下:

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

上述的場景,如果更新操作越是頻繁,併發數越多,讀取到更新這一段的耗時越長,資料出錯的概率就越大。

常用的鎖方案

併發控制尤為重要,有兩種通用的方案可以確保資料在併發更新時的正確性。

悲觀併發控制

悲觀鎖的含義:我認為每次更新都有衝突的可能,併發更新這種操作特別不靠譜,我只相信只有嚴格按我定義的粒度進行序列更新,才是最安全的,一個執行緒更新時,其他的執行緒等著,前一個執行緒更新完成後,下一個執行緒再上。

關係型資料庫中廣泛使用該方案,常見的表鎖、行鎖、讀鎖、寫鎖,依賴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
}

關鍵錯誤資訊:version_conflict_engine_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內部資料啟動同步時,順序不能保證都是先到先得,情況可能是這樣,第三次更新請求比第二次更新請求先到,如下圖:

如果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架構社群

相關推薦

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

概要 本篇主要介紹一下Elasticsearch的併發控制和樂觀鎖的實現原理,列舉常見的電商場景,關係型資料庫的併發控制、ES的併發控制實踐。 併發場景 不論是關係型資料庫的應用,還是使用Elasticsearch做搜尋加速的場景,只要有資料更新,併發控制是永恆的話題。 當我們使用ES更新document的時

Hibernate併發控制樂觀實現-Version

通過在表中及POJO中增加一個version欄位來表示記錄的版本,來達到多使用者同時更改一條資料的衝突 資料庫指令碼: createtable studentVersion (id varchar(32),name varchar(32),ver int); POJO package Versio

JPA事務併發樂觀實現隔離機制)

事務(4個特性ACID) 原子性(atomic),事務必須是原子工作單元;對於其資料修改,要麼全都執行,要麼全都不執行 一致性(consistent),事務在完成時,必須使所有的資料都保持一致狀態。 隔離性(insulation),由事務併發所作的修改必須與任何其它併發事務所作的修改

大白話講解併發控制的悲觀樂觀 / 高效能 MySQL 筆記

MySQL架構和歷史 MySQL邏輯架構 第一層處理網路連線等, 比如連結認證,授權等 第二層是 MySQL 的核心, 用來解析優化 SQL 語句, 設計快取, 以及各種函式的實現, 包括儲存過程, 觸發器, 檢視等 第三層包括儲存引擎, 負責具體資料的存取, 伺服器通過 API 和儲存引擎通訊,

mybatis 樂觀實現,解決併發問題。

https://blog.csdn.net/zhouzhiwengang/article/details/54973509轉載情景展示:銀行兩操作員同時操作同一賬戶就是典型的例子。比如A,B操作員同時讀取一餘額為1000元的賬戶,A操作員為該賬戶增加100元,B操作員同時為該

mysql樂觀實現

color new lan 什麽 clas 事務處理 帶來 解決 提交 一、為什麽需要鎖(並發控制)? 在多用戶環境中,在同一時間可能會有多個用戶更新相同的記錄,這會產生沖突。這就是著名的並發性問題。 典型的沖突有: 1.丟失更新:一

JAVA樂觀實現-CAS

sub 缺點 c語言 get get() fin mage 個數 接口實現 是什麽 全稱compare and swap,一個CPU原子指令,在硬件層面實現的機制,體現了樂觀鎖的思想。 JVM用C語言封裝了匯編調用。Java的基礎庫中有很多類就是基於JNI調用C接口實現了

【轉】【MySQL】MySQL的併發控制與加分析

https://www.cnblogs.com/yelbosh/p/5813865.html  本文主要是針對MySQL/InnoDB的併發控制和加鎖技術做一個比較深入的剖析,並且對其中涉及到的重要的概念,如多版本併發控制(MVCC),髒讀(dirty read),幻讀(phantom read

mysql 樂觀實現

mysql 樂觀鎖實現 一、為什麼需要鎖(併發控制)?       在多使用者環境中,在同一時間可能會有多個使用者更新相同的記錄,這會產生衝突。這就是著名的併發性問題。       典型的衝突有:      

MySQL的併發控制和加分析

本文主要是針對MySQL/InnoDB的併發控制和加鎖技術做一個比較深入的剖析,並且對其中涉及到的重要的概念,如多版本併發控制(MVCC),髒讀(dirty read),幻讀(phantom read),四種隔離級別(isolation level)等作詳細的闡述,並且基於一個簡單的例子,對MySQ

CAS和MySql樂觀實現下單

CAS和MySql樂觀鎖實現下單 準備 建表t_order: CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `version` int(255) DEFAULT NULL, `stock`

理解高併發(15).Future、Callable實現原理用法

概述 jdk1.5推出的,使用它能帶來2個方便: 能夠獲得到執行緒執行後返回的結果 執行緒異常有效捕獲 簡單例子 輸出結果:result=hello public class ThreadLocal

java共享實現原理CountDownLatch解析

前言 前面介紹了ReentrantLock,又叫排他鎖,本篇主要通過CountDownLatch的學習來了解java併發包中是如何實現共享鎖的。 CountDownLatch使用解說 CountDownLatch是java5中新增的一個併發工具類,

Hibernate樂觀實現—Version

樂觀併發控制,可以有三種方式。 1,Version版本號 2,時間戳 3,自動版本控制。 這裡不建議在新的應用程式中定義沒有版本或者時間戳列的版本控制:它更慢,更復雜,如果你正在使用脫管物件,它則不會生效。 通過在表中及POJO中增加一個version欄

秒殺---使用樂觀實現或cache實現

概念 秒殺系統的特點 新品上市 價格低廉 市場造勢 大幅推廣 指定時間開售 瞬時售空 讀多寫少 秒殺系統難點 高併發、負載壓力大 競爭資源是有限的 對其他業務的影響 提防“黃牛黨”

Redis深入操作(redis事務控制樂觀,密碼配置,效能監控)

一、redis事務控制1.1redis本身支援事務處理,但是這種支援的事務處理本身是存在有設計缺陷的,而且與傳統資料庫的事務控制不同,首先來看一下redis中事務支援命令:                        .開啟事務:multi                 

java樂觀實現案例

簡單說說樂觀鎖。樂觀鎖是相對於悲觀鎖而言。悲觀鎖認為,這個執行緒,發生併發的可能性極大,執行緒衝突機率大,比較悲觀。一般用synchronized實現,保證每次操作資料不會衝突。樂觀鎖認為,執行緒衝突可能性小,比較樂觀,直接去操作資料,如果發現數據已經被更改(通過版本號控制)

python資料庫併發處理(樂觀

1.資料庫併發處理問題 在多個使用者同時發起對同一個資料提交修改操作時(先查詢,再修改),會出現資源競爭的問題,導致最終修改的資料結果出現異常。 比如限量商品在熱銷時,當多個使用者同時請求購買商品時,最終修改的資料就會出現異常  下面我們來寫點程式碼還原一下現象: 1

Hibernate 樂觀實現之 Version

通過在表中及POJO中增加一個Timestamp欄位來表示記錄的最後更新時間,來達到多使用者同時更改一條資料的衝突,這個timestamp由資料庫自動新增,無需人工干預 資料庫結構: package com.ematchina.test; import java.sql.Timestamp; imp

我是如何基於二階段遞交悲觀實現分散式事務的

  由於框架一開始的定位就是需要支援強一致性分散式儲存,所以如何實現分散式事務成為一個大挑戰。作者學習了CockroachDB及TiDB等資料庫的實現方式後,決定參考TiDB的實現方式,但不同於使用樂觀方式而是採用悲觀鎖方式,遇到事務衝突採用排隊的方式而不是重啟事務。 一、二階段(2PC)遞交流程: 參考下圖