redis mysql 中的跳錶(skip list) 查找樹(btree)

跳錶(skip list)

image.png

數組和鏈表對比:html

  • 數組支持隨機訪問,根據下標隨機訪問的時間複雜度是 O(1)
  • 數組的插入和刪除操做效率不高,平均狀況下的時間複雜度是 O(logN)
  • 鏈表隨機訪問性能沒有數組好,平均狀況下的時間複雜度是 O(logN)
  • 鏈表插入和刪除操做只須要改變相鄰節點的指針,時間複雜度是 O(1)

二分查找底層依賴數組結構,跳錶經過構建多級索引來提升查詢效率,實現了基於鏈表結構的「二分查找」(查找、刪除、添加等操做均可以擁有對數時間複雜度)java

跳錶時間和空間複雜度:node

  • 查詢操做的平均時間複雜度是 O(logN),最壞時間複雜度 O(N)
  • 插入操做的平均時間複雜度是 O(logN),最壞時間複雜度 O(N)
  • 刪除操做的平均時間複雜度是 O(logN),最壞時間複雜度 O(N)
  • 平均空間複雜度是 O(N),最壞空間複雜度 O(N logN)

跳錶時間複雜度分析:mysql

  • 設原始鏈表有 N 個節點,每兩個節點抽取一個節點做爲上一級索引節點,這樣第 k 層索引有 N/(2^k)個節點
  • 設共有 h 級索引,最高一級索引有 2 個節點,結合上面的分析知道 N/2^h = 2,因此 h + 1 = log2(N)
  • 加上原始鏈表這一層,整個跳錶的高度是 log2(N)
  • 查找某個數據時若每層需比較 m 個節點,總的時間複雜度是 log2(m*N),可簡化爲 O(logN)(m 是個常數)

跳錶索引動態更新:git

  • 往跳錶中插入數據時會選擇性的將這個數據同步插入部分索引層中
  • 由隨機函數來肯定須要插入哪些索引層級,這樣在能夠避免在插入大量數據後跳錶查詢性能退化

Redis 有序集合(Sorted Set)

Reids 有序集合支持的核心操做有:插入數據、查找數據、刪除數據、根據 score 按照區間查找數據github

Redis 有序集合的底層編碼有兩種實現,分別是 ziplist 和 skiplist,當有序集合的元素個數小於 zset-max-ziplist-entries 配置(默認128個),而且每一個元素的值都小於 zset-max-ziplist-value 配置(默認64字節)時,Redis 會用 ziplist 來做爲有序集合的內部實現,上述兩個條件之一不知足時,Redis 啓用 skiplist 做爲有序集合的內部實現(轉換過程是不可逆轉,只能從小內存編碼向大內存編碼轉換)redis

下面演示了先查看 redis 的默認配置,並演示了往 zset 中添加元素時因爲元素大於 64 字節,Redis 內部存儲結構由開始的 ziplist 轉變爲一個 dict 加一個 skiplist (dict 用來查詢數據到分數的對應關係,而 skiplist 用來根據分數查詢數據)
clipboard.pngsql

Redis 實現的跳躍表:mongodb

  • Redis 的跳躍表實現由 zskiplist 和 zskiplistNode 兩個結構組成, 其中 zskiplist 用於保存跳躍表信息(好比表頭節點、表尾節點、長度), 而 zskiplistNode 則用於表示跳躍表節點
  • 每一個跳躍表節點的層高都是 1 至 32 之間的隨機數(程序根據冪定律生成,越大的數出現的機率越小)
  • 在同一個跳躍表中,多個節點能夠包含相同的分值,但每一個節點的成員對象必須是惟一的
  • 跳躍表中的節點按照分值大小進行排序, 當分值相同時, 節點按照成員對象的大小進行排序
  • 原始鏈表層的每一個節點有一個指向前繼節點的指針,用於從表尾方向向表頭方向迭代(當執行 ZREVRANGE 或 ZREVRANGEBYSCORE 等逆序處理有序集的命令時用到)

image.png

爲何 Redis 用跳錶而不是查找樹實現有序集合:數據庫

  • 針對數據插入、查詢、刪除及序區間查找等操做,跳錶的時間複雜度不會比平衡樹差
  • 跳錶比樹的結構更簡潔,這樣代碼更容易實現、更容易維護和調試
  • 能夠靈活的調整索引節點個數和原始鏈表節點個數之間的比例來平衡索引對內存的消耗和查詢效率

