聊聊Mysql索引和redis跳錶 ---redis的有序集合zset數據結構底層採用了跳錶原理 時間複雜度O(logn)(阿里)

redis使用跳錶不用B+數的緣由是:redis是內存數據庫,而B+樹純粹是爲了mysql這種IO數據庫準備的。B+樹的每一個節點的數量都是一個mysql分區頁的大小(阿里面試)html

還有個幾個姊妹篇:介紹mysql的B+索引原理 參考:一步步分析爲何B+樹適合做爲索引的結構 以及索引原理 (阿里面試)mysql

參考:kafka如何實現高併發存儲-如何找到一條須要消費的數據(阿里)面試

參考:二分查找法:各類排序算法的時間複雜度和空間複雜度(阿里)redis

關於mysql 存儲引擎 介紹包括默認的索引方式參考:MySql的多存儲引擎架構, 默認的引擎InnoDB與 MYISAM的區別(滴滴 阿里)算法

敲黑板:

每級遍歷 3 個結點便可,而跳錶的高度爲 h ,因此每次查找一個結點時,須要遍歷的結點數爲 3*跳錶高度 ,因此忽略低階項和係數後的時間複雜度就是 ○(㏒n),空間複雜度是O(n) 

數據結構 實現原理 key查詢方式 查找效率 存儲大小 插入、刪除效率
Hash 哈希表 支持單key 接近O(1) 小,除了數據沒有額外的存儲 O(1)
B+樹 平衡二叉樹擴展而來 單key,範圍,分頁 O(Log(n) 除了數據,還多了左右指針,以及葉子節點指針 O(Log(n),須要調整樹的結構,算法比較複雜
跳錶 有序鏈表擴展而來 單key,分頁 O(Log(n) 除了數據,還多了指針,可是每一個節點的指針小於<2,因此比B+樹佔用空間小 O(Log(n),只用處理鏈表,算法比較簡單

 對LSM結構感興趣的能夠看下cassandra vs mongo (1)存儲引擎sql

問題

若是對如下問題感到困惑或只知其一;不知其二,請繼續看下去,相信本文必定會對你有幫助數據庫

  • mysql 索引如何實現
  • mysql 索引結構B+樹與hash有何區別。分別適用於什麼場景
  • 數據庫的索引還能有其餘實現嗎
  • redis跳錶是如何實現的
  • 跳錶和B+樹,LSM樹有和區別呢

解析

首先爲何要把mysql索引和redis跳錶放在一塊兒討論呢,由於他們解決的都是同一種問題,用於解決數據集合的查找問題,即根據指定的key,快速查到它所在的位置(或者對應的value)編程

當你站在這個角度去思考問題時,還會不知道B+樹索引和hash索引的區別嗎數組

數據集合的查找問題

如今咱們將問題領域邊界劃分清楚了,就是爲了解決數據集合的查找問題。這一塊須要考慮哪些問題呢安全

  1. 須要支持哪些查找方式,單key/多key/範圍查找,
  2. 插入/刪除效率
  3. 查找效率(即時間複雜度)
  4. 存儲大小(空間複雜度)

咱們看下幾種經常使用的查找結構

hash

 在這裏插入圖片描述

hash是key,value形式,經過一個散列函數,可以根據key快速找到value

關於hash算法 ,這也是阿里的必考題 深度的原理 我寫了幾篇博客:尤爲是最後一篇resize ,以及resize以前與以後的hashmap的狀況,

      參考:HashMap的實現原理--鏈表散列

      參考:Hashtable數據存儲結構-遍歷規則,Hash類型的複雜度爲啥都是O(1)-源碼分析 

      參考:HashMap, HashTable,HashSet,TreeMap 的時間複雜度   

      參考:HashMap底層實現原理/HashMap與HashTable區別/HashMap與HashSet區別 

      參考:ConcurrentHashMap原理分析(1.7與1.8)-put和 get 兩次Hash到達指定的HashEntry 

resize 參考:HashMap多線程併發問題分析-正常和異常的rehash1(阿里)

B+ 樹:

注意 這是關於B+樹的總結,若是你掌握到這個程度 是遠遠不夠的,

請參考詳細的B+樹原理一步步分析爲何B+樹適合做爲索引的結構 以及索引原理 (阿里面試)

B+樹 的數據都在葉子節點,非葉子節點存放 索引

 

在這裏插入圖片描述

 

B+樹是在平衡二叉樹基礎上演變過來,爲何咱們在算法課上沒學到B+樹和跳錶這種結構呢。由於他們都是從工程實踐中獲得,在理論的基礎上進行了妥協。

B+樹首先是有序結構,爲了避免至於樹的高度過高,影響查找效率,在葉子節點上存儲的不是單個數據,而是一頁數據,提升了查找效率,而爲了更好的支持範圍查詢,B+樹在葉子節點冗餘了非葉子節點數據,爲了支持翻頁,葉子節點之間經過指針鏈接。

跳錶  

 

跳錶:爲何 Redis 必定要用跳錶來實現有序集合? 

上幾篇主要是學習二分查找算法,可是二分查找底層依賴的是數組隨機訪問的特性,因此只能用數組來實現。若是數據存儲在鏈表中,就沒辦法使用二分查找了嗎? 

此時跳錶出現了,跳錶(Skip list) 實際上就是在鏈表的基礎上改造生成的。 

跳錶是一種各方面性能都比較優秀的 動態數據結構,能夠支持快速的插入、刪除、查找操做,寫起來也不復雜,甚至能夠替代 紅黑樹??。 

Redis 一共有5種數據結構,包括:

 

一、字符串(String)
redis對於KV的操做效率很高,能夠直接用做計數器。例如,統計在線人數等等,另外string類型是二進制存儲安全的,因此也可使用它來存儲圖片,甚至是視頻等。

二、哈希(hash)
存放鍵值對,通常能夠用來存某個對象的基本屬性信息,例如,用戶信息,商品信息等,另外,因爲hash的大小在小於配置的大小的時候使用的是ziplist結構,比較節約內存,因此針對大量的數據存儲能夠考慮使用hash來分段存儲來達到壓縮數據量,節約內存的目的,例如,對於大批量的商品對應的圖片地址名稱。好比:商品編碼固定是10位,能夠選取前7位作爲hash的key,後三位做爲field,圖片地址做爲value。這樣每一個hash表都不超過999個,只要把redis.conf中的hash-max-ziplist-entries改成1024,便可。
三、列表(List)
列表類型,能夠用於實現消息隊列,也可使用它提供的range命令,作分頁查詢功能。

 

四、集合(Set)
集合,整數的有序列表能夠直接使用set。能夠用做某些去重功能,例如用戶名不能重複等,另外,還能夠對集合進行交集,並集操做,來查找某些元素的共同點

 

五、有序集合(zset)
有序集合,可使用範圍查找,排行榜功能或者topN功能。

其中第五個zset 有序集合 就是用跳錶來實現的那 Redis 爲何會選擇用跳錶來實現有序集合呢?  

1、如何理解跳錶? 

對於單鏈表來講,咱們查找某個數據,只能從頭至尾遍歷鏈表,此時時間複雜度是 ○(n)。 

 
單鏈表  

那麼怎麼提升單鏈表的查找效率呢?看下圖,對鏈表創建一級 索引,每兩個節點提取一個結點到上一級,被抽出來的這級叫作 索引 或 索引層。 

 
第一級索引  

開發中常常會用到一種處理方式,hashmap 中存儲的值類型是一個 list,這裏就能夠把索引當作 hashmap 中的鍵,將每 2 個結點當作每一個鍵對應的值 list。 

因此要找到13,就不須要將16前的結點全遍歷一遍,只須要遍歷索引,找到13,而後發現下一個結點是17,那麼16必定是在 [13,17] 之間的,此時在13位置降低到原始鏈表層,找到16,加上一層索引後,查找一個結點須要遍歷的結點個數減小了,也就是說查找效率提升了 

那麼咱們再加一級索引呢?
跟前面創建一級索引的方式類似,咱們在第一級索引的基礎上,每兩個結點就抽出一個結點到第二級索引。此時再查找16,只須要遍歷 6 個結點了,須要遍歷的結點數量又減小了。 

 
第二級索引  

當結點數量多的時候,這種添加索引的方式,會使查詢效率提升的很是明顯、

 
這種鏈表加多級索引的結構,就是跳錶。  

2、用跳錶查詢到底有多快 

在一個單鏈表中,查詢某個數據的時間複雜度是 ○(n),那在一個具備多級索引的跳錶中,查詢某個數據的時間複雜度是多少呢? 

按照上面的示例,每兩個節點就抽出一個一級索引,每兩個一級索引又抽出一個二級索引,因此第一級索引的結點個數大約就是 n/2,第二級索引的結點個數就是 n/4,第 k 級索引的結點個數就是 n/2^k。 

假設一共創建了 h 級索引,最高級的索引有兩個節點(若是最高級索引只有一個結點,那麼這一級索引發不到判斷區間的做用,那麼是沒什麼意義的),因此有: 

 
時間複雜度的分析  
 
每級遍歷多少個結點  

根據上圖得知,每級遍歷 3 個結點便可,而跳錶的高度爲 h ,因此每次查找一個結點時,須要遍歷的結點數爲 3*跳錶高度 ,因此忽略低階項和係數後的時間複雜度就是 ○(㏒n) 

其實此時就至關於基於單鏈表實現了二分查找。可是這種查詢效率的提高,因爲創建了不少級索引,會不會很浪費內存呢? 

3、跳錶是否是很浪費內存? 

來分析一下跳錶的空間複雜度。 爲O(n)

 
每層索引結點數  
 
空間複雜度  

因此若是將包含 n 個結點的單鏈表構形成跳錶,咱們須要額外再用接近 n 個結點的存儲空間,那怎麼才能下降索引佔用的內存空間呢? 

前面是每兩個結點抽一個結點到上級索引,若是咱們每三個,或每五個結點,抽一個結點到上級索引,是否是就不用那麼多索引結點了呢? 

 
每三個結點抽取一個上級索引  

計算空間複雜度的過程與前面的一致,儘管最後空間複雜度依然是 ○(n),但咱們知道,使用大○表示法忽略的低階項或係數,實際上一樣會產生影響,只不過咱們爲了關注高階項而將它們忽略。 

 
空間複雜度  

實際上,在實際開發中,咱們不須要太在乎索引佔據的額外空間,在學習數據結構與算法時,咱們習慣的將待處理數據當作整數,可是實際開發中,原始鏈表中存儲的極可能是很大的對象,而索引結點只須要存儲關鍵值(用來比較的值)和幾個指針(找到下級索引的指針),並不須要存儲原始鏈表中完整的對象,因此當對象比索引結點大不少時,那索引佔用的額外空間就能夠忽略了。 

4、高效的動態插入和刪除 

跳錶這個動態數據結構,不只支持查找操做,還支持動態的插入、刪除操做,並且插入、刪除操做的時間複雜度也是 ○(㏒n)。 

對於單純的單鏈表,須要遍歷每一個結點來找到插入的位置。可是對於跳錶來講,由於其查找某個結點的時間複雜度是 ○(㏒n),因此這裏查找某個數據應該插入的位置,時間複雜度也是 ○(㏒n)。 

 
插入操做  

那麼刪除操做呢? 

 
刪除操做  

5、跳錶索引動態更新 

當咱們不停的往跳錶中插入數據時,若是咱們不更新索引,就可能出現某 2 個索引結點之間數據很是多的狀況。極端狀況下,跳錶會退化成單鏈表。 

 
做爲一種動態數據結構,咱們須要某種手段來維護索引與原始鏈表大小之間的平滑,也就是說若是鏈表中結點多了,索引結點就相應地增長一些,避免複雜度退化,以及查找、插入、刪除操做性能降低。

跳錶是經過隨機函數來維護前面提到的 平衡性。 

咱們往跳錶中插入數據的時候,能夠選擇同時將這個數據插入到第幾級索引中,好比隨機函數生成了值 K,那咱們就將這個結點添加到第一級到第 K 級這 K 級索引中。 

 
隨機函數能夠保證跳錶的索引大小和數據大小的平衡性,不至於性能過分退化。

跳錶的實現有點複雜,而且跳錶的實現並非這篇的重點。主要是學習思路。 

6、解答開篇 

Redis 中的有序集合是經過跳錶來實現的,嚴格點講,還用到了散列表(關於散列表),若是查看 Redis 開發手冊,會發現 Redis 中的有序集合支持的核心操做主要有下面這幾個: 

  • 插入一個數據
  • 刪除一個數據
  • 查找一個數據
  • 按照區間查找數據(好比查找在[100,356]之間的數據)
  • 迭代輸出有序序列 

其中,插入、查找、刪除以及迭代輸出有序序列這幾個操做,紅黑樹也能完成,時間複雜度和跳錶是同樣的,可是,按照區間來查找數據這個操做,紅黑樹的效率沒有跳錶高。 

對於按照區間查找數據這個操做,跳錶能夠作到 ○(㏒n) 的時間複雜度定位區間的起點,而後在原始鏈表中順序日後遍歷就能夠了。這樣作很是高效。 

固然,還有其餘緣由,好比,跳錶代碼更容易實現,可讀性好不易出錯。跳錶更加靈活,能夠經過改變索引構建策略,有效平衡執行效率和內存消耗。 

不過跳錶也不能徹底替代紅黑樹。由於紅黑樹出現的更早一些。不少編程語言中的 Map 類型都是用紅黑樹來實現的。寫業務的時候直接用就行,可是跳錶沒有現成的實現,開發中想用跳錶,得本身實現。 

參考:Redis詳解(四)------ redis的底層數據結構

參考:聊聊Mysql索引和redis跳錶

參考:redis的五種數據結構原理分析

相關文章
相關標籤/搜索