1. 程式人生 > >Redis的資料模型和最終一致性

Redis的資料模型和最終一致性

原文:http://antirez.com/news/36

作者:Antirez weblog

最近我注意到Amazon Dynamo的設計和它的原稿,可以說是資料庫領域的最有趣的事情之一,Redis的最終一致性從來沒有特別討論過。
Redis的叢集例項,系統更偏向一致性而非可用性。 Redis的哨兵(Sentinel)本身是具有一致性目標和Master/Slave部署的HA解決方案。
偏向一致性超過可用性,具有最終一致性的確有一些很好的理由,我大致可概括一下三點:
1)Dynamo設計部分依賴於寫入而不是修改值的方法,除了重寫一個全新的值。在Redis的資料模型中,大多數的操作是修改現有值。
2)另外Dynamo對值的大小進行了約定,即應限制在1MB左右。 Redis的Key可以容納多個百萬元素的聚合資料型別。
3)當Redis的叢集規範制定好的時候,已經有相當數量的最終一致性的資料庫正在開發,或者已經實現。我的猜測是,提供一個不同權衡的分散式系統可以惠及更多的使用者群,提供了一個不拘泥流行特性的選擇。
不過我每次介紹Redis的一個新版本時,我需要一些時間來解釋新的想法,或者說,我從來不認為是可行的思路。MIGRATE, DUMP和RESTORE命令的引入,Lua指令碼的支援,和設定毫秒級過期的能力,這些使得Redis建立客戶端叢集第一次變得如此容易。
所以我花了幾個小時的時間來驗證Redis的資料模型的想法,不需要修改Redis伺服器本身而構建出一個最終一致性的叢集。這是相當有趣的,在這個部落格中我分享一些有關該話題的發現和想法。

分割槽
===
在Dynamo設計中資料分割槽使用一致性雜湊。
寫入資料被傳輸到N個節點中的優先順序列表(preference list),如果有不可用的節點,使用下一個節點。
讀取使用N+M個節點,以達到優先順序列表(preference list)外的節點,並負責雜湊環(hash ring)的修改(例如增加一個新的節點)。
在我的Redis-rb客戶端分散式測試中我使用了一致的雜湊實現,為了我的需要稍做修改。


===
寫操作被執行傳送相同的命令給優先順序列表中的每一個節點,一個接一個地並且跳過不可用的節點。
寫的過程中,不執行任何跨節點鎖定,所以讀取時可能會發現僅僅一個節點的子集的更新。這個問題在讀取時被處理。通常在客戶端設計中,大部分的複雜性是在讀取端。
當執行寫入或讀取時,不可用節點(使用者配置的超時後未響應)從下一個請求被臨時暫停持續一個配置的時間值(例如一分鐘),以避免為每個請求增加延遲。在我的測試中,在N個連續的錯誤後我使用一個簡單的錯誤計數器來暫停該節點。


===
讀操作傳送相同的命令給在優先順序列表中的每個節點,以及一定數量的附加連續節點。
讀操作有兩類:
1 )主動讀取操作。在此類讀取中,來自不同節點的結果被比較,以檢測可能的不一致性,在可能的情況下將觸發一個合併操作。
2 )被動讀取操作。為了返回最合適的結果,該操作中不同的答覆被簡單地過濾,而不會觸發合併。
例如GET是一個主動讀操作,如果在結果中發現不一致則可以觸發一個合併操作。
ZRANK取而代之的是一個被動的讀操作。被動讀操作使用一個依賴於命令的勝出者結果選擇。例如在ZRANK的具體情況下各個節點之間的最常見的應答被返回。在每一個節點返回一個不同排名(rank)的指定元素的情況下,返回最小的排名(rank)(帶有minor 整數值) 。

