資料一緻性是任何允許多線程對資料作CRUD操作的db系統都繞不過的問題,ElasticSearch也不例外。ElasticSearch提供了版本系統來解決資料的一緻性問題。
ElasticSearch版本系統的大緻思想是:ElasticSearch中存儲的每一個document都包含了一個版本号,當使用者每次對該document作增删改操作時,ElasticSearch都會對document的版本号進行加1操作。ElasticSearch采用了樂觀鎖來保證資料的一緻性,也就是說,當使用者對document進行操作時,并不需要對該document作加鎖和解鎖的操作,隻需要指定要操作的版本即可。當版本号一緻時,ElasticSearch會允許該操作順利執行,而當版本号存在沖突時,ElasticSearch會提示沖突并抛出異常(VersionConflictEngineException異常)。
以ElasticSearch的内部版本(INTERNAL)為例,當假設目前ElasticSearch中存儲了一個verion為10的document。A線程和B線程都擷取該document,A線程修改了該document後,将version然後向ElasticSearch發送更新請求,此時,ElasticSearch收到請求後會更新該document,然後将version更新為11。而當B線程修改完document後,将version設為10,然後向ElasticSearch發送更新請求的時候,ElasticSearch會拒絕更新并抛出一個VersionConflictEngineException異常。
ElasticSearch的版本号的取值範圍為1到2^63-1,其中有這麼幾個特殊值:
1)當傳遞給ElasticSearch的版本号為-3時,表示目前的寫操作會被強制地寫入,不會受目前版本控制的限制。(這裡需要注意的是,在1.2以前的版本這個值是0,并且在很長的一段時間,-3和0這兩個值是并存的,都表示強制寫入。那麼當VersionType為INTERNAL時就會出現一種很尴尬的情況,當document不存在時,使用者必須将version指定為0才能插入,但是如果遇到之前的例子,A插入之後version變為1,這時候B将version指定為0時依然可以插入成功,這個時候就會覆寫A的操作。好在5.0版本已經把這個蛋疼的常量完全廢棄了)
2)所有不存在的document的version值都為-1(或者說當version為-1時表示這個document不存在)
3)-2表示document存在,但是沒有version值的情況
4)-4表示當寫操作遇到document剛被删除的情況時,不考慮版本控制的限制強制寫入。這裡要提一點,官方文檔中提到了這麼一種情況:假設ElasticSearch中有一個version為1000的document,這時候A将這個document删掉,随後B向ElasticSearch發送了一個更新version為999的document的請求,那麼這時候就尴尬了。document已經被删除不存在了,那麼B的更新操作将會成功被寫入,但是實際上,我們知道version為999的更新操作時在删除操作之前,也就是正确的情況下document是應該被删除的。為了解決這個問題,es會在60s内記錄這個删除操作,這個時候新的寫入請求到來時,如果發現與删除操作的版本存在沖突,則寫入操作依舊不會成功。
/** used to indicate the write operation should succeed regardless of current version **/
public static final long MATCH_ANY = -3L;
/** indicates that the current document was not found in lucene and in the version map */
public static final long NOT_FOUND = -1L;
// -2 was used for docs that can be found in the index but do not have a version
/**
* used to indicate that the write operation should be executed if the document is currently deleted
* i.e., not found in the index and/or found as deleted (with version) in the version map
*/
public static final long MATCH_DELETED = -4L;
ElasticSearch的VersionType分為這麼幾種:INTERNAL,EXTERNAL,EXTERNAL_GTE以及FORCE(在5.0版本已經被棄用)。
1)INTERNAL的判重邏輯如下:
@Override
public boolean isVersionConflictForWrites(long currentVersion, long expectedVersion, boolean deleted) {
return isVersionConflict(currentVersion, expectedVersion, deleted);
}
@Override
public boolean isVersionConflictForReads(long currentVersion, long expectedVersion) {
return isVersionConflict(currentVersion, expectedVersion, false);
}
private boolean isVersionConflict(long currentVersion, long expectedVersion, boolean deleted) {
if (expectedVersion == Versions.MATCH_ANY) {
return false;
}
if (expectedVersion == Versions.MATCH_DELETED) {
return deleted == false;
}
if (currentVersion != expectedVersion) {
return true;
}
return false;
}
進行寫操作時,若指定版本号為-4,且document沒有被删除,則判定目前版本沖突,若目前版本号與指定的版本号不相等也判定為版本沖突。而寫操作如果指定的版本号為-4時,不管document是否被删除都判定為沖突。
2)EXTERNAL(我不知道叫外部版本号準不準确)是用于如下場景:例如使用者的資料之前存儲在其他的db中(例如MySQL),其使用了與ElasticSearch不同的版本号編号方式。而使用者想把這套版本号平移到ElasticSearch,這時候就可以使用EXTERNAL(有個限制是版本号的取值範圍也必須在1到2^63-1)。當VersionType被置威EXTERNAL的時候,每次的寫操作ElasticSearch将不會再去把version加1。接着來看下判重邏輯:
public boolean isVersionConflictForWrites(long currentVersion, long expectedVersion, boolean deleted) {
if (currentVersion == Versions.NOT_FOUND) {
return false;
}
if (expectedVersion == Versions.MATCH_ANY) {
return true;
}
if (currentVersion >= expectedVersion) {
return true;
}
return false;
}
@Override
public boolean isVersionConflictForReads(long currentVersion, long expectedVersion) {
if (expectedVersion == Versions.MATCH_ANY) {
return false;
}
if (currentVersion == Versions.NOT_FOUND) {
return true;
}
if (currentVersion != expectedVersion) {
return true;
}
return false;
}
當操作為寫操作時,若指定的版本号為-3或者指定版本号小于等于目前版本号時,則判定版本沖突。操作為讀操作時,若指定版本号為-1或者與目前版本号不相等時,則判定為版本沖突。
3)EXTERNAL_GTE(這種在官方文檔中沒有找到描述,應用場景也不太了解)
@Override
public boolean isVersionConflictForWrites(long currentVersion, long expectedVersion, boolean deleted) {
if (currentVersion == Versions.NOT_FOUND) {
return false;
}
if (expectedVersion == Versions.MATCH_ANY) {
return true;
}
if (currentVersion > expectedVersion) {
return true;
}
return false;
}
@Override
public boolean isVersionConflictForReads(long currentVersion, long expectedVersion) {
if (expectedVersion == Versions.MATCH_ANY) {
return false;
}
if (currentVersion == Versions.NOT_FOUND) {
return true;
}
if (currentVersion != expectedVersion) {
return true;
}
return false;
}
讀操作的判重邏輯與EXTERNAL一緻。寫操作的判重邏輯與EXTERNAL稍有不同,若指定的版本号為-3或者指定版本号 小于目前版本号時,則判定版本沖突。
4)FORCE,顧名思義就是強制執行。
@Override
public boolean isVersionConflictForWrites(long currentVersion, long expectedVersion, boolean deleted) {
if (currentVersion == Versions.NOT_FOUND) {
return false;
}
if (expectedVersion == Versions.MATCH_ANY) {
throw new IllegalStateException("you must specify a version when use VersionType.FORCE");
}
return false;
}
從源碼上看,讀操作和寫操作的判重邏輯是一樣的。僅當指定版本為-3時,會抛出異常。
參考資料:https://www.elastic.co/blog/elasticsearch-versioning-support