日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis 数据结构 :SDS、链表、字典、跳表、整数集合、压缩列表

發(fā)布時(shí)間:2024/4/11 数据库 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis 数据结构 :SDS、链表、字典、跳表、整数集合、压缩列表 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

  • SDS
    • 結(jié)構(gòu)分析
    • 內(nèi)存策略
      • 空間預(yù)分配
      • 惰性空間釋放
    • 總結(jié)
  • 鏈表
    • 結(jié)構(gòu)分析
    • 總結(jié)
  • 字典
    • 結(jié)構(gòu)分析
    • rehash
    • 漸進(jìn)式rehash
    • 總結(jié)
  • 跳表
    • 結(jié)構(gòu)分析
    • 總結(jié)
  • 整數(shù)集合
    • 結(jié)構(gòu)分析
    • 升級
    • 降級
    • 總結(jié)
  • 壓縮列表
    • 結(jié)構(gòu)分析
    • 連鎖更新
    • 總結(jié)


SDS

結(jié)構(gòu)分析

由于C字符串存在大量問題,所以在Redis中,并沒有使用C風(fēng)格字符串,而是自己構(gòu)建了一個(gè)簡單動(dòng)態(tài)字符串即SDS(simple dynamic string)

struct sdshdr { // buf 中已占用空間的長度int len;// buf 中剩余可用空間的長度int free;// 數(shù)據(jù)空間char buf[]; };

為解決C字符串緩沖區(qū)溢出問題以及長度計(jì)算問題,SDS中引入了len來統(tǒng)計(jì)當(dāng)前已使用空間長度,free來計(jì)算剩余的空間長度

C字符串的主要缺陷就是因?yàn)樗鼪]有記錄自己的長度,而如果在需要了解長度時(shí),就只能通過O(N)的效率進(jìn)行一次遍歷


并且因?yàn)镃字符串沒有統(tǒng)計(jì)剩余空間的字段,也沒有容量字段,所以很容易就會(huì)因?yàn)閟trcat等函數(shù)造成緩沖區(qū)的溢出,為彌補(bǔ)這一缺陷,redis在sds中增加了free字段

通過標(biāo)記剩余空間,當(dāng)對SDS進(jìn)行插入操作時(shí),就會(huì)提前判斷當(dāng)前剩余空間是否足夠,如果不足則會(huì)先進(jìn)行空間的拓展,再進(jìn)行插入,這樣就解決了緩沖區(qū)溢出的問題


內(nèi)存策略

由于Redis作為一個(gè)高效的內(nèi)存數(shù)據(jù)庫,用于速度要求嚴(yán)苛,插入刪除頻繁
的場景,為了提高內(nèi)存分配的效率,防止大量使用內(nèi)存重分配而調(diào)用系統(tǒng)函數(shù)導(dǎo)致的性能損失問題(用戶態(tài)和內(nèi)核態(tài)的切換),Redis主要依靠空間預(yù)分配和惰性空間釋放來解決這個(gè)問題


空間預(yù)分配

為減少空間分配的次數(shù),當(dāng)需要進(jìn)行空間拓展時(shí),不僅僅會(huì)為SDS分配修改所必須要的空間,并且會(huì)為SDS預(yù)分配額外的未使用空間。

預(yù)分配未使用空間的策略如下

  • 當(dāng)SDS修改后的長度小于1MB時(shí),將會(huì)預(yù)分配大小和當(dāng)前l(fā)en一樣的空間(free = len),也就是使空間增長一倍,來減少因?yàn)槌跏紩r(shí)申請大空間導(dǎo)致的連續(xù)分配問題
  • 當(dāng)SDS修改后的長度大于等于1MB時(shí),每次分配都會(huì)分配1MB的空間,防止空間的浪費(fèi)。

惰性空間釋放

當(dāng)我們對SDS進(jìn)行刪除操作時(shí),并不會(huì)立即回收刪除后空余的空間,而是將空余空間以free字段記錄下來,以備后面使用。
這樣做的目的在于防止因?yàn)榭臻g縮短后因?yàn)樵俣炔迦雽?dǎo)致的空間拓展問題
并且如果有需求需要真正釋放空間,Redis也提供了對應(yīng)的API,所以不必?fù)?dān)心會(huì)因?yàn)槎栊缘目臻g釋放而導(dǎo)致的內(nèi)存浪費(fèi)問題。