不一致檢測
===
在讀取時同時,以一個型別和操作相關的方式進行不一致的檢測。
例如GET命令檢測字串型別時,如果檢查到所關聯的節點返回的結果中至少有一個值與其它值不同,則說明存在不一致。
然而,在高寫入負載的情況下很容易看到一個GET可能只發現傳播到節點的子集的部分寫入。在這種情況下應該不會被檢測到不一致,以避免無用的合併操作。
解決這個問題的方法是在更新非常接近時(不到幾毫秒)忽略差異。我在Redis2.6實現了這樣一個系統,使用PSETEX命令建立了一個生存大約數毫秒時間的Key。
所以字串型別實際的不一致檢測用下列兩個測試執行:
1)一個或多個值是不同的(包括不存在的或有錯誤的型別)。
2)如果第一個條件為真,同樣的節點都使用同一個Key的EXIST操作來探測對該Key最近的修改。只有當所有節點不一致返回false時操作被認為是有效的,合併操作才被觸發。
對於這個系統,該系統庫中的SET命令的實現在傳送實際的SET命令之前應該使用PSETEX命令。

不一致的聚合資料型別
===
更復雜的資料型別使用更復雜的不一致性檢測演算法,以及使用值級別的短生命Key來作為最近的變化訊號。
例如作為Set型別,SISMEMBER是一個主動讀操作,如果以下兩個條件都為真,則說明檢測到不一致。
1)對應SISMEMBER中集合內的特定值至少一個節點返回不同的結果。
2)在任何所涉及的節點中最近沒有新增或從該集合中移除值。

合併(Merge)操作
===
在我看來合併階段是實驗中最有趣的部分,因為Redis的資料模型相比於Dynamo資料模型是不同的,更加複雜。
我使用型別相關的合併策略,資料庫使用者可以用它來選擇不同的權衡取捨,以滿足應用程式需要非常高的資料庫寫入可用性的不同需求。

*字串使用的是最後寫勝出合併。
*集合(Set)使用的是所有的版本衝突的並集合並。
*列表(List)的合併通過在頭部和尾部兩側插入丟失值來試圖保持插入順序,一直遇到兩側的第一個正常值。
*雜湊(Hash)的合併增加正常和不正常的域(fields),使用的是最近發生的不同的更新值。
*排序集(Sorted set),類似於一個集合合併。我沒有做更多的驗證,所以這是一個正在進行中的工作。

例如在亞馬遜Dynamo文件中的購物車的具體例子將很容易使用Set型別來建模,這樣舊的項可以恢復而不會丟失。
另一方面,針對關注順序的事件,使用列表(list)更加合適。
在我的測試中,字串,雜湊值和列表元素的前面帶一個二進位制位元組的8微秒時間戳,因此對於客戶端時鐘脈衝相位差這是個明智選擇。
當執行客戶合併操作時,Lua指令碼是非常合適的。例如對舊的節點發送勝出值時,我用以下的Lua指令碼:
   if (redis.call("type",KEYS[1]) ~= "string" or
        redis.call("get",KEYS[1]) ~= ARGV[1])
    then
        redis.call("set",KEYS[1],ARGV[2])
    end
因此只有當發現無效的舊版本時值才會被替換。

合併大值
===
在客戶端叢集中,合併大值的一個問題是,使用Redis vanilla API在客戶端驅動兩個大集合的合併可能耗費大量時間。
不過幸運的是使用DUMP,RESTORE和MIGRATE命令可以帶來幫助。在Redis的不穩定分支中MIGRATE命令現在已經支援COPY和REPLACE選項,使得它在這種情況下更加有用。
舉例來說,為了執行兩個節點A和B中兩個集合的並集操作,有可能只是使用臨時Key從一個節點MIGRATE COPY(COPY意味著不刪除源Key)集合到另外一個節點,然後呼叫SUNIONSTORE來完成這項工作。
MIGRATE是非常有效的,可以在較短的時間內傳輸大的Key,但是在Redis的叢集本身,在這篇部落格文章中描述的設計並不適用於大數目的大尺寸Key的應用程式。

結論
===
有許多未解決的問題和實現細節,這個部落格帖子我省略了,但我希望提供一些背景作進一步實驗。
我希望在未來幾周或幾個月內能盡最大的努力持續這項工作,但在這一點目前尚不清楚是否會達到可用庫的形式,但是我很想看到可以使用的產品出現。
如果我有這項工作的訊息我肯定會寫一個新的部落格文章。