一、前言
ElasticSearch(以下簡(jiǎn)稱ES)的數(shù)據(jù)寫入支持高并發(fā),高并發(fā)就會(huì)帶來(lái)很普遍的數(shù)據(jù)一致性問(wèn)題。常見(jiàn)的解決方法就是加鎖。同樣,ES為了保證高并發(fā)寫的數(shù)據(jù)一致性問(wèn)題,加入了類似于鎖的實(shí)現(xiàn)方法--版本控制。鎖從其中的一個(gè)角度可分為樂(lè)觀鎖和悲觀鎖。
對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定會(huì)有別的線程過(guò)來(lái)修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改。而樂(lè)觀鎖則認(rèn)為自己在使用數(shù)據(jù)時(shí)不會(huì)有別的線程來(lái)修改數(shù)據(jù),所以不會(huì)添加鎖,只是在更新或者提交數(shù)據(jù)的時(shí)候去判斷之前有沒(méi)有別的線程更新了這個(gè)數(shù)據(jù)。那么ES屬于那種鎖呢?下面大獅兄就和大家一起探討官方的具體做法來(lái)回答這個(gè)問(wèn)題。
二、版本控制實(shí)現(xiàn)及驗(yàn)證
1. ES6.7 Before
# 新建測(cè)試索引
PUT test
{
"settings" : {
"number_of_shards" : "3",
"number_of_replicas" : "0"
}
}
## 插入文檔
PUT test/_doc/1
{"user": "zhangsan", "age": 12}
## 響應(yīng)結(jié)果
{
"_index" : "test",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
更新文檔(version版本大于已寫入文檔版本),更新年齡為10,版本號(hào)為200
## 更新文檔
PUT test/_doc/1?version=200&version_type=external
{"user": "zhangsan", "age": 10}
## 返回結(jié)果
{
"_index" : "test",
"_type" : "_doc",
"_id" : "1",
"_version" : 200,
"result" : "updated",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
## 查詢文檔
GET test/_doc/1
## 返回結(jié)果
{
"_index" : "test",
"_type" : "_doc",
"_id" : "1",
"_version" : 200,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "zhangsan",
"age" : 10
}
}
更新成功,年齡更新為10且版本號(hào)更新為200
更新文檔(version版本小于或等于已寫入文檔版本),更新年齡為22,版本號(hào)為180
## 更新文檔
PUT test/_doc/1?version=180&version_type=external
{"user": "zhangsan", "age": 22}
## 返回結(jié)果
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, current version [200] is higher or equal to the one provided [180]",
"index_uuid" : "fCv7Q1dkTl6e9E1Z0dNE1g",
"shard" : "2",
"index" : "test"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, current version [200] is higher or equal to the one provided [180]",
"index_uuid" : "fCv7Q1dkTl6e9E1Z0dNE1g",
"shard" : "2",
"index" : "test"
},
"status" : 409
}
## 查詢文檔
GET test/_doc/1
## 返回結(jié)果
{
"_index" : "test",
"_type" : "_doc",
"_id" : "1",
"_version" : 200,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "zhangsan",
"age" : 10
}
}
更新失敗,數(shù)據(jù)沒(méi)有變化,提示版本沖突,現(xiàn)有的版本號(hào)大于要插入的版本號(hào)。
- vertion_type=external 或者 vertion_type=external_gt :目標(biāo)版本號(hào)大于已有的版本號(hào)才會(huì)更新成功。
- vertion_type=external_gte :目標(biāo)版本號(hào)大于或等于已有的版本號(hào)才會(huì)更新成功。
2. ES6.7 OR Later
# 新建測(cè)試索引
PUT testccc
{
"settings" : {
"number_of_shards" : "1",
"number_of_replicas" : "0"
}
}
## 插入文檔
PUT testccc/_doc/1
{"user": "lisi", "age": 12}
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
返回結(jié)果注意最后的兩個(gè)字段,_seq_no表示序列號(hào)是自增的,_primary_term表是文檔位于哪個(gè)shard。
更新數(shù)據(jù)(seq_no大于已寫入文檔序列號(hào)),更新年齡為10,序列號(hào)為20
## 更新文檔
PUT testccc/_doc/1?if_seq_no=20&if_primary_term=1
{"user": "lisi", "age": 10}
## 返回結(jié)果
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, required seqNo [20], primary term [1]. current document has seqNo [0] and primary term [1]",
"index_uuid" : "N6LzBNj9S5yqVWFubt3x4Q",
"shard" : "0",
"index" : "testccc"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, required seqNo [20], primary term [1]. current document has seqNo [0] and primary term [1]",
"index_uuid" : "N6LzBNj9S5yqVWFubt3x4Q",
"shard" : "0",
"index" : "testccc"
},
"status" : 409
}
## 查詢文檔
GET testccc/_doc/1
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "lisi",
"age" : 12
}
}
更新失敗,數(shù)據(jù)無(wú)變化,提示版本沖突,最近文檔的序列號(hào)為0,要更新的序列號(hào)為20。
更新數(shù)據(jù)(seq_no等于已寫入文檔序列號(hào)),更新年齡為10
## 更新文檔
PUT testccc/_doc/1?if_seq_no=0&if_primary_term=1
{"user": "lisi", "age": 10}
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
## 查詢文檔
GET testccc/_doc/1
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "lisi",
"age" : 10
}
}
更新成功,且seq_no自增為1。
## 插入新文檔
PUT testccc/_doc/2
{"user": "wangwu", "age": 40}
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "2",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
## 更新原文檔
PUT testccc/_doc/1?if_seq_no=1&if_primary_term=1
{"user": "lisi", "age": 50}
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
## 更新新寫入文檔
PUT testccc/_doc/2?if_seq_no=2&if_primary_term=1
{"user": "wangwu", "age": 80}
## 返回結(jié)果
{
"_index" : "testccc",
"_type" : "_doc",
"_id" : "2",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
可以觀察到對(duì)于不同的文檔,seq_no總是自增1的。
三、總結(jié)
- ES版本控制類似于Java中的樂(lè)觀鎖,尤其對(duì)版本號(hào)字段的巧妙使用與解決樂(lè)觀鎖ABA問(wèn)題的CAS算法有異曲同工之妙。
- ES6.7之后添加的if_seq_no與if_primary_term版本控制是針對(duì)于整個(gè)索引的,而_version和version_type版本控制是針對(duì)于單條記錄(即單個(gè)文檔)的,不同的應(yīng)用場(chǎng)景可使用不同的版本控制策略。
- if_seq_no配置的值必須等于存在于現(xiàn)有文檔中才能更新成功,而_version配置的值根據(jù)不同的version_type,必須大于或者大于等于文檔最近更改過(guò)的_version值才能更新成功。