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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis之数据结构底层实现

發布時間:2024/4/11 数据库 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis之数据结构底层实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

redis底層數據結構實現

Redis數據結構

String字符串

常用命令

SDS的定義

SDS的好處

應用場景

List列表

常用命令

壓縮列表ziplist

quicklist

應用場景

Hash哈希

常用命令

hashtable

應用場景

Set集合

常用命令

inset整型集合

應用場景

ZSet有序集合

存儲原理

skiplist

應用場景

參考鏈接


redis底層數據結構實現

redis是(REmote DIctionary Service)作為NoSQL數據庫,以key-value的字典方式來存儲數據,其中的value主要支持五種數據類型。

本文主要講解redis的五種常用數據類型(string、list、hash、set、zset)的底層數據結構實現。

Redis數據結構

Redis采用key-value的方式來存儲數據,每個鍵值對都會有一個dictEntry,里面有指向key,value的指針,還有指向下一個鍵值對的next指針

typedef struct dictEntry {void *key; /* key 關鍵字定義*/union {void *val; uint64_t u64; /* value 定義*/int64_t s64;double d;} v;struct dictEntry *next; /* 指向下一個鍵值對節點*/ } dictEntry;

這里的key 是字符串,使用了Redis自己定義的SDS數據結構來存儲,而value 是存儲在redisObject 中的。

typedef struct redisObject {unsigned type:4; /* 對象的類型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET*/unsigned encoding:4; /* 底層存儲的具體數據結構*/unsigned lru:LRU_BITS; /* 24 位,對象最后一次被訪問的時間,與內存淘汰機制有關*/int refcount; /* 引用計數。當其為0的時候,表示該對象已經不被任何對象引用,可以進行垃圾回收*/void *ptr; /* 指向對象實際的數據結構*/ } robj;

String字符串

redis中并沒有使用C語言的?字符串表示(以空字符結尾的字符數組),而是自己定義了一個SDS(Simple Dynamic String,簡單動態字符串)作為字符串的默認實現

常用命令

1SET key value
設值
2GET key
取值。
3MGET key1 [key2..]
獲取所有(一個或多個)給定 key 的值。
4SETEX key seconds value
設值,并將 key 的過期時間設為 seconds (以秒為單位) (原子性)。
5SETNX key value
只有在 key 不存在的時候才可以成功設置(可以根據這個特性來創建分布式鎖)
6MGET key1 [key2..]
獲取所有(一個或多個)給定 key 的值。
7MSET key value [key value ...]
同時設置一個或多個 key-value 對(批量操作,原子性)。
8INCR/DECR key
將 key 中儲存的數字值增一/減一。
9APPEND key value
向key對應的value值后追加新的value到其末尾

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