有序結合使用字典結構的優點:

  • 能夠在 O(1) 時間複雜度內檢查給定 member 是否存在於有序集
  • 能夠在 O(1) 時間複雜度內取出 member 對應的 score 值(實現 ZSCORE 命令)

爲何 Redis 使用 skiplist 轉換 ziplist:

  • 壓縮列表是 Redis 爲了節約內存而開發的, 由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構
  • 壓縮列表編碼應用範圍普遍,能夠分別做爲hash、list、zset類型的底層數據結構實現
  • 壓縮列表新增刪除操做涉及內存從新分配或釋放,加大了操做的複雜性,適合存儲小對象和長度有限的數據
  • Redis 提供了 {type}-max-ziplist-value 和 {type}-max-ziplist-entries 相關參數來控制 ziplist 編碼轉換

Redis 每種數據類型(type)能夠採用的編碼方式(encoding)對應關係

image.png

參考資料:
Redis Zset 源代碼
Redis ZipList 源代碼
Redis ziplist 設計與實現
Redis skiplist 設計與實現
Redis ziplist 實現有序集合
Redis skiplist 實現有序集合

Lucene 倒排索引列表

image.png

倒排索引/反向索引(Inverted index):

  • 倒排索引用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射,若是把一本書的目錄理解爲書的正向索引,那麼書最後的索引頁就是書的倒排索引

Lucene 是一個開源的高性能、可擴展的信息檢索引擎,Lucene 的索引是基於倒排索引結構組織的,倒排列表本質上是基於 Term 的反向列表,倒排索引由 Term index,Term Dictionary 和 Posting List 組成

  • 單詞詞典(Term Dictionary)記錄全部文檔的單詞,並記錄單詞到倒排列表的關聯關係
  • 倒排列表(Posting list)記錄了單詞對應的文檔集合,倒排鏈由有序的倒排索引項組成
  • 倒排索引項(Posting)中包含了文檔Id(docId)、詞頻(TF)、位置(Position)和偏移量(Offset)

爲了可以快速進行倒排鏈的查找和 docid 查找,Lucene 倒排列表採用了 SkipList 結構,這樣能夠快速的對兩個倒排列集合求交集和並集

Elasticsearch 搜索服務器底層依賴於 Lucene 檢索引擎,Elasticsearch 在處理多個索引查詢合併操做時支持 skip list、bitmap 和 Roaring bitmap 三種實現方式,若是查詢的 filter 緩存到了內存中(以 bitset 的形式),那麼合併就是兩個 bitset 的 AND,若是查詢的 filter 沒有緩存就用 skip list 的方式去遍歷兩個 on disk 的 posting list

參考資料:
Multi-level skipping on posting lists
Frame of Reference and Roaring Bitmaps
MultiLevelSkipListWriter.java
MultiLevelSkipListReader.java
時間序列數據庫的祕密——索引
Lucene 查詢原理及解析
基於Lucene查詢原理分析Elasticsearch的性能

B-樹(B-Tree)

二叉查找樹(binary search tree):

  • 每一個節點其左子樹上全部節點值要小於該節點值,右子樹上全部節點的值要大於該節點值

平衡二叉樹查找樹:

  • 二叉樹查找樹中任意節點的左子樹和右子樹的高度差不大於一

B-Tree 遵循以下規則:

  • B-Tree 是一種自平衡的 M 叉查找樹
  • 根節點至少存在兩個子節點,至多存在 M 個子節點
  • 除了根節點和葉子節點,每節點包含 k-1 個關鍵字和 k 個指向子節點的指針(k 的取值範圍[M/2,M])
  • 葉子節點包含 k-1 個關鍵字(k 的取值範圍 [M/2,M] )
  • 全部葉子節點在樹的同一層

B+樹(B+Tree)

image.png

B+ 樹遵循以下規則:

  • B+Tree 是一顆自平衡的查找樹
  • 每一個節點最多有 M 個子節點(下文 MySQL 索引部分說明 M 取值)
  • 除根節點外,每一個節點至少有 M/2 個子節點,根節點至少有兩個子節點
  • 非葉子節點中只存儲關鍵字和指向子節點的指針,不存儲指向實際數據的指針
  • 經過雙向鏈表將葉子節點串聯起來,能夠方便按區間查找(不用每次返回根節點)

B+ 樹時間和空間複雜度:

  • 查詢數據的時間複雜度是 O(logN)
  • 插入操做的時間複雜度是 O(logN)
  • 刪除操做的時間複雜度是 O(logN)
  • 空間複雜度是 O(N)

