Redis學習之Redis資料結構詳解(RedisObject、SDS)
redis是一個key-value儲存系統。和Memcached類似,它支援儲存的value型別相對更多,包括string(字串)、list(連結串列)、set(集合)、zset(sorted set --有序集合)和hash(雜湊型別)
redis字串:在redis-Client中執行以下命令:
SET USER_NAME zhangsan
會建立一個key為USER_NAME,value為zhangsan 的鍵值對。那麼,那麼這個字串的值 "zhangsan" 在資料庫中是以哪種資料結構儲存的呢?
RedisObject物件
redis並沒有直接只用string list set等來直接實現鍵值對資料庫,而是根據這些資料結構建立了一個物件系統,這個系統包含了redis中五種資料結構。並且redis物件中記錄最後一次訪問時間,伺服器會根據這個時間刪除物件,從而釋放記憶體(如果啟用了maxmemory功能的情況下).
redisObject資料結構分析:
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; void *ptr; } robj;
type:佔用4個bit位,記錄了物件的型別REDIS_STRING 0,REDIS_LIST 1,REDIS_SET 2,REDIS_ZSET 3,REDIS_HASH 4
ptr:該指標執行物件底層實現資料結構,這些資料結構物件由encoding屬性決定。
encoding屬性:記錄了物件所使用的編碼,也就是說物件使用了什麼資料結構作為物件底層實現。
可以這樣理解:type記錄的資料結構是針對redis使用者來說的。encoding記錄的資料結構是redis底層的具體實現。並且每種型別的物件redis至少兩種不同的編碼,也就是兩種不同的實現方式。使用 OBJECT ENCODING命令來檢視資料庫鍵的值物件編碼。
可以看到“zhangsan”的編碼是embstr.
字串物件
字串物件的編碼可以是int ,raw 或者embstr.如果一個字串物件儲存的是整數值,並且這個整數值可以用long型別標識,那麼字串物件將以整數值儲存在ptr屬性裡面,並將字串物件的編碼設定為INT。
如果對"zhang_san"字串進行修改,型別會變成raw。
為什麼對字串進行更改時,會變成raw型別呢,emptr型別與raw型別有什麼區別呢,接下類針對這兩個具體型別進行詳細描述。
embstr編碼是為專門儲存短字串的一種編碼方式,這種編碼和raw編碼一樣,都使用redisObject結構和sdshdr結構來標識字串物件,但raw編碼會呼叫兩次記憶體分配函式來建立redisObject結構和sdshdr結構,而embstr編碼則通過呼叫一次記憶體分配函式來分配一塊連續的空間,空間中一次包含redisObject和sdshdr兩個結構。
SDS資料結構
在c語言中,傳統的字串以空字元結尾的字元陣列來表示,redis並沒有直接使用這種傳統的字串標識,而是自己構建了一種名為(simple dynamid string ,SDS)的抽象型別,並將SDS用作Redis預設字串表示。在redis中c字串只會用在一些無需對字串值修改的地方。
typedef char *sds; struct sdshdr { int len; int free; char buf[]; };
其中len表示buf中已佔用空間的長度,free中表示buf中可剩餘空間的長度,buf是資料空間,如下圖所示。
free屬性值為0,表示這個SDS沒有分配任何未使用空間,len為5表示這個SDS儲存了一個五個位元組的字串,buf為一個char型別的陣列,陣列的前五個位元組分別儲存了'H','E','L','L','O'五個字元,最後一個位元組則儲存了空字串\0.
SDS字串與C字串的區別
- C語言使用長度為N+1的字元陣列來表示長度為N的字串,並且字元陣列的最後一個元素總是空字元'\0'.(java也是用的char[])
- 獲取字串的長度,對c語言字串來說,c字串並不記錄自身的長度,獲取一個c字串的長度必須遍歷整個字串,這個操作的複雜度為O(n)的。而redis的STRLEN命令的複雜度僅為O(1)。
- 減少記憶體分配的次數,c字串並不記錄自身長度,因此每次增長或者縮短一個字串時,都需要對記憶體進行一次記憶體重新分配的操作。對於redis中sds來說會進行空間預分配:(可以參考JAVA 中 ArrayList這種資料結構的擴容,他們是類似的。ArrayList沒記錯的話是每次擴1.5倍)。
sds sdsMakeRoomFor(sds s, size_t addlen) { struct sdshdr *sh, *newsh; // 獲取sds目前空閒的長度 size_t free = sdsavail(s); size_t len, newlen; // 空間足夠,則直接返回,不重新分配 if (free >= addlen) return s; // 獲取 s 目前已佔用空間的長度 len = sdslen(s); sh = (void*) (s-(sizeof(struct sdshdr))); // s 最少需要的長度 newlen = (len+addlen); // 根據新長度,為 s 分配新空間所需的大小 if (newlen < SDS_MAX_PREALLOC) // 如果新長度小於 SDS_MAX_PREALLOC // 那麼為它分配兩倍於所需長度的空間 newlen *= 2; else // 否則,分配長度為目前長度加上 SDS_MAX_PREALLOC newlen += SDS_MAX_PREALLOC; // T = O(N) newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); // 記憶體不足,分配失敗,返回 if (newsh == NULL) return NULL; // 更新 sds 的空餘長度 newsh->free = newlen - len; // 返回 sds return newsh->buf; }
- 同時sds是惰性空間釋放,當sds字串縮短操作時不會立即free()空間,而是使用free屬性將這些位元組的數量記錄起來,並等待將來使用。
- C字串中的字元必須符合某種編碼,比如(ASCII),並且出了字串末尾之外,字串裡不能包含空字元,否則會認為已經到結尾,因此c字串只能儲存文字資料,而不能儲存圖片,音訊,視訊等二進位制資料。
- 同時c字串提供的api是不安全的,比如在修改內容時忘記分配空間。
總結:
C字串 | SDS |
獲取字串長度複雜度為O(n) | 獲取字串長度複雜度為O(1) |
API是不安全的,可能造成緩衝區溢位 | API是安全的,可能造成緩衝區溢位 |
修改N次字串必然進行N次記憶體分配 | 修改N次字串最多進行N次記憶體分配 |
只能儲存文字資料 | 可以儲存文字資料和二進位制資料 |
對SET key HELLO 命令執行後,內部儲存結構示意圖。