前言:
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 存在但不是字符串類型時,返回一個錯誤。