B+ 樹動態更新索引節點:

  • 寫入數據後若某節點的子節點個數大於 M,會將對應節點分裂爲兩個節點,父節點若有須要會級聯分裂
  • 刪除數據後,如某節點的子節點個數小於 M/2,將相鄰的兄弟節點合併

B+Tree 與 B-Tree 不一樣點:

  • 每一個節點有 k 個關鍵字就有 k 個子節點(B-Tree 有 k 個關鍵字時有 k+1 個子節點)
  • 非葉子節點的關鍵字也存在於子節點中,而且是子節點中的最小/最大關鍵字
  • B+Tree 非葉子節點只用於索引,不保存數據記錄(B-Tree 中非葉子節點既保存索引也保存數據記錄)
  • B+Tree 關鍵字只出如今葉子節點,而且構成有序鏈表(按關鍵字從小到大排列)

MySQL InnoDB 索引

文件系統和數據庫系統一般使用 B+Tree 來存儲索引,MySQL 的大部分索引(PRIMARY KEY、UNIQUE INDEX)使用 B+Tree 結構存儲,也有一些特例,如 InnoDB 使用倒排索引(inverted lists)做爲全文索引(FULLTEXT)的存儲結構 (MongoDB 也是使用 b-tree 構造索引

  • MySQL 的索引分爲聚簇索引(clustered index)和二級索引(secondary index)
  • 能夠把 MySQL 的索引理解爲一顆聚簇索引 B+Tree 和其餘一到多顆二級索引 B+Tree
  • 聚簇索引樹的葉子節點保存了主鍵和實際數據記錄行
  • 二級索引樹的葉子節點保存了指向主鍵的指針和建立二級索引的列數據

聚簇索引:
mysql cluster index.png
二級索引:
mysql secondary index.png

MySQL 不一樣存儲引擎支持的索引存儲結構以下

image.png

爲何 MySQL 使用 B+Tree 結構實現索引:

  • 對於數據存儲在磁盤中的數據庫系統而言,I/O 操做次數是影響性能的重要因素
  • 操做系統是按頁(getconf PAGESIZE,默認 4K)讀取磁盤中數據,一次讀取一頁數據
  • 若是讀取的數據量超過一頁大小,會觸發屢次 I/O 操做
  • 若 M 的取值讓每一個節點大小等於頁大小,這時讀取一個節點只須要一次磁盤 I/O 操做
  • B+Tree 的非葉子結點只保存關鍵字和指向子結點的指針,相同的頁大小能夠存儲更多的節點數,同時減小了樹的高度增長了樹的分叉數,進而減小了磁盤 I/O 操做次數
  • 刪除數據時更簡單,由於 B+Tree 實際數據只保存在葉子結點,因此不須要刪除非葉子結點

爲何 MySQL InnoDB 索引遵循最左匹配原則

  • InnoDB 存儲引擎使用 B+Tree 保存索引
  • B+Tree 是一顆全部節點有序的查找樹,每次查找從根節點開始對比,根據比較的結果肯定繼續查找左子樹或右子樹

處理從右到左匹配的需求:

方案一:表結構新增一列用來存儲須要從右到左匹配列的倒序字符並構建索引,缺點是新增列和索引都須要佔用磁盤空間
方案二:Mysql 5.7 版本提供了虛擬列功能,使用 reverse 函數構建虛擬列並建立索引
具體腳本能夠參考 mysql innodb 索引使用指南

參考資料:
create-index
https://dev.mysql.com/doc/internals/en/innodb-fil-header.html
高性能 mysql
mysql 5.7 virtual generated columns index
create-table-generated-columns

紅黑樹(Red-Black Tree)

Diagram of binary tree. The black root node has two red children and four black grandchildren. The child nodes of the grandchildren are black nil pointers or red nodes with black nil pointers.

紅黑樹是一顆自平衡的二叉查找樹(只作到了近似平衡)

紅黑樹遵循以下規則:

  • 每一個節點要麼是紅色要麼是黑色
  • 根節點始終是黑色的
  • 沒有相鄰的兩個紅色節點(每一個紅色節點的兩個子節點都是黑色)
  • 從任意節點到任意葉子節點的路徑,包含相同數量的黑色節點

紅黑樹與 B+Tree 對比:

  • B+Tree 比紅黑樹的查詢性能更好,由於 B+Tree 是嚴格的平衡樹
  • 紅黑樹比 B+Tree 的插入和刪除性能更好(紅黑樹有更鬆散的平衡性,插入和刪除數據後樹的節點再平衡操做更少,性能更穩定)
  • 紅黑樹適合用於構建存儲在內存中的索引如 JDK 中 HashMap,B+Tree 適合用來構建存儲在磁盤中的索引,如 MySQL 和 Oracle 中的索引

JDK HashMap

Java 7 以及以前版本的 HashMap 同一個桶(Bucket)裏面的節點(Entry)使用鏈表(Linked list)串聯起來,當同一個桶裏面存在過多節點時(對不一樣 key 的 hashcode 函數取值相等),查詢時間的複雜度會從哈希 O(1) 退化到鏈表 O(N),爲了不上述問題, Java 8 的 HashMap 同一個桶中的節點個數在知足必定條件時會使用紅黑樹結構代替鏈表結構

紅黑樹和鏈表相互轉換規則:

  • 當單個桶中的節點個數大於 TREEIFY_THRESHOLD( 默認 8),而且桶的個數大於 MIN_TREEIFY_CAPACITY( 默認 64),對應的桶會使用紅黑樹替代鏈表結構
  • 當移除元素後單個桶中的節點個數小於 UNTREEIFY_THRESHOLD( 默認 6),對應的桶會從紅黑樹恢復到鏈表結構
/**
 * The bin count threshold for using a tree rather than list for a bin.  
 * Bins are converted to trees when adding an element to a bin with at least this many 
 * nodes The value must be greater than 2 and should be at least 8 to mesh with 
 * assumptions in tree removal about conversion back to plain bins upon shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;
/**
 * The bin count threshold for untreeifying a bin during a resize operation.Should be less
 * than TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;
/**
 * The smallest table capacity for which bins may be treeified. (Otherwise the table is 
 * resized if too many nodes in a bin.) Should be at least 4 plus TREEIFY_THRESHOLD to 
 * avoid conflicts between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

JDK ConcurrentSkipListMap

  • ConcurrentSkiplistMap 對插入,刪除,更新和獲取元素支持併發操做
  • map 的元素根據建立時 key 的天然順序排序
  • 針對 containsKey、get、put 和 remove 操做確保了 O(log(n)) 的平均時間複雜度
  • ConcurrentSkiplistMap 是基於 skiplist 結構實現的
/**
 * A scalable concurrent ConcurrentNavigableMap implementation.The map is sorted 
 * according to the {@linkplain Comparable natural ordering} of its keys, or by a 
 * Comparator provided at map creation time, depending on which constructor is used.
 *
 * <p>This class implements a concurrent variant of SkipLists providing expected 
 * average <i>log(n)</i> time cost for the {@code containsKey}, {@code get}, 
 * {@code put} and {@code remove} operations and their variants.  Insertion, removal,
 * update, and access operations safely execute concurrently by multiple threads.
 */

JDK TreeMap 與 TreeSet

  • JDK TreeMap 是基於紅黑樹實現了 java.util.NavigableMap 接口
  • TreeMap 的元素根據建立時 key 的天然順序排序
  • TreeMap 提供了在 log(N) 平均時間複雜度下的 get,put,containsKey 和 remove 操做
/**
 * A Red-Black tree based {@link NavigableMap} implementation. The map is sorted according 
 * to the {@linkplain Comparable natural ordering} of its keys, or by a {@link Comparator} 
 * provided at map creation time, depending on which constructor is used.
 * 
 * This implementation provides guaranteed log(n) time cost for the {@code containsKey}, 
 * {@code get}, {@code put} and {@code remove} operations.  Algorithms are adaptations of 
 * those in Cormen, Leiserson, and Rivest's Introduction to Algorithms.
 */
  • JDK TreeSet 是基於 TreeMap 實現了 java.util.NavigableSet 接口
  • TreeSet 的元素根據建立時 key 的天然順序排序
  • TreeSet 提供了在 log(N) 平均時間複雜度下的 add,remove 和 contains 操做
/**
 * A {@link NavigableSet} implementation based on a {@link TreeMap}.
 * The elements are ordered using their {@linkplain Comparable natural
 * ordering}, or by a {@link Comparator} provided at set creation
 * time, depending on which constructor is used.
 *
 * <p>This implementation provides guaranteed log(n) time cost for the basic
 * operations ({@code add}, {@code remove} and {@code contains}).
 */

常見數據結構空間時間複雜度

image.png
參考資料:
https://www.bigocheatsheet.com/

相關文章
相關標籤/搜索