關于redis,通常我們的認知是他很快,拋開io設計模式及線程架構,我們單從底層數據結構了解一下redis的trade-off.
先了解一下redis的sds,sds的編碼有3種:int、raw或者embstr,當存入的的內容是Long型,redisObject的ptr指針會直接賦值為數據,這樣節省了指針空間開銷,當存入的內容是字符串,一種情況字符串小于等于44字節,redisObjec的元數據,指針和sds是同一塊內存區域,是連續的,可以避免內存碎片,這種編碼方式為embstr,當字符串大于44字節,那么sds將會是一塊獨立空間,ptr指向這塊內存。然后關于sds本身的結構,分為兩部分sds指針和sdshdr,sds是一個char類型的指針,指向buf數組首元素,sdshdr儲存頭部信息,例如len長度,作用是避免O(N)的便利而以O(1)直接獲得長度,sdshdr的另一個屬性bff數組,儲存實際數據,根據不同的字符串長度,有5,8,16,32,64五種類型,作用是節省內存。
可以看出,redis關于字符串的設計,存取時間復雜度交給本身字典的O(1),內存方面不采用c的字符串而是使用sds,首先是避免了c的字符串擴縮容的內存溢出和泄露,sds通過預分配機制,就是原本的len+新插入的數據len2,如果原本空間夠用,那么不擴容,如果len+len2 < 1Mb,那么分配
2*(len+len2),如果小于1Mb,那么分配len+len2+1M。其次是可以儲存各種類型的數據,例如音視頻數據,因為c字符串以\0代表結束,而sds是以len長度為結束,這樣便能支持更多場景的數據。
接下來結合場景以及另一個數據結構ziplist,對比一下sds,講述一下關于我們日常開發中系統設計的trade-off,壓縮列表表頭有3個字段,zlbytes,zltail和zllen,本身的entry有pre-len,len,encoding和content,通過頭部屬性,我們對表頭表尾的操作皆為O(1),而整體操作的時間復雜度便不盡人意,因為我們要遍歷。所以可以看出redis在時間空間的trade-off上面,選擇了節省內存。接下來通過一個例子,從而了解ziplist的意義
有個場景,本地有1億張圖片,對應云儲存上的圖片,我們的需求是需要快速通過本地的圖片id找到實際云上資源,那么需要把本地的id和云上的id做映射儲存起來,假如我們通過string儲存 ,根據sds本身的屬性空間開銷和redisObjec的開銷加上id數據空間開銷加上全局hash表的dictEntry:key value next三個指針,加上redis內存分配機制,jemalloc會 根據我們申請的字節數去找到最接近的2的冪次數,比如申請6字節實際會分配8字節,籠統下來,存儲一張圖片的10位id的映射需要64字節,而實際上呢,我們兩個10位id,long型實際只需要16字節。因為如果用string的話,一個鍵值對就需要一個dictEntry,如果我們采用集合,那么一個集合才消耗一個dictEntry,基于ziplist實現的數據類型有 list,hash和sorted set.
hash底層有2中數據接口,ziplist和hash表,如果我們配置好hash-max-ziplist-entries和hash-max-ziplist-value,這樣在我們的條件內,就不會轉換成hash表從而使用ziplist儲存,需要注意的地方是,pre-len是代表前一個entry的長度,如果前一個entry的長度小于255字節,那么pre-len的取值就是1字節反之5字節,所以我們要使用ziplist的時候要注意數據的增刪會引起ziplist的連鎖更新,比如本來前一個entry是小于255的 插進來一個大于255的,那么我的pre-len得變成5,假如我本身在253,那么prelen的變化會使我的大小超過255,那么從而影響到我的下一個entry.
關于skiplist,通過建立多級索引從而可以實現O(logN)的時間復雜度,思想類似二分法,不多贅述。
所以從數據結構可以看出,redis提供了在時間和空間各有優勢的數據結構,得看我們的使用場景的具體選擇從而發揮redis在時間和空間上面的優勢,舉個例子,redis的list,底層實現是雙向鏈表和壓縮列表,操作復雜度0(N),但是呢他的頭尾操作效率很高,我們就不要用它來做隨機讀取,而應當用于FIFO的場景,發揮數據結構本身的優勢,再比如sortSet,底層是skiplist,我們則應該盡量避免遍歷操作,不可避免的話,建議用scan代替