1、逆熵
Cassandra數據庫在分布式的架構上借鑒了Amazon的Dynamo,而在數據的存儲模型上參考了Google的Bigtable,因而在數據一致性方面與Dynamo和Bigtable有着很深的聯系,逆熵機制就是這種聯系的一種體現。
逆熵與gossip協議一樣也是基於傳染病理論的算法,它主要用來保證不同節點上的數據能夠更新到最新的版本。要了解逆熵必須先來了解一下Merkle Tree,在Cassandra中每個數據項可以表示為 (key, value) 對,key 均勻的分布在一個 2^n 的 key 空間中(比如 key 可以取 value 的 SHA1 hash 值)。兩個節點在進行數據同步時分別對數據集生成一個 Merkle Tree。Merkey Tree 是一棵二叉數。Merkel Tree 的最底層可以是 16 個 key 的異或值 (xor)。每個父節點是兩個子節點的 xor 值。這樣,在比較的時候,兩個節點首先傳最頂層的 tree node, 如果相等,那么就不用繼續比較了。否則,分別比較左右子樹。 Cassandra正是基於上述所說的比較機制來確定兩個節點之間數據是否一致的,如果不一致節點將通過數據記錄中的時間戳來進行更行。
Cassandra中的Merkle樹與Amazon的Dynamo中的Merkle樹有一些不同,在Cassandra中我們要求每個列族都有自己的Merkle樹,並且在主壓緊操作過程中,Merkle樹將作為一個快照被創建,而其生命周期僅限於它被需要發送給換上鄰居節點的時候,從而降低了磁盤的I/O操作。在每一次更新中逆熵算法都會被引入,這會對數據庫進行校驗和,並且與其他節點比較校驗和。如果校驗和不同,就會進行數據的交換,這需要一個時間窗口來保證其他節點可以有機會得到最近的更新,這樣系統就不會中時進行沒有必要的逆熵操作了。
逆熵在很大程度上解決了Cassandra數據庫的數據一致性的問題,但是這種策略也存在着一些問題。在數據量差異很小的情況下, Merkle Tree 可以減少網絡傳輸開銷。但是兩個參與節點都需要遍歷所有數據項以計算 Merkle Tree, 計算開銷 (或 IO 開銷,如果需要從磁盤讀數據項)是很大的,可能會影響服務器的對外服務,這也是一些大公司放棄Cassandra的主要原因。
2、讀修復
有兩種類型的讀請求,一個協調器(讀代理)可以將這兩種讀請求發送到一個副本:直接讀請求和讀修理請求。讀請求所要讀取的副本的數量將會有用戶在調用讀請求自己進行設定,例如:設定為ONE時,將會只對一個副本進行讀取,設定為QUORUM,則會在讀取超過半數的一致性的副本后返回一份副本給客戶端。讀修復機制在讀請求結果發送回用戶之后將對所有的副本就行檢測和修復,確保所有的副本保持一致。
用戶在向Cassandra請求數據時已經指定了一致性的級別,讀請求的協調器就根據用戶的一致性界別對Cassandra數據庫中的符合一致性界別的節點進行讀取,並將讀取出來的結果進行比較,檢查他們是不是一致的,如果是一致的,那么毫無意外返回相應的值即可,如果不是一致的,則基於時間戳從這些數據中提取出最新的數據返回給用戶。結果已經返回給用戶了,然而為了確保數據在數據庫中的一致性,Cassandra將會在后台自己進行所有相關數據的副本一致性的檢測,並且對那些不滿足一致性的數據進行一致性同步,這就是讀修復機制的修復過程。
例如:在一個集群中,副本因子為3(同一種數據存三份),在讀取時一致性級別指定為2,也就是說對已一種數據將會讀取3個備份中的兩個。在這種情況下,如圖1所示,Cassandra將會為我們讀取兩個副本,並且在兩個副本中決定出最新的那個副本數據,然后返回給用戶,之后讀修復策略將會對第三份未進行讀取的副本進行修復,以確定這三個副本的數據是一致的。

