在大量請求並發訪問和更新執行個體中儲存的共用資源時,必須有一種精準高效的並發控制機制來防止邏輯異常和資料錯誤,樂觀鎖就是這樣一種機制。比起原生Redis,Tair(企業版)的TairString模組能協助您實現效能更高、成本更低的樂觀鎖。
並發與Last-Writer-Win
下圖展示了一個典型的並發導致資源競爭的情境:
初始狀態,string類型資料key_1的值為
hello
。t1時刻,App1讀取到key_1的值
hello
。t2時刻,App2讀取到key_1的值
hello
。t3時刻,App1將key_1的值修改為
world
。t4時刻,App2將key_1的值修改為
universe
。
key_1的值是由最後一次寫入決定的,到了t4時刻,App1對key_1的認知已經出現了明顯的誤差,後續操作很可能出現問題,這就是所謂的Last-Writer-Win。要解決Last-Writer-Win問題,就需要保證訪問並更新String資料這個操作的原子性,或者說,將作為共用資源的String資料轉變為具有原子性的變數。您可以使用exString資料結構,構建高效能的樂觀鎖來達成這個效果。
使用TairString實現樂觀鎖
TairString,又稱為exString(extended string),是一種帶版本號碼的String類型資料結構。原生Redis String僅由Key和Value組成,而TairString不僅包含Key和Value,還攜帶了版本(Version),非常適合樂觀鎖等情境。更多介紹及命令詳情資訊請參見exString。
TairString與Redis原生String是兩種不同的資料結構,相關命令不可混用。
TairString有以下特性:
每個Key都帶有Version,用於說明當前Value的版本。使用EXSET命令建立Key時,預設Version為1。
使用EXGET命令查詢Key時,可以擷取到Value和Version兩個欄位。
更新Value時,需要校正Version,如果校正失敗會返回異常資訊
ERR update version is stale
;校正成功則更新Value,且自動將Version加1。除了位元位(bit)相關操作外,TairString可以覆蓋原生Redis String的所有其它功能。
因為這些特性,使得TairString類型資料具有了鎖的機制,使用TairString實現樂觀鎖就非常方便了,樣本如下:
while(true){
{value, version} = EXGET(key); // 擷取Key的Value和Version
value2 = update(...); // 先將新Value儲存到Value2
ret = EXSET(key, value2, version); // 嘗試更新Key並將傳回值賦予變數ret
if(ret == OK)
break; // 如果傳回值為OK則更新成功,跳出迴圈
else if (ret.contains("version is stale"))
continue; // 如果傳回值包含"version is stale"則更新失敗,重複迴圈
}
刪除TairString後,即便以相同的key重新設定一條TairString,其Version也會是1,而不會繼承原TairString的Version。
使用ABS選項可以跳過Version校正強行覆蓋Version並更新TairString,詳情參見EXSET。
降低樂觀鎖的效能消耗
前文的範例程式碼中,如果在執行EXGET後該共用資源被其它用戶端更新了,當前用戶端會擷取到更新失敗的異常資訊,然後重複迴圈,再次執行EXGET擷取共用資源的當前Value和Version,直到更新成功,這樣每次迴圈都有兩次訪問Redis的IO操作。如果使用TairString的EXCAS命令,可以將兩次訪問減少為一次,極大地節約系統資源消耗,提升高並發情境下的服務效能。
EXCAS命令可以在調用時攜帶一個用於校正的Version值,如果校正成功則直接更新TairString的Value,如果校正失敗則返回三個欄位:
"ERR update version is stale"
Value
Version
更新失敗後可以直接得到TairString當前的版本,無需重新查詢,將原本每個迴圈需要進行兩次的訪問減少到一次。樣本如下:
while(true){
{ret, value, version} = excas(key, new_value, old_version) // 直接嘗試用CAS命令置換Value
if(ret == OK)
break; // 如果傳回值為OK則更新成功,跳出迴圈
else (if ret.contains("update version is stale")) // 如果傳回值包含"update version is stale"則更新失敗,更新兩個變數
update(value);
old_version = version;
}