Redis基本數據類型以及String(一)


前言:
     Redis也有自己的數據類型,包含string,list,hash,set,sorted set。下面就對每種數據類型原理以及操作做一個詳細的介紹。
     Redis是面向編程的語言,除了字符串,其他類型怎么表示呢?
     Redis中定義了一個對象的結構體:

 /*
 * Redis 對象
 */
typedef struct redisObject {

    // 類型
    unsigned type:4;        

    // 不使用(對齊位)
    unsigned notused:2;

    // 編碼方式
    unsigned encoding:4;

    // LRU 時間(相對於 server.lruclock)
    unsigned lru:22;

    // 引用計數
    int refcount;

    // 指向對象的值
    void *ptr;

} robj;

     type表示了該對象的對象類型,即上面五個中的一個。但為了提高存儲效率與程序執行效率,每種對象的底層數據結構實現都可能不止一種。encoding就表示了對象底層所使用的編碼。下面先介紹每種底層數據結構的實現,再介紹每種對象類型都用了什么底層結構並分析他們之間的關系。
     Redis對象底層數據結構共八種:
     編碼常量 編碼所對應的底層數據結構
     REDIS_ENCODING_INT(long 類型的整數)
     REDIS_ENCODING_EMBSTR embstr (編碼的簡單動態字符串)
     REDIS_ENCODING_RAW (簡單動態字符串)
     REDIS_ENCODING_HT (字典)
     REDIS_ENCODING_LINKEDLIST (雙端鏈表)
     REDIS_ENCODING_ZIPLIST (壓縮列表)
     REDIS_ENCODING_INTSET (整數集合)
     REDIS_ENCODING_SKIPLIST (跳躍表和字典)

string     

     Redis的字符串也是字符序列,一個Key對應一個Value,它是Redis里面最為基礎的數據存儲類型。字符串類型是二進制安全(字符串不是根據某種特殊的標志來解析的,無論輸入是什么,總能保證輸出是處理的原始輸入而不是根據某種特殊格式來處理)的,可以包含任何數據等等。

      一:實現原理

           在C語言中,字符串可以用'\0'結尾的char數組標示。這種簡單的字符串表示,在大多數情況下都能滿足要求,但是不能高效的計算length和append數據。所以Redis自己實現了SDS(簡單動態字符串)的抽象類型。

           字符串的編碼可以是int,raw或者embstr。如果一個字符串內容可轉為long,那么該字符串會被轉化為long類型,對象ptr指向該long,並且對象類型也用int類型表示。普通的字符串有兩種,embstr和raw。如果字符串對象的長度小於44字節,就用embstr對象。否則用的raw對象。代碼如下:                 

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));     o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;

    /* Set the LRU to the current lruclock (minutes resolution). */
    o->lru = LRU_CLOCK();
    return o;
}

/* Create a string object with encoding OBJ_ENCODING_RAW, that is a plain
 * string object where o->ptr points to a proper sds string. */
robj *createRawStringObject(const char *ptr, size_t len) {
    return createObject(OBJ_STRING,sdsnewlen(ptr,len));
}

/* Create a string object with encoding OBJ_ENCODING_EMBSTR, that is
 * an object where the sds string is actually an unmodifiable string
 * allocated in the same chunk as the object itself. */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    o->lru = LRU_CLOCK();

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