總結(jié)

比起 C 字符串, SDS 具有以下優(yōu)點(diǎn):

  • 常數(shù)復(fù)雜度獲取字符串長度。(len字段)
  • 杜絕緩沖區(qū)溢出。(free字段)
  • 減少修改字符串長度時(shí)所需的內(nèi)存重分配次數(shù)。(空間預(yù)分配,惰性空間釋放)
  • 二進(jìn)制安全。(以二進(jìn)制形式處理)
  • 兼容部分 C 字符串函數(shù)。(底層基于C字符串,以空字符結(jié)尾)

  • 鏈表

    結(jié)構(gòu)分析

    typedef struct listNode {// 前置節(jié)點(diǎn)struct listNode *prev;// 后置節(jié)點(diǎn)struct listNode *next;// 節(jié)點(diǎn)的值void *value; } listNode;/** 雙端鏈表迭代器*/ typedef struct listIter {// 當(dāng)前迭代到的節(jié)點(diǎn)listNode *next;// 迭代的方向int direction; } listIter;/** 雙端鏈表結(jié)構(gòu)*/ typedef struct list {// 表頭節(jié)點(diǎn)listNode *head;// 表尾節(jié)點(diǎn)listNode *tail;// 節(jié)點(diǎn)值復(fù)制函數(shù)void *(*dup)(void *ptr);// 節(jié)點(diǎn)值釋放函數(shù)void (*free)(void *ptr);// 節(jié)點(diǎn)值對比函數(shù)int (*match)(void *ptr, void *key);// 鏈表所包含的節(jié)點(diǎn)數(shù)量unsigned long len;} list;


    從上面的結(jié)構(gòu)可以看出,Redis的鏈表是一個(gè)帶頭尾的雙端無環(huán)鏈表,并且通過len字段記錄了鏈表節(jié)點(diǎn)的長度

    同時(shí)為了實(shí)現(xiàn)多態(tài)與泛型,鏈表中還提供了dup,free,match屬性來設(shè)置相關(guān)的函數(shù),使得鏈表支持不同類型的值的存儲(chǔ)


    總結(jié)

    • 鏈表被廣泛用于實(shí)現(xiàn) Redis 的各種功能, 比如列表鍵, 發(fā)布與訂閱, 慢查詢, 監(jiān)視器, 等等。
    • 每個(gè)鏈表節(jié)點(diǎn)由一個(gè) listNode 結(jié)構(gòu)來表示, 每個(gè)節(jié)點(diǎn)都有一個(gè)指向前置節(jié)點(diǎn)和后置節(jié)點(diǎn)的指針, 所以 Redis 的鏈表實(shí)現(xiàn)是雙端鏈表。
    • 每個(gè)鏈表使用一個(gè) list 結(jié)構(gòu)來表示, 這個(gè)結(jié)構(gòu)帶有表頭節(jié)點(diǎn)指針、表尾節(jié)點(diǎn)指針、以及鏈表長度等信息。
    • 因?yàn)殒湵肀眍^節(jié)點(diǎn)的前置節(jié)點(diǎn)和表尾節(jié)點(diǎn)的后置節(jié)點(diǎn)都指向 NULL , 所以 Redis 的鏈表實(shí)現(xiàn)是無環(huán)鏈表。
    • 通過為鏈表設(shè)置不同的類型特定函數(shù), Redis 的鏈表可以用于保存各種不同類型的值。

    字典

    結(jié)構(gòu)分析

    Redis的字典底層采用了哈希表來進(jìn)行實(shí)現(xiàn)。

    首先看看字典底層哈希表的結(jié)構(gòu)

    typedef struct dictht {// 哈希表數(shù)組dictEntry **table;// 哈希表大小unsigned long size;// 哈希表大小掩碼,用于計(jì)算索引值// 總是等于 size - 1unsigned long sizemask;// 該哈希表已有節(jié)點(diǎn)的數(shù)量unsigned long used;} dictht;


    哈希表中記錄了當(dāng)前的總長度,已有節(jié)點(diǎn),以及當(dāng)前索引大小(用于哈希函數(shù)來計(jì)算節(jié)點(diǎn)位置)

    為解決哈希沖突,Redis字典采用了鏈地址法來構(gòu)造了哈希桶的結(jié)構(gòu),也就是哈希數(shù)組中的每個(gè)元素都是一個(gè)鏈表。

    下面來看看哈希節(jié)點(diǎn)的結(jié)構(gòu)

    typedef struct dictEntry {// 鍵void *key;// 值union {void *val;uint64_t u64;int64_t s64;} v;// 指向下個(gè)哈希表節(jié)點(diǎn),形成鏈表struct dictEntry *next; } dictEntry;

    可以看到,為保證鍵值對適用于多重類型,key值使用的時(shí)void的形式,而value使用了64位有符號整型和64位無符號整型,void指針的一個(gè)聯(lián)合體,每個(gè)節(jié)點(diǎn)使用next來鏈接成一個(gè)鏈表

    typedef struct dictType {// 計(jì)算哈希值的函數(shù)unsigned int (*hashFunction)(const void *key);// 復(fù)制鍵的函數(shù)void *(*keyDup)(void *privdata, const void *key);// 復(fù)制值的函數(shù)void *(*valDup)(void *privdata, const void *obj);// 對比鍵的函數(shù)int (*keyCompare)(void *privdata, const void *key1, const void *key2);// 銷毀鍵的函數(shù)void (*keyDestructor)(void *privdata, void *key);// 銷毀值的函數(shù)void (*valDestructor)(void *privdata, void *obj);} dictType;

    為保證字典具有多態(tài)及泛型,dictType中提供了如哈希函數(shù)以及K-V的各種操作函數(shù),使得字典適用于多重情景


    rehash

    /** 字典*/ typedef struct dict {// 類型特定函數(shù)dictType *type;// 私有數(shù)據(jù)void *privdata;// 哈希表dictht ht[2];// rehash 索引// 當(dāng) rehash 不在進(jìn)行時(shí),值為 -1int rehashidx; /* rehashing not in progress if rehashidx == -1 */// 目前正在運(yùn)行的安全迭代器的數(shù)量int iterators; /* number of iterators currently running */} dict;

    從字典的結(jié)構(gòu)中,我們可以看到里面同時(shí)存放了兩個(gè)哈希表,以及一個(gè)rehashidx屬性。
    這就牽扯到了字典的核心之一,rehash。

    Redis作為一個(gè)插入頻繁且對效率要求高的數(shù)據(jù)庫,當(dāng)插入的數(shù)據(jù)過多時(shí),就會(huì)因?yàn)楣1碇械呢?fù)載因子過高而導(dǎo)致查詢或者插入的效率降低,此時(shí)就需要通過rehash來進(jìn)行重新擴(kuò)容并重新映射。
    但是如果只是用一個(gè)哈希表,映射時(shí)就會(huì)導(dǎo)致數(shù)據(jù)庫暫時(shí)不可用,作為一個(gè)使用頻繁的數(shù)據(jù)庫,短期的停機(jī)幾乎是不可容許的問題,所以Redis設(shè)計(jì)時(shí)采用了雙哈希的結(jié)構(gòu),并采用了漸進(jìn)式rehash的方法來解決這個(gè)問題。

    rehash的步驟如下

    • 為ht[1]的哈希表分配空間
    • 將ht[0]中的鍵值對重新映射到ht[1]上
    • 當(dāng)ht[0]的數(shù)據(jù)遷移完成,此時(shí)ht[0]為一個(gè)空表,此時(shí)釋放ht[0],并讓ht[1]成為新的ht[0],再為ht[1]創(chuàng)建一個(gè)新的空白哈希表,為下一次的rehash做準(zhǔn)備



    漸進(jìn)式rehash

    由于數(shù)據(jù)庫中可能存在大量的數(shù)據(jù),而rehash的時(shí)候又過長,為了避免因?yàn)閞ehash造成的服務(wù)器停機(jī),rehash的過程并不是一次完成的,而是一個(gè)多次的,漸進(jìn)式的過程。







    在漸進(jìn)式rehash的時(shí)候,由于數(shù)據(jù)不斷的進(jìn)行遷移,無法確定數(shù)據(jù)處于哪一個(gè)表上, 此時(shí)如果進(jìn)行插入、刪除、查找的操作時(shí)就會(huì)在兩個(gè)表上進(jìn)行,如果在一個(gè)表中沒找到對應(yīng)數(shù)據(jù),就會(huì)到另一個(gè)表中繼續(xù)查找。

    并且如果此時(shí)新插入節(jié)點(diǎn),都會(huì)統(tǒng)一的防止在新表ht[1]中,防止對ht[0]的rehash造成干擾,保證ht[0]節(jié)點(diǎn)的只減少不增加


    總結(jié)

    • 字典被廣泛用于實(shí)現(xiàn) Redis 的各種功能, 其中包括數(shù)據(jù)庫和哈希鍵。
    • Redis 中的字典使用哈希表作為底層實(shí)現(xiàn), 每個(gè)字典帶有兩個(gè)哈希表, 一個(gè)用于平時(shí)使用, 另一個(gè)僅在進(jìn)行 rehash 時(shí)使用。
    • 當(dāng)字典被用作數(shù)據(jù)庫的底層實(shí)現(xiàn), 或者哈希鍵的底層實(shí)現(xiàn)時(shí), Redis 使用 MurmurHash2 算法來計(jì)算鍵的哈希值。
    • 哈希表使用鏈地址法來解決鍵沖突, 被分配到同一個(gè)索引上的多個(gè)鍵值對會(huì)連接成一個(gè)單向鏈表。
    • 在對哈希表進(jìn)行擴(kuò)展或者收縮操作時(shí), 程序需要將現(xiàn)有哈希表包含的所有鍵值對 rehash 到新哈希表里面, 并且這個(gè) rehash 過程并不是一次性地完成的, 而是漸進(jìn)式地完成的。

    跳表

    跳表是一個(gè)較為少見的數(shù)據(jù)結(jié)構(gòu),如果不了解的可以看看我之前的博客
    看了這篇博客,還敢說你不懂跳表嗎?
    由于跳表的實(shí)現(xiàn)簡單且性能可與平衡樹相媲美,對于大量插入刪除的數(shù)據(jù)庫來說,跳表只需要進(jìn)行簡單的鏈表插入和索引的選拔,而不像平衡樹一樣需要進(jìn)行整體平衡的維持。并且由于在范圍查找上的效率遠(yuǎn)遠(yuǎn)強(qiáng)于平衡樹,所以Redis底層選取跳表來作為有序集合的底層之一。

    結(jié)構(gòu)分析

    typedef struct zskiplistNode {// 成員對象robj *obj;// 分值double score;// 后退指針struct zskiplistNode *backward;// 層struct zskiplistLevel {// 前進(jìn)指針struct zskiplistNode *forward;// 跨度unsigned int span;} level[];} zskiplistNode;

    跳躍表的查詢從最頂層出發(fā),通過前進(jìn)指針來往后查找,通過比較節(jié)點(diǎn)的分?jǐn)?shù)來判斷當(dāng)前節(jié)點(diǎn)是否與索引匹配,如果查找不到則進(jìn)入下層繼續(xù)查找,并記錄下跨越的層數(shù)span來進(jìn)行排位。

    同時(shí)為了處理特殊情況,還準(zhǔn)備了一個(gè)后退指針來進(jìn)行從表尾到表頭的遍歷,但是與前進(jìn)不同,后退指針并不存在跳躍,而是只能一個(gè)一個(gè)向后查詢

    /** 跳躍表*/ typedef struct zskiplist {// 表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)struct zskiplistNode *header, *tail;// 表中節(jié)點(diǎn)的數(shù)量unsigned long length;// 表中層數(shù)最大的節(jié)點(diǎn)的層數(shù)int level;} zskiplist;

    跳躍表通過保存表頭和表尾節(jié)點(diǎn),來快速訪問表頭和表尾。并且保存了節(jié)點(diǎn)的數(shù)量來實(shí)現(xiàn)O(1)的長度計(jì)算

    為了避免因?yàn)閷訑?shù)過高導(dǎo)致的大量空間損失,Redis跳躍表的節(jié)點(diǎn)高度最高位32層。


    總結(jié)

    • 跳躍表是有序集合的底層實(shí)現(xiàn)之一, 除此之外它在 Redis 中沒有其他應(yīng)用。
    • Redis 的跳躍表實(shí)現(xiàn)由 zskiplist 和 zskiplistNode 兩個(gè)結(jié)構(gòu)組成, 其中 zskiplist 用于保存跳躍表信息(比如表頭節(jié)點(diǎn)、表尾節(jié)點(diǎn)、長度), 而 zskiplistNode 則用于表示跳躍表節(jié)點(diǎn)。
    • 每個(gè)跳躍表節(jié)點(diǎn)的層高都是 1 至 32 之間的隨機(jī)數(shù)。
    • 在同一個(gè)跳躍表中, 多個(gè)節(jié)點(diǎn)可以包含相同的分值, 但每個(gè)節(jié)點(diǎn)的成員對象必須是唯一的。
    • 跳躍表中的節(jié)點(diǎn)按照分值大小進(jìn)行排序, 當(dāng)分值相同時(shí), 節(jié)點(diǎn)按照成員對象的大小進(jìn)行排序。

    整數(shù)集合

    整數(shù)集合時(shí)集合鍵的底層實(shí)現(xiàn)之一,當(dāng)集合中的元素全部都是整數(shù)值的時(shí)候,并且集合中元素不多時(shí),Redis就會(huì)使用整數(shù)集合來作為集合鍵的底層結(jié)構(gòu)。整數(shù)集合具有去重且排序的特性


    結(jié)構(gòu)分析

    typedef struct intset {// 編碼方式uint32_t encoding;// 集合包含的元素?cái)?shù)量uint32_t length;// 保存元素的數(shù)組int8_t contents[];} intset;

    在上面的結(jié)構(gòu)中,雖然content數(shù)組的類型是一個(gè)8bit的整型,但是數(shù)據(jù)真正存儲(chǔ)的方式并不是這個(gè)類型,而是根據(jù)encoding來決定具體的類型,8bit只是作為一個(gè)基本單位來進(jìn)行使用。

    例如此時(shí)encoding設(shè)置為INTSET_ENC_INT16時(shí),數(shù)組存儲(chǔ)的格式就有每個(gè)元素16bit

    如果encoding設(shè)置為INTSET_ENC_INT64時(shí),數(shù)組存儲(chǔ)的格式就有每個(gè)元素64bit


    升級

    升級的流程

  • 根據(jù)插入的類型來決定集合升級的類型,拓展數(shù)組的整體空間并為新節(jié)點(diǎn)分配空間
  • 對集合中所有數(shù)據(jù)類型進(jìn)行升級,在保持有序的前提下將所有升級后的元素移動(dòng)到合適的位置上
  • 將新節(jié)點(diǎn)插入到集合的對應(yīng)位置
  • 新元素的插入位置
    由于會(huì)引起升級的元素的類型都必頂比數(shù)組中的所有數(shù)據(jù)都大,所以也就決定了其要么比所有數(shù)據(jù)都大,要么比所有數(shù)據(jù)都小(負(fù)數(shù)),所以插入位置只能是首部和尾部

    • 當(dāng)新元素比所有數(shù)據(jù)都大時(shí)在尾部插入
    • 當(dāng)新元素比所有數(shù)據(jù)都小時(shí)在首部插入


      如果插入一個(gè)32位的數(shù)據(jù),則引起全體升級

      分配底層空間


      整體升級并挪動(dòng)位置

    插入元素


    降級

    在整數(shù)列表中升級是一個(gè)不可逆的過程,即使將所有高類型的數(shù)據(jù)刪除了,也不會(huì)進(jìn)行降級。
    理由是防止因?yàn)榻导壓笤俅紊墡淼拇罅繑?shù)據(jù)挪動(dòng)的問題,在保證了效率的同時(shí),也帶來了一定程度上的空間浪費(fèi)(非必要時(shí)盡量不要升級)


    升級帶來的好處

  • 靈活 : 由于C語言是一個(gè)靜態(tài)類型語言,為了避免出現(xiàn)類型錯(cuò)誤通常會(huì)將不同類型分開,例如如果要存儲(chǔ)16bit、32bit、64bit的整型就需要3個(gè)數(shù)組,但是使用整數(shù)集合就可以通過升級的策略來進(jìn)行元素類型的自適應(yīng),就可以任意的將各種類型的整數(shù)插入進(jìn)去,而不必?fù)?dān)心類型錯(cuò)誤
  • 節(jié)約內(nèi)存:與上面類似,如果我們只有一個(gè)64bit的數(shù)組,而里面存儲(chǔ)了不少16bit、32bit的元素,則會(huì)造成空間的大量浪費(fèi)。而使用整數(shù)集合則可以同時(shí)保存多個(gè)類型,只需要確保升級的操作只在必要時(shí)進(jìn)行,就不會(huì)造成空間的浪費(fèi)

  • 總結(jié)

    • 整數(shù)集合是集合鍵的底層實(shí)現(xiàn)之一。
    • 整數(shù)集合的底層實(shí)現(xiàn)為數(shù)組, 這個(gè)數(shù)組以有序、無重復(fù)的方式保存集合元素, 在有需要時(shí), 程序會(huì)根據(jù)新添加元素的類型, 改變這個(gè)數(shù)組的類型。
    • 升級操作為整數(shù)集合帶來了操作上的靈活性, 并且盡可能地節(jié)約了內(nèi)存。
    • 整數(shù)集合只支持升級操作, 不支持降級操作。

    壓縮列表

    壓縮列表(ziplist)是列表鍵和哈希鍵的底層實(shí)現(xiàn)之一。

    當(dāng)一個(gè)列表鍵只包含少量列表項(xiàng), 并且每個(gè)列表項(xiàng)要么就是小整數(shù)值, 要么就是長度比較短的字符串, 那么 Redis 就會(huì)使用壓縮列表來做列表鍵的底層實(shí)現(xiàn)。主要核心就是為了節(jié)約空間

    結(jié)構(gòu)分析




    例如這張圖,可以看出當(dāng)前包含三個(gè)節(jié)點(diǎn),總空間為0x50(十進(jìn)制80),到尾部的偏移量為0x3c(十進(jìn)制60),節(jié)點(diǎn)數(shù)量為0x3(十進(jìn)制3)


    每個(gè)壓縮列表節(jié)點(diǎn)可以由一個(gè)整數(shù)或者一個(gè)字節(jié)數(shù)組組成

    整數(shù)的類型可以是以下六種之一

    • 4位的介于0-12的無符號整數(shù)
    • 1字節(jié)的有符號整數(shù)
    • 3字節(jié)的有符號整數(shù)
    • int16_t
    • int32_t
    • int64_t

    字節(jié)數(shù)組可以是以下三種之一

    • 長度小于等于63(2^6 -1)字節(jié)的字節(jié)數(shù)組
    • 長度小于等于16383(2^14 -1)字節(jié)的字節(jié)數(shù)組
    • 長度小于等于4294967295(2^32 -1)字節(jié)的字節(jié)數(shù)組

    而壓縮列表節(jié)點(diǎn)又有三個(gè)屬性組成,分別是previous_entry_length,encoding,content。

    previous_entry_length
    這個(gè)屬性記錄了壓縮列表前一個(gè)節(jié)點(diǎn)的長度,該屬性根據(jù)前一個(gè)節(jié)點(diǎn)的大小不同可以是1個(gè)字節(jié)或者5個(gè)字節(jié)。

    • 如果前一個(gè)節(jié)點(diǎn)的長度小于254個(gè)字節(jié),那么previous_entry_length的大小為1個(gè)字節(jié),即前一個(gè)節(jié)點(diǎn)的長度可以使用1個(gè)字節(jié)表示
    • 如果前一個(gè)節(jié)點(diǎn)的長度大于等于254個(gè)字節(jié),那么previous_entry_length的大小為5個(gè)字節(jié),第一個(gè)字節(jié)會(huì)被設(shè)置為0xFE(十進(jìn)制的254),之后的四個(gè)字節(jié)則用于保存前一個(gè)節(jié)點(diǎn)的長度。

    小于254字節(jié)時(shí)的表示

    大于等于254字節(jié)時(shí)的表示

    為什么要這樣設(shè)計(jì)呢?
    由于壓縮列表中的數(shù)據(jù)以一種不規(guī)則的方式進(jìn)行緊鄰,無法通過后退指針來找到上一個(gè)元素,而通過保存上一個(gè)節(jié)點(diǎn)的長度,用當(dāng)前的地址減去這個(gè)長度,就可以很容易的獲取到了上一個(gè)節(jié)點(diǎn)的位置,通過一個(gè)一個(gè)節(jié)點(diǎn)向前回溯,來達(dá)到從表尾往表頭遍歷的操作

    encoding
    encoding通過以下規(guī)則來記錄content的類型

    • 一字節(jié)、兩字節(jié)或者五字節(jié)長, 值的最高位為 00 、 01 或者 10 的是字節(jié)數(shù)組編碼: 這種編碼表示節(jié)點(diǎn)的 content 屬性保存著字節(jié)數(shù)組, 數(shù)組的長度由編碼除去最高兩位之后的其他位記錄;
    • 一字節(jié)長, 值的最高位以 11 開頭的是整數(shù)編碼: 這種編碼表示節(jié)點(diǎn)的 content 屬性保存著整數(shù)值, 整數(shù)值的類型和長度由編碼除去最高兩位之后的其他位記錄;

    content
    content屬性負(fù)責(zé)保存節(jié)點(diǎn)的值,值的具體類型由上一個(gè)字段encoding來決定。

    例如存儲(chǔ)字節(jié)數(shù)組,00表示類型為字節(jié)數(shù)組,01011表示長度為11

    存儲(chǔ)整數(shù)值,表示存儲(chǔ)的為整數(shù),類型為int16_t


    連鎖更新

    當(dāng)添加或刪除節(jié)點(diǎn)時(shí),可能就會(huì)因?yàn)閜revious_entry_length的變化導(dǎo)致發(fā)生連鎖的更新操作。


    假設(shè)e1的previous_entry_length只有1個(gè)字節(jié),而新插入的節(jié)點(diǎn)大小超過了254字節(jié),此時(shí)由于e1
    的previous_entry_length無法該長度,就會(huì)將previous_entry_length的長度更新為5字節(jié)。

    但是問題來了,假設(shè)e1原本的大小為252字節(jié),當(dāng)previous_entry_length更新后它的大小則超過了254,此時(shí)又會(huì)引發(fā)對e2的更新。
    順著這個(gè)思路,一直更新下去


    同理,刪除也會(huì)引發(fā)連鎖的更新

    從上圖可以看出來,在最壞情況下,會(huì)從插入位置一直連鎖更新到末尾,即執(zhí)行了N次空間重分配, 而每次空間重分配的最壞復(fù)雜度為 O(N) , 所以連鎖更新的最壞復(fù)雜度為 O(N^2) 。

    即使存在這種情況,但是并不影響我們使用壓縮列表

    • 壓縮列表里要恰好有多個(gè)連續(xù)的、長度介于 250 字節(jié)至 253 字節(jié)之間的節(jié)點(diǎn), 連鎖更新才有可能被引發(fā), 這種情況就和連中彩票一樣,很少見
    • 即使出現(xiàn)連鎖更新, 但只要被更新的節(jié)點(diǎn)數(shù)量不多, 就不會(huì)對性能造成任何影響: 比如說, 對三五個(gè)節(jié)點(diǎn)進(jìn)行連鎖更新是絕對不會(huì)影響性能的;

    總結(jié)

    • 壓縮列表是一種為節(jié)約內(nèi)存而開發(fā)的順序型數(shù)據(jù)結(jié)構(gòu)。
    • 壓縮列表被用作列表鍵和哈希鍵的底層實(shí)現(xiàn)之一。
    • 壓縮列表可以包含多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者整數(shù)值。
    • 添加新節(jié)點(diǎn)到壓縮列表, 或者從壓縮列表中刪除節(jié)點(diǎn), 可能會(huì)引發(fā)連鎖更新操作, 但這種操作出現(xiàn)的幾率并不高。

    總結(jié)

    以上是生活随笔為你收集整理的Redis 数据结构 :SDS、链表、字典、跳表、整数集合、压缩列表的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。