對于字符串,其內部的encoding有三種:? 根本原因還是為了減少內存消耗

  • int 存儲8字節的長整型(最大存儲2^63-1)
  • embstr? 代表embstr格式的SDS(Simple Dynamic String),字符串大小<44字節
  • raw? 存儲大于44字節的字符串
  • Embstrraw的區別? 區別在于分配內存的次數

    Embstr在使用的時候只需要分配一次內存空間(RedisObject和SDS是連續的),而raw需要分配兩次。如果字符串的長度增加導致需要重新分配內存空間,embstr類型的RedisObject和SDS都需要重新分配,因此 Redis中的embstr表現為只讀(對embstr進行修改就會轉化為raw編碼)

    ?

    SDS的定義

    redis中的SDS有各種結構,sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存儲不同的長度的字符串(節省內存空間),分別代表2^5=32byte, 2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB

    /* sds.h */ struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* 當前字符數組的長度*/uint8_t alloc; /*當前字符數組總共分配的內存大小*/unsigned char flags; /* 當前字符數組的屬性,用來標識是sdshdr8 還是sdshdr16 等*/char buf[]; /* 字符串真正的值,最后一個字符保存了空字符 '\0' */ };

    比方說一個字符串"Redis",給它分配了32個字節的空間,目前只保存了5個字符

    SDS的好處

    1.在聲明的時候提前預留了空間,并且會在內存不夠的時候進行擴容

    2.在SDS定義了字符串的長度len,獲取其長度的時間復雜度為O(1)

    3.通過事先分配空間(空間預分配)和惰性空間釋放,較少內存重新分配的次數,大大提高存儲效率

    4.以從開始的第len個字符表示字符串的結束,不用擔心存儲二進制數據的時候由于’\0’而導致無法完整獲取數據,是二進制安全的

    5.同樣以'\0'結尾是因為這樣就可以使用C語言中函數庫操作字符串的函數了

    應用場景

    緩存熱點數據,可以提升熱點數據的訪問速度

    在分布式下共享數據 eg.分布式session

    分布式鎖 (setnx)

    計數器:頁面訪問流量統計(incr)

    List列表

    用于存儲有序的字符串,可以從頭和尾添加或者獲取元素(Left/Right),列表里的元素可以重復,能夠充當隊列和棧的角色

    常用命令

    1BLPOP key1 [key2 ] timeout
    從左側移出并獲取列表的第一個元素, 如果列表沒有元素會阻塞直到超時或發現可彈出元素為止。
    2BRPOP key1 [key2 ] timeout
    從右側并獲取列表的最后一個元素, 如果列表沒有元素會阻塞直到超時或發現可彈出元素為止。
    3LPUSH key value1 [value2]
    將一個或多個值插入到列表頭部(左側)
    4RPUSH key value1 [value2]
    向列表尾部添加一個或多個值(右側)

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    先來說一下redis里的壓縮列表的數據結構

    壓縮列表ziplist

    壓縮列表(ziplist),顧名思義,在條件允許的情形下對保存的列表數據盡可能的進行壓縮,是Redis 為了節約內存而開發的, 一個經過特殊編碼的連續內存塊組成的雙向鏈表。它不像普通的鏈表存儲指向上一個鏈表節點和指向下一個鏈表節點的指針,而是存儲上一個節點長度和當前節點長度,通過犧牲部分讀寫性能,來換取高效的內存空間利用率。只適合用在字段個數少,字段值小的場景里面。

    typedef struct zlentry {unsigned int prevrawlensize; /* 上一個鏈表節點占用的長度*/unsigned int prevrawlen; /* 存儲上一個鏈表節點的長度數值所需要的字節數*/unsigned int lensize; /* 存儲當前鏈表節點長度數值所需要的字節數*/unsigned int len; /* 當前鏈表節點占用的長度*/unsigned int headersize; /* 當前鏈表節點的頭部大小(prevrawlensize + lensize),即非數據域 的大小*/ unsigned char encoding; /* 編碼方式*/unsigned char *p; /* 壓縮鏈表以字符串的形式保存,該指針指向當前節點起始位置*/ } zlentry;

    其存儲結構如下圖:

    早期版本里,redis的列表是通過ziplist或者linkedlist的結構實現,數據量較小的時候會使用ziplist來保存數據,較大的時候會使用linkedlist(雙向鏈表的結構)來存儲,類似于下圖,就不再贅述了

    quicklist

    3.2版本之后,統一用quicklist來存儲。quicklist存儲了一個雙向鏈表,每個節點都是一個ziplist。?

    typedef struct quicklist { quicklistNode *head; /* 指向雙向列表的表頭 */ quicklistNode *tail; /* 指向雙向列表的表尾 */ unsigned long count; /* 所有的 ziplist 中一共存了多少個元素 */ unsigned long len; /* 雙向鏈表的長度,node 的數量 */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* 壓縮深度,0:不壓縮; */} quicklist; typedef struct quicklistNode { struct quicklistNode *prev; /* 前一個節點 */ struct quicklistNode *next; /* 后一個節點 */ unsigned char *zl; /* 指向實際的 ziplist */ unsigned int sz; /* 當前 ziplist 占用多少字節 */ unsigned int count : 16; /* 當前 ziplist 中存儲了多少個元素,占 16bit(下同),最大 65536 個 */ unsigned int encoding : 2; /* 是否采用了 LZF 壓縮算法壓縮節點,1:RAW 2:LZF */ unsigned int container : 2; /* 2:ziplist,未來可能支持其他結構存儲 */ unsigned int recompress : 1; /* 當前 ziplist 是不是已經被解壓出來作臨時使用 */ unsigned int attempted_compress : 1; /* 測試用 */ unsigned int extra : 10; /* 預留給未來使用 */ } quicklistNode; quicklist結構圖

    ?

    應用場景

    消息隊列: List提供了兩個帶阻塞功能的pop操作:BLPOP/BRPOP,可以實現簡單的類似消息隊列的功能

    隊列:先進先出:rpush blpop
    棧:先進后出:rpush brpop
    ?

    Hash哈希

    存儲包含鍵值對的無序散列表,其value只能是字符串,不能嵌套其他類型

    常用命令

    1HDEL key field1 [field2]
    刪除一個或多個哈希表字段
    2HEXISTS key field
    查看哈希表中,是否存在指定的field
    3HGET key field
    獲取存儲在哈希表中指定field對應的value。
    4HGETALL key
    獲取在哈希表中指定 key 的所有字段和值
    5HKEYS key
    獲取key對應的哈希表中所有的字段
    6HMSET key field1 value1 [field2 value2 ]
    同時將多個 field-value (鍵值對)設置到哈希表 key 中。
    7HSET key field value
    將哈希表 key 中的字段 field 的值設為 value 。

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    :zipl

    前面說到redis本身就是一個K-V鍵值對的字典數據庫,對于Hash結構,相當于將Redis的Value也使用field-value的方式來進行存儲。其存儲方式有兩種:ziplist和hashtable

    當hash對象同時滿足以下兩個條件的時候,會使用ziplist編碼:
    1)所有的鍵值對的健和值的字符串長度都小于等于64byte;
    2)哈希對象保存的鍵值對數量小于512個。?

    壓縮列表在前面就介紹過了,這里就介紹下hashtable\

    hashtable

    哈希表的節點使用dictEntry來表示,每個?dictEntry?結構都保存著一個鍵值對:

    typedef struct dictEntry {void *key; /* key 關鍵字定義*/union {void *val; uint64_t u64; /* value 定義*/int64_t s64; double d;} v;struct dictEntry *next; /* 指向下一個鍵值對節點*/ } dictEntry;

    而dictEntry存儲在一個dictht里(一個hashtable),

    /*Thisisourhashtablestructure.Everydictionaryhastwoofthisaswe *implementincrementalrehashing,fortheoldtothenewtable.*/ typedef struct dictht{dictEntry **table;/* 哈希表數組 每一個元素是一個dictEntry*/ unsigned long size;/* 哈希表大小 */ unsigned long sizemask;/* 掩碼大小,用于計算索引值。總是等于 size-1*/ unsigned long used;/* 已有節點數 */} dictht;

    而上述哈希表又保存到了dict里

    typedef struct dict{ dictType *type;/* 字典類型 */ void *privdata;/* 私有數據 */ dictht ht[2];/* 一個字典有兩個哈希表 */ long rehashidx;/*rehash 索引 */ unsigned long iterators;/* 當前正在使用的迭代器數量 */ } dict;

    從外層到底層是這樣的一個包含關系

    dict-->dictht-->dictEntry

    在普通情形下,一個哈希的字典的存儲結構如下圖:

    其存儲的方式類似于hashMap,如果發生hash沖突,那么就會將對應下標的最后一個元素的next指針指向新的dictEntry

    這里定義了兩個hashtable,主要是為了在發生大量哈希碰撞的時候進行擴容使用

    一般情形下,dict里使用hashtable的時候,默認使用的是ht[0],ht[1]不會進行初始化和分配空間。哈希表使用鏈地址法來解決hash碰撞,如果碰撞劇烈,導致ht[0]的鏈很長,就會影響到redis的查詢速度。故hashtable的查詢性能取決于其table大小和保存的節點數量之間的比值。當上述比值較大的時候,也就是說hash碰撞發生比較劇烈的時候會對其進行擴容

    此時需滿足兩個條件:

    1)允許擴容 dict_can_resize=1

    2)table里保存的節點數/table的大小大于dict_force_resize_ratio

    擴容時,會對ht[1]進行初始化,并且分配空間,新的hashtable的大小為當前hashtable保存的節點數*2,然后將ht[0]里的dictEntry遷移到ht[1],重新計算哈希值和索引,存放到新的索引下。遷移完成后,將ht[1]設置為ht[0]表,然后把原來的ht[0]清空回收內存,將其設置為ht[1]以供下次rehash使用

    應用場景

    字符串數據結構可以做的事情,Hash也都能實現

    存儲對象類型的數據,以field為屬性,value為對應的屬性值,便于管理

    Set集合

    string類型的無序集合

    常用命令

    1SADD key member1 [member2]
    向集合添加一個或多個成員
    2SCARD key
    獲取集合的成員數
    3SDIFF key1 [key2]
    返回給定所有集合的差集
    4SDIFFSTORE destination key1 [key2]
    返回給定所有集合的差集并存儲在 destination 中
    5SINTER key1 [key2]
    返回給定所有集合的交集
    6SINTERSTORE destination key1 [key2]
    返回給定所有集合的交集并存儲在 destination 中
    7SISMEMBER key member
    判斷 member 元素是否是集合 key 的成員
    8SPOP key
    移除并返回集合中的一個隨機元素
    9SRANDMEMBER key [count]
    返回集合中一個或多個隨機數
    10SREM key member1 [member2]
    移除集合中一個或多個成員
    11SUNION key1 [key2]
    返回所有給定集合的并集
    12SUNIONSTORE destination key1 [key2]
    所有給定集合的并集存儲在 destination 集合中

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    Redis采用用intset或hashtable來存儲set。如果元素都是整數類型,使用inset存儲。
    如果不全是整數類型,就用hashtable(數組+鏈表的結構來存儲),目的還是為了節省存儲空間

    inset整型集合

    typedef struct intset {// 編碼方式 : INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64uint32_t encoding;// 集合包含的元素數量uint32_t length;// 保存元素的數組 不同的encoding,其數組的元素大小也不一樣int8_t contents[]; } intset;

    使用hashtable來存儲set的時候,dictEntry里的key對應于set里的成員,value為null

    應用場景

    抽獎 : 隨機獲取一個成員

    簽到 , 點贊,打卡

    商品標簽

    商品篩選 : 通過交集,差集,并集等做商品篩選

    ?

    ZSet有序集合


    有序的集合,每個元素都會有對應的score,根據score來排序;score相同時,按照key的ASCII碼排序。

    1ZADD key score1 member1 [score2 member2]
    向有序集合添加一個或多個成員,或者更新已存在成員的分數
    3ZCOUNT key min max
    計算score在指定區間的有序集合的成員數
    4ZINCRBY key increment member
    有序集合中對指定成員的分數加上增量 increment
    5ZRANGE key start stop [WITHSCORES]
    通過索引區間返回有序集合指定區間內的成員
    6ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
    通過分數返回有序集合指定區間內的成員
    7ZRANK key member
    返回有序集合中指定成員的索引
    8ZREM key member [member ...]
    移除有序集合中的一個或多個成員
    9ZREVRANGE key start stop [WITHSCORES]
    返回有序集中指定區間內的成員,通過索引,分數從高到低
    10ZREVRANGEBYSCORE key max min [WITHSCORES]
    返回有序集中指定分數區間內的成員,分數從高到低排序
    11ZREVRANK key member
    返回有序集合中指定成員的排名,有序集成員按分數值遞減(從大到小)排序
    12ZSCORE key member
    返回有序集中,成員的分數值

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    存儲原理

    有序集合底層采用ziplist或者skiplist的方式進行存儲

    當?元素數量小于128個且所有member的長度都小于64字節的時候會使用ziplist存儲有序集合,在壓縮列表內部,按照score遞增的順序來進行存儲,故而每次插入或者刪除的時候要移動之后的數據

    當大于這個閾值時,會使用跳躍表(skiplist)來存儲

    skiplist

    大家都知道,對于鏈表,插入和刪除的效率比較高,但是查詢的效率會很低,因為需要從head節點開始遍歷,直到找到對應的元素或者遍歷完整個鏈表,其時間復雜度時O(n).同理,在有序鏈表里插入數據的時候也需要先查詢一遍才可以確定插入的位置

    對于有序數組我們可以使用二分查找法來優化查詢的速度,對于有序鏈表,可以使用跳躍表

    假如我們每相鄰的兩個節點間增加一個指針,形成一個新的Level(實際情形不一定是相鄰2個節點形成一個level,但是Level越大,該層上的節點數就越少),讓其上的指針指向下一個節點。這樣新的Level也是一個鏈表,但它包含的節點個數只有原來的一半(實際一定比原來少,具體多少不一定)(圖中的8, 19, 41)。

    如下圖:

    當想新增一個節點數據的時候,會根據冪次定律 (power law,越大的數出現的概率越小) 隨機生成一個介于?1?和?32?之間的值作為level數組的大小, 這個大小就是層的“高度” (redis t_zset.c 中的zslRandomLevel方法)。

    當我們想查詢數據V的時候,可以先沿著這個新鏈表(最頂層Level)進行查找。當碰到比V大的節點或者下一個節為null時,下落到下一層進行查找(因為之后的節點只可能更大或者到頭),下落到較小的level節點之后,比較節點值和V的大小,如果V較大,則繼續向前查找,如果V較小,則 通過后退指針"后退"查找,不斷繼續這個過程,直到找到對應的節點,或者V位于level1相鄰兩節點之間。
    在查找過程中,由于新增加的層級包含更少的節點,故不再需要與鏈表中每個節點逐個進行比較才能找到對應的位置了,這就是跳躍表。
    Redis中skiplist的定義

    typedef struct zskiplist{ struct zskiplistNode *header,*tail;/* 指向跳躍表的頭結點和尾節點 */ unsigned long length; /* 跳躍表內所有的節點數 */ int level;/* 跳躍表內,層數最大的那個節點的層數 */ }zskiplist typedef struct zskiplistNode{ sds ele;/*zset 的元素 */ double score;/* 分值 */ struct zskiplistNode *backward;/* 后退指針 */ struct zskiplistLevel{ struct zskiplistNode *forward;/* 前進指針,對應 level 的下一個節點 */ unsigned long span;/* 從當前節點到下一個節點的跨度(跨越的節點數) */ }level[];/* 層 */ } zskiplistNode;

    隨機獲取層數的函數

    int zslRandomLevel(void){ int level=1; while((random()&0xFFFF)<(ZSKIPLIST_P*0xFFFF))level+=1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }

    應用場景

    排行榜 點擊數前幾的新聞等

    參考鏈接

    http://redisbook.com/

    ?

    ?

    總結

    以上是生活随笔為你收集整理的Redis之数据结构底层实现的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。