/* Create a string object with EMBSTR encoding if it is smaller than
 * REIDS_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
 * used.
 *
 * The current limit of 39 is chosen so that the biggest string object
 * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

        優點:

          (1)embstr的創建只需分配一次內存,而raw為兩次(一次為sds分配對象,另一次為redisObject分配對象,embstr省去了第一次)。

          (2)相對地,釋放內存的次數也由兩次變為一次。

          (3)embstr的redisObject和sds放在一起,更好地利用緩存帶來的優勢

        缺點:redis並未提供任何修改embstr的方式,即embstr是只讀的形式。對embstr的修改實際上是先轉換為raw再進行修改。

        sds數據結構定義如下:     

typedef char *sds;
        struct sdshdr {
             // 記錄buf數據中已使用的字節數目
              int len;

            // 記錄buf 剩余的字符長度
              int free;

           // 字符數據,用於存儲字符串  大小等於len+free+1,其中多余的1個字節是用來存儲'\0'的。
           char buf[];
       };

         最新版本的數據結構定義如下:從下面代碼可以看出,除了sdshdr5之外,其它4個header的結構都包含3個字段:len: 表示字符串的真正長度(不包含NULL結束符在內)。 alloc: 表示字符串的最大容量(不包含最后多余的那個字節)。 flags: 總是占用一個 字節。其中的最低3個bit用來表示header的類型。header的類型共有5種,在sds.h中有常量定義。

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

    

      

           其中SDS_HDR用來從sds字符串獲得header起始位置的指針,比如SDS_HDR(8, s1)表示s1的header指針,SDS_HDR(16, s2)表示s2的header指針。使用SDS_HDR之前我們必須先知道到底是哪一種header,這樣我們才知道SDS_HDR第1個參數應該傳什么。由sds字符指針獲得header類型的方法是,先向低地址方向偏移1個字節的位置,得到flags字段。比如,s1[-1]和s2[-1]分別獲得了s1和s2的flags的值。然后取flags的最低3個bit得到header的類型。由於s1[-1] == 0x01 == SDS_TYPE_8,因此s1的header類型是sdshdr8。 由於s2[-1] == 0x02 == SDS_TYPE_16,因此s2的header類型是sdshdr16。有了header指針,就能很快定位到它的len和alloc字段:s1的header中,len的值為0x06,表示字符串數據長度為6;alloc的值為0x80,表示字符數組最大容量為128。 s2的header中,len的值為0x0006,表示字符串數據長度為6;alloc的值為0x03E8,表示字符數組最大容量為1000。

        注: __attrubte__ ((packed)) 的作用就是告訴編譯器取消結構在編譯過程中的優化對齊,按照實際占用字節數進行分配。

         通過sds.c中的sdsReqType可以清楚的明白要選用的header,長度在0和2^5-1之間,選用SDS_TYPE_5類型的header。 長度在2^5和2^8-1之間,選用SDS_TYPE_8類型的header。 長度在2^8和2^16-1之間,選用SDS_TYPE_16類型的header。 長度在2^16和2^32-1之間,選用SDS_TYPE_32類型的header。 長度大於2^32的,選用SDS_TYPE_64類型的header。能表示的最大長度為2^64-1。

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}

     從數據結構定義,可以看出Redis的sds和C語言區別就是,sds是通過buf以及len來判斷字符串內容的,而不是通過'\0'來判斷。      

      (1)計算字符串的長度復雜度為0(1)   

      (2)操作字符串時,內存的分配復雜度最多為O(N)        

           C語言中對於一個N長的字符串底層是一個N+1長的字符數組(有一個字節存放空字符)。C字符串的長度和底層數組之間的長度存在着這樣的關系,因此當進行字符串的操作而導致字符串長度發生變化的時候,需要對內存進行重新分配。如果操作會增長字符串,那么在執行之前,就需要進行內存分配擴充底層數組的大小。如果是縮短字符串的操作,則需要釋放額外的內存。      如果字符串的操作不是很頻繁,每次修改都重新分配一下內存是可以接受的。但是Redis作為一個數據庫,其讀寫速度,數據修改頻率都被要求達到很高的效率。因此這種低效的方式並不適合Redis。     

        Redis采用兩種方式處理內存問題:     

        (1) 空間預分配    這種方式用於處理字符串長度增加的問題。如果對字符串的修改使得字符串的長度增加,API首先會判斷buf的空間大小是否滿足,如果滿足則直接操作,如果不滿足,則進行如下操作:如果對SDS進行修改之后的,SDS的長度(即len的值)小於1MB。程序將額外分配和len一樣大小的未使用空間。以上面的”hello” + ” world”的操作為例。在這個例子中”hello”的len是5(不考慮’\0′),修改之后的字符串”hello world”長度為11,那么新的SDS的buf的容量就是11*2+1。其中len和free都是11,多余的1字節用來存儲'\0'。 如果對SDS修改之后的長度大於1MB,那么程序會分配1MB的未使用空間(沒有分配等同大小的空間,避免資源浪費)。比如原數據是5MB,修改之后需要6MB的空間,進行修改的操作后,buf的實際空間應該是7MB,其中len為6MB,free為1MB。通過該策略實現了最多分配N次。       

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    //內存大小分配
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

      (2)惰性空間釋放   當執行字符串長度縮短的操作的時候,SDS並不直接重新分配多出來的字節,而是修改len和free的值(len相應減小,free相應增大,buf的空間大小不變化)。通過惰性空間釋放,可以很好的避免縮短字符串需要的內存重分配的情況。而且多余的空間也可以為將來可能有的字符串增長的操作做優化。      

     (3)防止內存溢出            

          char a[10] = "hello";               

          strcat(a, " world");               

          strcpy(a, "hello world");      

      上面的三句代碼,就是C語言的字符串拼接和復制的使用,但是明顯出現了緩沖區溢出的問題。字符數組a的長度是10,而”hello world”字符串的長度為11,則需要12個字節的空間來存儲(不要忘記了’\0’)。Redis的SDS是怎么處理字符串修改的這種情況。當使用SDS的API對字符串進行修改的時候,API內部第一步會檢測字符串的大小是否滿足。如果空間已經滿足要求,那么就像C語言一樣操作即可。如果不滿足,則拓展buf的空間,使得滿足操作的需求,之后再進行操作。每次操作之后,len和free的值會做相應的修改。    

       總結: SDS 具有以下優點       

       (1) 常數復雜度獲取字符串長度。       

       (2)杜絕緩沖區溢出。       

       (3)減少修改字符串長度時所需的內存重分配次數。       

       (4)二進制安全。       

       (5)兼容部分 C 字符串函數。   

   二、命令操作        

      1. SET SETEX PSETEX SETNX          

          SET key value [EX][PX][NX][XX]          

          EX second:設置鍵的過期時間,單位為秒。(SET key value EX second等同於SETEX key second value)。    

          PX millisecond:設置鍵的過期時間,單位為毫秒。(SET key value PX millisecond等同於PSETEX key millisecond value)。    

          NX:當建不存在時,才對鍵進行設置操作。(SET key value NX等同於SETNX key value)。    

          XX:只有鍵已經存在時,才對鍵進行設置操作。    

        (1)對不存在的鍵進行設置                

   127.0.0.1:6379> set key "value"          

                   OK          

   127.0.0.1:6379> get key                

                  "value"    

       (2)對已存在的鍵進行設置   

  

  127.0.0.1:6379> set key "newvalue"                

                  OK          
  127.0.0.1:6379> get key   

                 "newvalue"    

        (3)使用EX選項     

127.0.0.1:6379> set key-with-EX "hello" EX 10                

                  OK                

  127.0.0.1:6379> get key-with-EX                

                 "hello"                 

  127.0.0.1:6379> get key-with-EX                 

                 (nil)  

       (4)使用PX選項                    

  127.0.0.1:6379> set key-with-PX "hello" PX 10000                 

                  OK                 

  127.0.0.1:6379> get key-with-PX                

                 "hello"                 

  127.0.0.1:6379> get key-with-PX                 

                 (nil)  

          (5)使用NX|XX選項          

  127.0.0.1:6379> set key-with-NX "hello" NX                

                  OK                

  127.0.0.1:6379> set key-with-NX "hello" NX                

                 (nil)       

  127.0.0.1:6379> set key-with-XX "hello"                

                  OK                

  127.0.0.1:6379> del key-with-XX                

                 (integer) 1                

  127.0.0.1:6379> set key-with-XX "hello" XX       

                 (nil)    

      2. APPEND

          APPEND key value              

          如果key已存在並且是一個字符串,APPEND命令將value追加到key原來的位置。            

          如果key不存在,APPEND簡單的將給定的key設為value。    

   127.0.0.1:6379> APPEND test "value"           

                  (integer) 5            

   127.0.0.1:6379> get test            

                  "value"            

   127.0.0.1:6379> APPEND test "1"           

                  (integer) 6           

   127.0.0.1:6379> get test           

                  "value1"           

      3. GET      

          返回key所關聯的字符串值   如果 key 不存在那么返回特殊值 nil 。假如 key 儲存的值不是字符串類型,返回一個錯誤,因為 GET 只能用於處理字符串值。   

      4. INCR INCRBY INCRBYFLOAT DECR DECRBY            

          INCR將key中的數字值加1。如果KEY不存在,那么KEY的值初始化為0,然后執行INCR操作。如果類型錯誤,返回一個錯誤。    

          INCR key    

          INCRBY    將 key 所儲存的值加上增量 increment。    

          INCRBY key increment    

          INCRBYFLOAT    將key 中所儲存的值加上浮點數增量 increment,無論加法計算所得的浮點數的實際精度有多長, INCRBYFLOAT 的計算結果也最多只能表示小數點的后十七位    

          INCRBYFLOAT key increment    DECR DECRBY操作與其相反。    

      5. MSET MSETNX      

          MSET key value [key value ...]            

          如果某個給定 key 已經存在,那么 MSET 會用新值覆蓋原來的舊值,如果這不是你所希望的效果,請考慮使用 MSETNX 命令:它只會在所有給定 key 都不存在的情況下進行設置操作。MSET 是一個原子性(atomic)操作,所有給定 key 都會在同一時間內被設置,某些給定 key 被更新而另一些給定 key 沒有改變的情況,不可能發生。       

      6. GETRANGE      

         GETRANGE key start end    

         返回 key 中字符串值的子字符串,字符串的截取范圍由 start 和 end 兩個偏移量決定(包括 start 和 end 在內)。             負數偏移量表示從字符串最后開始計數, -1 表示最后一個字符, -2 表示倒數第二個,以此類推。 GETRANGE 通過保證子字符串的值域(range)不超過實際字符串的值域來處理超出范圍的值域請求。   

         注:GETRANGE不支持回繞操作    

         例: 

  127.0.0.1:6379> set test2 "aaaaaaa"            

                  OK            

  127.0.0.1:6379> getrange test2 -1 -5 //回繞操作            

                  ""            

  127.0.0.1:6379> getrange test2 -3 -1            

                 "aaa" 

 

      7. MGET       

          MGET key [key ...]             

          返回所有(一個或多個)給定 key 的值。如果給定的 key 里面,有某個 key 不存在,那么這個 key 返回特殊值 nil 。因此,該命令永不失敗     

      8. STRLEN       

          返回 key 所儲存的字符串值的長度。key不存在時返回0,。當 key 儲存的不是字符串值時,返回一個錯誤。     

      9. SETRANGE       

          SETRANGE key offset value             

          用 value 參數覆寫(overwrite)給定 key 所儲存的字符串值,從偏移量 offset 開始。不存在的 key 當作空白字符串處理。SETRANGE 命令會確保字符串足夠長以便將 value 設置在指定的偏移量上,如果給定 key 原來儲存的字符串長度比偏移量小(比如字符串只有 5 個字符長,但你設置的 offset 是 10 ),那么原字符和偏移量之間的空白將用零字節(zerobytes, "\x00" )來填充。              注:能使用的最大偏移量是 2^29-1(536870911) ,因為 Redis 字符串的大小被限制在 512 兆(megabytes)以內。如果你需要使用比這更大的空間,你可以使用多個 key 。     

      10. GETSET       

           GETSET key value             

           將給定 key 的值設為 value ,並返回 key 的舊值(old value)。當 key 存在但不是字符串類型時,返回一個錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM