Cassandra 中的數據主要分爲三種: 算法
CommitLog 的數據只有一種,那就是按照必定格式組成 byte 組數,寫到 IO 緩衝區中定時的被刷到磁盤中持久化,在上一篇的配置文件詳解中已經有說到 CommitLog 的持久化方式有兩種,一個是 Periodic 一個是 Batch,它們的數據格式都是同樣的,只是前者是異步的,後者是同步的,數據被刷到磁盤的頻繁度不同。關於 CommitLog 的相關的類結構圖以下: 設計模式
它持久化的策略也很簡單,就是首先將用戶提交的數據所在的對象 RowMutation 序列化成 byte 數組,而後把這個對象和 byte 數組傳給 LogRecordAdder 對象,由 LogRecordAdder 對象調用 CommitLogSegment 的 write 方法去完成寫操做,這個 write 方法的代碼以下: 數組
public CommitLogSegment.CommitLogContext write(RowMutation rowMutation, Object serializedRow){ long currentPosition = -1L; ... Checksum checkum = new CRC32(); if (serializedRow instanceof DataOutputBuffer){ DataOutputBuffer buffer = (DataOutputBuffer) serializedRow; logWriter.writeLong(buffer.getLength()); logWriter.write(buffer.getData(), 0, buffer.getLength()); checkum.update(buffer.getData(), 0, buffer.getLength()); } else{ assert serializedRow instanceof byte[]; byte[] bytes = (byte[]) serializedRow; logWriter.writeLong(bytes.length); logWriter.write(bytes); checkum.update(bytes, 0, bytes.length); } logWriter.writeLong(checkum.getValue()); ... }
這個代碼的主要做用就是若是當前這個根據 columnFamily 的 id 尚未被序列化過,將會根據這個 id 生成一個 CommitLogHeader 對象,記錄下在當前的 CommitLog 文件中的位置,並將這個 header 序列化,覆蓋之前的 header。這個 header 中可能包含多個沒有被序列化到磁盤中的 RowMutation 對應的 columnFamily 的 id。若是已經存在,直接把 RowMutation 對象的序列化結果寫到 CommitLog 的文件緩存區中後面再加一個 CRC32 校驗碼。Byte 數組的格式以下: 緩存
上圖中每一個不一樣的 columnFamily 的 id 都包含在 header 中,這樣作的目的是更容易的判斷那些數據沒有被序列化。 安全
CommitLog 的做用是爲恢復沒有被寫到磁盤中的數據,那如何根據 CommitLog 文件中存儲的數據恢復呢?這段代碼在 recover 方法中: 數據結構
public static void recover(File[] clogs) throws IOException{ ... final CommitLogHeader clHeader = CommitLogHeader.readCommitLogHeader(reader); int lowPos = CommitLogHeader.getLowestPosition(clHeader); if (lowPos == 0) break; reader.seek(lowPos); while (!reader.isEOF()){ try{ bytes = new byte[(int) reader.readLong()]; reader.readFully(bytes); claimedCRC32 = reader.readLong(); } ... ByteArrayInputStream bufIn = new ByteArrayInputStream(bytes); Checksum checksum = new CRC32(); checksum.update(bytes, 0, bytes.length); if (claimedCRC32 != checksum.getValue()){continue;} final RowMutation rm = RowMutation.serializer().deserialize(new DataInputStream(bufIn)); } ... }
這段代碼的思路是:反序列化 CommitLog 文件的 header 爲 CommitLogHeader 對象,尋找 header 對象中沒有被回寫的最小 RowMutation 位置,而後根據這個位置取出這個 RowMutation 對象的序列化數據,而後反序列化爲 RowMutation 對象,而後取出 RowMutation 對象中的數據從新保存到 Memtable 中,而不是直接寫到磁盤中。CommitLog 的操做過程能夠用下圖來清楚的表示: 異步
Memtable 內存中數據結構比較簡單,一個 ColumnFamily 對應一個惟一的 Memtable 對象,因此 Memtable 主要就是維護一個 ConcurrentSkipListMap<DecoratedKey, ColumnFamily> 類型的數據結構,當一個新的 RowMutation 對象加進來時,Memtable 只要看看這個結構是否 <DecoratedKey, ColumnFamily> 集合已經存在,沒有的話就加進來,有的話取出這個 Key 對應的 ColumnFamily,再把它們的 Column 合併。Memtable 相關的類結構圖以下: 性能
Memtable 中的數據會根據配置文件中的相應配置參數刷到本地磁盤中。這些參數在上一篇中已經作了詳細說明。 spa
前面已經多處提到了 Cassandra 的寫的性能很好,好的緣由就是由於 Cassandra 寫到數據首先被寫到 Memtable 中,而 Memtable 是內存中的數據結構,因此 Cassandra 的寫是寫內存的,下圖基本上描述了一個 key/value 數據是怎麼樣寫到 Cassandra 中的 Memtable 數據結構中的。 線程
每添加一條數據到 Memtable 中,程序都會檢查一下這個 Memtable 是否已經知足被寫到磁盤的條件,若是條件知足這個 Memtable 就會寫到磁盤中。先看一下這個過程涉及到的類。相關類圖如圖 6 所示:
Memtable 的條件知足後,它會建立一個 SSTableWriter 對象,而後取出 Memtable 中全部的 <DecoratedKey, ColumnFamily> 集合,將 ColumnFamily 對象的序列化結構寫到 DataOutputBuffer 中。接下去 SSTableWriter 根據 DecoratedKey 和 DataOutputBuffer 分別寫到 Date、Index 和 Filter 三個文件中。
Data 文件格式以下:
Data 文件就是按照上述 byte 數組來組織文件的,數據被寫到 Data 文件中是接着就會往 Index 文件中寫,Index 中到底寫什麼數據呢?
其實 Index 文件就是記錄下全部 Key 和這個 Key 對應在 Data 文件中的啓示地址,如圖 8 所示:
Index 文件實際上就是 Key 的一個索引文件,目前只對 Key 作索引,對 super column 和 column 都沒有建索引,因此要匹配 column 相對來講要比 Key 更慢。
Index 文件寫完後接着寫 Filter 文件,Filter 文件存的內容就是 BloomFilter 對象的序列化結果。它的文件結構如圖 9 所示:
BloomFilter 對象實際上對應一個 Hash 算法,這個算法可以快速的判斷給定的某個 Key 在不在當前這個 SSTable 中,並且每一個 SSTable 對應的 BloomFilter 對象都在內存中,Filter 文件指示 BloomFilter 持久化的一個副本。三個文件對應的數據格式能夠用下圖來清楚的表示:
這個三個文件寫完後,還要作的一件事件就是更新前面提到的 CommitLog 文件,告訴 CommitLog 的 header 所存的當前 ColumnFamily 的沒有寫到磁盤的最小位置。
在 Memtable 往磁盤中寫的過程當中,這個 Memtable 被放到 memtablesPendingFlush 容器中,以保證在讀時候它裏面存的數據能被正確讀到,這個在後面數據讀取時還會介紹。
數據要寫到 Cassandra 中有兩個步驟:
數據寫入涉及的主要相關類以下圖所示:
大慨的寫入邏輯是這樣的:
CassandraServer 接收到要寫入的數據時,首先建立一個 RowMutation 對象,再建立一個 QueryPath 對象,這個對象中保存了 ColumnFamily、Column Name 或者 Super Column Name。接着把用戶提交的全部數據保存在 RowMutation 對象的 Map<String, ColumnFamily> 結構中。接下去就是根據提交的 Key 計算集羣中那個節點應該保存這條數據。這個計算的規則是:將 Key 轉化成 Token,而後在整個集羣的 Token 環中根據二分查找算法找到與給定的 Token 最接近的一個節點。若是用戶指定了數據要保存多個備份,那麼將會順序在 Token 環中返回與備份數相等的節點。這是一個基本的節點列表,後面 Cassandra 會判斷這些節點是否正常工做,若是不正常尋找替換節點。還有還要檢查是否有節點正在啓動,這種節點也是要在考慮的範圍內,最終會造成一個目標節點列表。最後把數據發送到這些節點。
接下去就是將數據保存到 Memtable 中和 CommitLog 中,關於結果的返回根據用戶指定的安全等級不一樣,能夠是異步的,也能夠是同步的。若是某個節點返回失敗,將會再次發送數據。下圖是當 Cassandra 接收到一條數據時到將數據寫到 Memtable 中的時序圖。
Cassandra 的寫的性能要好於讀的性能,爲什麼寫的性能要比讀好不少呢?緣由是,Cassandra 的設計原則就是充分讓寫的速度更快、更方便而犧牲了讀的性能。事實也的確如此,僅僅看 Cassandra 的數據的存儲形式就能發現,首先是寫到 Memtable 中,而後將 Memtable 中數據刷到磁盤中,並且都是順序保存的不檢查數據的惟一性,並且是隻寫不刪(刪除規則在後面介紹),最後纔將順序結構的多個 SSTable 文件合併。這每一步難道不是讓 Cassandra 寫的更快。這個設計想一想對讀會有什麼影響。首先,數據結構的複雜性,Memtable 中和 SSTable 中數據結構確定不一樣,可是返回給用戶的確定是同樣的,這必然會要轉化。其次,數據在多個文件中,要找的數據可能在 Memtable 中,也可能在某個 SSTable 中,若是有 10 個 SSTable,那麼就要在到 10 個 SSTable 中每一個找一遍,雖然使用了 BloomFilter 算法能夠很快判斷到底哪一個 SSTable 中含有指定的 key。還有可能在 Memtable 到 SSTable 的轉化過程當中,這也是要檢查一遍的,也就是數據有可能存在什麼地方,就要到哪裏去找一遍。還有找出來的數據多是已經被刪除的,但也沒辦法仍是要取。
下面是讀取數據的相關類圖:
根據上面的類圖讀取的邏輯是,CassandraServer 建立 ReadCommand 對象,這個對象保存了用戶要獲取記錄的全部必須指定的條件。而後交給 weakReadLocalCallable 這個線程去到 ColumnFamilyStore 對象中去搜索數據,包括 Memtable 和 SSTable。將找到的數據組裝成 Row 返回,這樣一個查詢過程就結束了。這個查詢邏輯能夠用下面的時序圖來表示:
在上圖中還一個地方要說明的是,取得 key 對應的 ColumnFamily 要至少在三個地方查詢,第一個就是 Memtable 中,第二個是 MemtablesPendingFlush,這個是將 Memtable 轉化爲 SSTable 以前的一個臨時 Memtable。第三個是 SSTable。在 SSTable 中查詢最爲複雜,它首先將要查詢的 key 與每一個 SSTable 所對應的 Filter 作比較,這個 Filter 保存了全部這個 SSTable 文件中含有的全部 key 的 Hash 值,這個 Hsah 算法能快速判斷指定的 key 在不在這個 SSTable 中,這個 Filter 的值在所有保存在內存中,這樣能快速判斷要查詢的 key 在那個 SSTable 中。接下去就要在 SSTable 所對應的 Index 中查詢 key 所對應的位置,從前面的 Index 文件的存儲結構知道,Index 中保存了具體數據在 Data 文件中的 Offset。,拿到這個 Offset 後就能夠直接到 Data 文件中取出相應的長度的字節數據,反序列化就能夠達到目標的 ColumnFamily。因爲 Cassandra 的存儲方式,同一個 key 所對應的值可能存在於多個 SSTable 中,因此直到查找完全部的 SSTable 文件後再與前面的兩個 Memtable 查找出來的結果合併,最終纔是要查詢的值。
另外,前面所描述的是最壞的狀況,也就是查詢在徹底沒有緩存的狀況下,固然 Cassandra 在對查詢操做也提供了多級緩存。第一級直接針對查詢結果作緩存,這個緩存的設置的配置項是 Keyspace 下面的 RowsCached。查詢的時候首先會在這個 Cache 中找。第二級 Cache 對應 SSTable 的 Index 文件,它能夠直接緩存要查詢 key 所對應的索引。這個配置項一樣在 Keyspace 下面的 KeysCached 中,若是這個 Cache 能命中,將會省去 Index 文件的一次 IO 查詢。最後一級 Cache 是作磁盤文件與內存文件的 mmap,這種方式能夠提升磁盤 IO 的操做效率,鑑於索引大小的限制,若是 Data 文件太大隻能在 64 位機器上使用這個技術。
從前面的數據寫入規則能夠想象,Cassandra 要想刪除數據是一件麻煩的事,爲什麼這樣說?理由以下:
除了這三點以外還有其它一些難點如 SSTable 持久化數據是順序存儲的,若是刪除中間一段,那數據有如何移動,這些問題都很是棘手,若是設計不合理,性能將會很是之差。
本部分將討論 Cassandra 是如何解決這些問題的。
CassandraServer 中刪除數據的接口只有一個 remove,下面是 remove 方法的源碼:
public void remove(String table, String key, ColumnPath column_path, long timestamp, ConsistencyLevel consistency_level){ checkLoginDone(); ThriftValidation.validateKey(key); ThriftValidation.validateColumnPathOrParent(table, column_path); RowMutation rm = new RowMutation(table, key); rm.delete(new QueryPath(column_path), timestamp); doInsert(consistency_level, rm); }
仔細和 insert 方法比較,發現只有一行不一樣:insert 方法調用的是 rm.add 而這裏是 rm.delete。那麼這個 rm.delete 又作了什麼事情呢?下面是 delete 方法的源碼:
public void delete(QueryPath path, long timestamp){ ... if (columnFamily == null) columnFamily = ColumnFamily.create(table_, cfName); if (path.superColumnName == null && path.columnName == null){ columnFamily.delete(localDeleteTime, timestamp); }else if (path.columnName == null){ SuperColumn sc = new SuperColumn(path.superColumnName, DatabaseDescriptor.getSubComparator(table_, cfName)); sc.markForDeleteAt(localDeleteTime, timestamp); columnFamily.addColumn(sc); }else{ ByteBuffer bytes = ByteBuffer.allocate(4); bytes.putInt(localDeleteTime); columnFamily.addColumn(path, bytes.array(), timestamp, true); } }
這段代碼的主要邏輯就是,若是是刪除指定 Key 下的某個 Column,那麼將這個 Key 所對應的 Column 的 vlaue 設置爲當前系統時間,並將 Column 的 isMarkedForDelete 屬性設置爲 TRUE,若是是要刪除這個 Key 下的全部 Column 則設置這個 ColumnFamily 的刪除時間期限屬性。而後將這個新增的一條數據按照 Insert 方法執行下去。
這個思路如今已經很明顯了,它就是經過設置同一個 Key 下對應不一樣的數據來更新已經在 ConcurrentSkipListMap 集合中存在的數據。這種方法的確很好,它可以達到以下目的:
可是這仍然有兩個問題:這個只是修改了指定的數據,它並無刪除這條數據;還有就是 SSTable 是根據 Memtable 中的數據保存的,極可能會出現不一樣的 SSTable 中保存相同的數據,這個又怎麼解決?的確如此,Cassandra 並無刪除你要刪除的數據,Cassandra 只是在你查詢數據返回以前,過濾掉 isMarkedForDelete 爲 TRUE 的記錄。它可以保證你刪除的數據你不能再查到,至於何時真正刪除,你就不須要關心了。Cassandra 刪除數據的過程很複雜,真正刪除數據是在 SSTable 被壓縮的過程當中,SSTable 壓縮的目的就是把同一個 Key 下對應的數據都統一到一個 SSTable 文件中,這樣就解決了同一條數據在多處的問題。壓縮的過程當中 Cassandra 會根據判斷規則斷定哪些數據應該被刪除。
數據的壓縮其實是數據寫入 Cassandra 的一個延伸,前面描述的數據寫入和數據的讀取都有一些限制,如:在寫的過程當中,數據會不停的將必定大小的 Memtable 刷到磁盤中,這樣不停的刷,勢必會產生不少的一樣大小的 SSTable 文件,不可能這樣無限下去。一樣在讀的過程當中,若是太多的 SSTable 文件必然會影響讀的效率,SSTable 越多就會越影響查詢。還有一個 Key 對應的 Column 分散在多個 SSTable 一樣也會是問題。還有咱們知道 Cassandra 的刪除一樣也是一個寫操做,一樣要處理這些無效的數據。
鑑於以上問題,必然要對 SSTable 文件進行合併,合併的最終目的就是要將一個 Key 對應的全部 value 合併在一塊兒。該組合的組合、該修改的修改,該刪除的刪除。而後將這個 Key 所對應的數據寫在 SSTable 所對應的 Data 文件的一段連續的空間上。
什麼時候壓縮 SSTable 文件由 Cassandra 來控制,理想的 SSTable 文件個數在 4~32 個。當新增一個 SSTable 文件後 Cassandra 會計算當期的平均 SSTable 文件的大小當新增的 SSTable 大小在平均 SSTable 大小的 0.5~1.5 倍時 Cassandra 就會調用壓縮程序壓縮 SSTable 文件,致使的結果就是從新創建 Key 的索引。這個過程能夠用下圖描述:
本文首先描述了 Cassandra 中數據的主要的存儲格式,包括內存中和磁盤中數據的格式,接下去介紹了 Cassandra 處理這些數據的方式,包括數據的添加、刪除和修改,本質上修改和刪除是一個操做。最後介紹了數據的壓縮。
接下去兩篇將向軟件開發人員介紹 Cassandra 中使用的設計模式、巧妙的設計方法和 Cassandra 的高級使用方法——利用 Cassandra 搭建存儲與檢索一體化的實時檢索系統