圖1 Cassandra讀修復機制(虛線為讀修復過程)
3、提示移交
當一個寫請求到達Cassandra時,如果此時負責這部分的Cassandra節點由於種種原因不能夠達到用戶指定的副本因子的要求,這個時候寫入將會成為麻煩的事情,寫入將會因為節點的缺失而失敗。為了解決這樣的問題,Cassandra和其他一些分布式的場景一樣提出了提示移交機制。該機制是指當寫入因為相應節點不能夠滿足副本因子時,將會把數據寫到其他的節點上去,之后向用戶返回寫入成功,當相關的節點又恢復服務之后,Cassandra將寫入其他節點的那部分數據在從新寫入到該節點。
提示移交允許Cassandra對於寫操作永遠可用,降低了在寫節點恢復服務之后的不一致的時間,當用戶的一致性級別定為ANY時,也就是意味着即便是有一個提示被記錄下來,寫操作也就可以認為是成功了。例如:Key A按照規則首要寫入節點為N1,然后復制到N2。假如N1宕機,如果寫入N2能滿足一致性級別要求,則Key A對應的Row Mutation將封裝一個帶hint信息的頭部(包含了目標為N1的信息),然后隨機寫入一個節點N3,此副本不可讀。同時正常復制一份數據到N2,此副本可以提供讀。如果寫N2不滿足寫一致性要求,則寫會失敗。 等到N1恢復后,原本應該寫入N1的帶hint頭的信息將重新寫回N1。
提示移交機制在很多分布式的場景下被用來保持數據寫時的一致性,被認為是一個保持數據庫持久性的深思熟慮的設計,並且這種機制還在很多的分布式的計算模式中出現,例如Java消息服務(JMS)。在具有持久性的“保障傳遞”JMS隊列中,如果消息無法發送該接受者,JMS將會等待一個給定的時間,然后重新傳遞,直到成功接收為止。然而在實際的系統中,不論是對於JMS的可靠傳輸還是對於Cassandra的提示移交,都存在一個問題:如果節點離線持續了一段時間,已經有很多的提示信息存在了其他的節點上了,那么在節點重新上線之后,請求將會集中的發送到這個節點,對於這個剛剛恢復服務、非常脆弱的節點來說是無法承受的。
4、分布式刪除
很多在單機中非常簡單的操作,一旦放在集中分布式的環境當中就沒有那么簡單了,就像刪除一樣,單機刪除非常簡單,只需要把數據直接從磁盤上去掉即可,而對於分布式,則大大不同了。分布式刪除的難點在於:如果某對象的一個備份節點 A 當前不在線,而其他備份節點刪除了該對象,那么等 A 再次上線時,它並不知道該數據已被刪除,所以會嘗試恢復其他備份節點上的這個對象,這使得刪除操作無效。
分布式刪除機制正是為了解決以上的所提到的分布式刪除的所遇到的問題。刪除一個 column 其實只是插入一個關於這個 column 的墓碑(tombstone),並不直接刪除原有的 column。該墓碑被作為對該列族的一次修改,記錄在 Memtable 和 SSTable 中。墓碑的內容是刪除請求被執行的時間,該時間是接受客戶端請求的存儲節點在執行該請求時的本地時間(local delete time),稱為本地刪除時間。需要注意區分本地刪除時間和時間戳,每個列族修改記錄都有一個時間戳,這個時間戳可以理解為該 column 的修改時間,是由客戶端給定的,而本地刪除時間只有在采用分布式刪除機制時才會有。
由於被刪除的 column 並不會立即被從磁盤中刪除,所以系統占用的磁盤空間會越來越大,這就需要有一種垃圾回收的機制,定期刪除被標記了墓碑的 column,而在Cassandra當中垃圾回收是在壓緊的過程中完成。