一、ES解決併發問題的實質
ES在多線程異步修改數據時,是根據_version(版本號)來進行樂觀鎖控制的。
1.1 瞭解_version是什麼
1、在第一次創建document的時候,該document的_version版本號爲1,每次對document進行修改、刪除操作,document的_version版本號加1
PUT /test_index/test_type/6
{
"test_field": "test test"
}
返回結果:
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "6",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
2、刪除這條document
DELETE /test_index/test_type/6
結果:
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "6",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
3、重新創建這條document
PUT /test_index/test_type/6
{
"test_field": "test test"
}
結果:
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "6",
"_version" : 3,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
結論:
在刪除命令後,該document並不是立即物理刪除掉的,它的一些版本號等信息還是保留着的。先刪除一條document,再重新創建這條document,其實會在delete version基礎之上,再把version號加1。
1.2 圖解ES樂觀鎖實質
假設無ES樂觀鎖控制的場景
①創建一條document,假設field1 = text1
②線程1修改數據field1 = text2 (先修改)
③線程2修改數據field1 = text3 (後修改)
④ES內部Primary Shard的數據會同步到Replica Shard中,類似於Replica Shard的同步請求,都是多線程異步的,也就是說多個修改請求之間是亂序的,有可能存在後修改的被先修改,先修改的被後修改等問題。
如果field1 = text3先修改,不進行_version的版本控制,field1 = text2會直接覆蓋,此時數據出現錯誤。正確的邏輯應該是先修改的先修改,後修改的後修改,此時就引出了ES樂觀鎖控制併發的問題。
ES樂觀鎖控制併發的場景
①創建一條document,假設field1 = text1 _version = 1
②線程1修改數據field1 = text2 (先修改) 獲取到修改前的_version=1
③線程2修改數據field1 = text3 (後修改) 獲取到修改前的_version=2
④假設後修改的數據先到,field1 = text3 ,此時_version=2。
此時先修改的數據後到,field1 = text2, 獲取到修改前的_version=2,自己的_version=1,不相同,將低版本的數據忽略,保持field1 = text3,通過版本控制不會讓舊數據覆蓋新數據。
二、實戰模擬ES基於_version併發修改的原理
1、創建一條document數據
PUT /test_index/test_type/8
{
"test_field": "test test"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "8",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
2、打開兩個Kibana客戶端,或打開兩個命令行,模擬兩個客戶端,都獲取到了同一條數據
GET test_index/test_type/8
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "8",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"test_field" : "test test"
}
}
3、其中一個客戶端,更新該document,同時帶上數據的版本號,確保es中的數據的版本號,跟客戶端中的數據的版本號是相同的,才能修改
PUT /test_index/test_type/8?version=1
{
"test_field": "test client 1"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "8",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
4、另外一個客戶端,嘗試基於version=1的數據去進行修改,同樣帶上version版本號,進行樂觀鎖的併發控制
PUT /test_index/test_type/7?version=1
{
"test_field": "test client 2"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "8",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
5、在樂觀鎖成功阻止併發問題之後,嘗試正確的完成更新
GET /test_index/test_type/8
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "8",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"test_field" : "test client 1"
}
}
基於最新的數據和版本號進行修改,修改後,帶上最新的版本號,可能這個步驟會需要反覆執行好幾次,才能成功,特別是在多線程併發更新同一條數據很頻繁的情況下。
嘗試再次修改,直到修改成功:
PUT /test_index/test_type/8?version=2
{
"test_field": "test client 2"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "8",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
三、實戰模擬ES基於version external併發修改的原理
es提供了一個feature,可以不用它提供的內部_version版本號來進行併發控制,可以基於自己維護的一個版本號來進行併發控制。舉個列子,假設數據在mysql裏也有一份,然後應用系統本身就維護了一個版本號,無論是什麼自己生成的,程序控制的。這個時候,在進行樂觀鎖併發控制的時候,可能並不是想要用es內部的_version來進行控制,而是用你自己維護的那個version來進行控制。
語法比較:
?version=1
?version=1&version_type=external
version_type=external,唯一的區別在於,_version,只有當提供的version與es中的_version一模一樣的時候,纔可以進行修改,只要不一樣,就報錯;當version_type=external的時候,只有當提供的version比es中的_version大的時候,才能完成修改。
es,_version=1,?version=1,才能更新成功
es,_version=1,?version>1&version_type=external,才能成功,比如說version=2&version_type=external
(1)先構造一條數據
PUT /test_index/test_type/9
{
"test_field": "test"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "9",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
(2)模擬兩個客戶端同時查詢到這條數據
GET /test_index/test_type/9
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "9",
"_version" : 1,
"_seq_no" : 3,
"_primary_term" : 1,
"found" : true,
"_source" : {
"test_field" : "test"
}
}
(3)第一個客戶端先進行修改,此時客戶端程序是在自己的數據庫中獲取到了這條數據的最新版本號,比如說是2
PUT /test_index/test_type/9?version=2&version_type=external
{
"test_field": "test client 1"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "9",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
(4)模擬第二個客戶端,同時拿到了自己數據庫中維護的那個版本號,也是2,同時基於version=2發起了修改
PUT /test_index/test_type/9?version=2&version_type=external
{
"test_field": "test client 2"
}
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[test_type][9]: version conflict, current version [2] is higher or equal to the one provided [2]",
"index_uuid": "h99RQHS7Si6VxZXsQnzTOA",
"shard": "1",
"index": "test_index"
}
],
"type": "version_conflict_engine_exception",
"reason": "[test_type][9]: version conflict, current version [2] is higher or equal to the one provided [2]",
"index_uuid": "h99RQHS7Si6VxZXsQnzTOA",
"shard": "1",
"index": "test_index"
},
"status": 409
}
(5)在併發控制成功後,重新基於最新的版本號發起更新
GET /test_index/test_type/9
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "9",
"_version" : 2,
"_seq_no" : 4,
"_primary_term" : 1,
"found" : true,
"_source" : {
"test_field" : "test client 1"
}
}
PUT /test_index/test_type/9?version=3&version_type=external
{
"test_field": "test client 2"
}
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : "9",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 1
}