面試官:Redis數據結構與內部編碼,你知道多少?

介紹css

Redis是一個基於內存的數據庫,全部的數據都存儲在內存中,因此如何優化存儲,減小內存空間佔用對成本控制來講是很是重要的。精簡鍵名和鍵值是最直觀的減小內存佔用的方式,而Redis則是經過內部編碼規則來節省更多的內存空間。linux

Redis爲每種數據類型都提供了兩三種內部編碼方式,以散列類型爲例,散列類型是經過散列表實現的,這樣就能夠實現0(1)時間複雜度的查找、賦值操做,然而當鍵中元素不多的時候,0(1)的操做並不會比0(n)有明顯的性能提升,因此這種狀況下Redis會採用一種更爲緊湊但性能稍差(獲取元素的時間複雜度爲0(n))的內部編碼方式。內部編碼方式的選擇對於開發者來講是透明的,Redis會根據實際狀況自動調整。當鍵中元素變多時Redis會自動將該鍵的內部編碼方式轉換成散列表。redis


1、Redis DB數據結構算法

Redis中是有16個redisDB庫,默認是使用第一個。咱們先來看下redisDB的數據結構:數據庫

  • dict:字典swift

  • dictht:就是一個hashtable,以o(1)時間複雜度獲取size,used當前數組裏面用掉了多少空間數組

  • dictEntry:數組裏面的元素,**table指針指向數組,redis中全部的key都是存在dictEntry中緩存

  • *var:存儲key的值,也是一個指針,指向redisObject結構進行數據存儲,這個指針指向真實的數據存儲ruby

  • next:當key發生hash衝突時(好比都是數組0),經過next指針創建一個單向的鏈表解決hash衝突
    bash

robj字段介紹:

  • type:對外的數據類型,string,list,hash,set,zset等

  • encoding:內部編碼,raw,int,ziplist等,對內存利用率極致追求

  • LRU_BITS:內存淘汰策略

  • refcount:redis內存管理須要

  • *ptr:指向真實的數據存儲結構,ziplist等,最終指向數據編碼的對象


2、內部編碼方式

下面是Redis數據結構與內部編碼的關係:

查看一個鍵的內部編碼方式:

127.0.0.1:6379set foo barOK127.0.0.1:6379> object encoding foo"raw"

Redis的每一個鍵值都是使用一個redisObject結構體保存的,在redis.h中聲明的redisObj定義的以下:

typedef struct redisObject {   unsigned type:4/** 4 bit */  unsigned encoding:4; /** 4 bit */   unsigned lru:LRU_BITS; /** 24 bit */  int refcount;  /** 4 byte */  void * ptr;  /** 8 byte */}robj;

其中type字段表示的是鍵值的數據類型,值能夠是以下內容:

#define REDIS_STRING 0 #define REDIS_LIST 1 #define REDIS_SET 2 #define REDIS_ZSET 3 #define REDIS_HASH 4

encoding字段表示的就是Redis鍵值的內部編碼方式,取值能夠是:

#define REDIS_ENCODING_RAW 0 /** Raw representation */ #define REDIS_ENCODING_INT 1 /** ed as integer */ #define REDIS_ENCODING_EMBSTR 2 /** cpu cache line */ #define REDIS_ENCODING_HT 3 /** Encoded as hash table */ #define REDIS_ENCODING_ZIPMAP 4 /** Encoded as zipmap */ #define REDIS_ENCODING_QUICKEDLIST 5 /** Encoded as regular quicked list */ #define REDIS_ENCODING_ZIPLIST 6 /** Encoded as ziplist */ #define REDIS_ENCODING_INTSET 7 /** Encoded as intset */ #define REDIS_ENCODING_SKIPLIST 8 /** Encoded as skiplist */

各個數據類型可能採用的內部編碼方式以及相應的OBJECT ENCODING命令執行結果以下:

數據類型
內部編碼方式
OBJECT ENCODING命令結果
字符串類型 REDIS_ENCODING_RAW
"raw"
REDIS_ENCODING_INT "int"
REDIS_ENCODING_EMBSTR "embstr"
散列類型
REDIS_ENCODING_HT "hashtable"
REDIS_ENCODING_ZIPLIST "ziplist"
列表類型
REDIS_ENCODING_QUICKEDLIST "quickedlist"
REDIS_ENCODING_ZIPLIST "ziplist"
集合類型
REDIS_ENCODING_HT "hashtable"
REDIS_ENCODING_INTSET "intset"
有序集合類型
REDIS_ENCODING_SKIPLIST "skiplist"
REDIS_ENCODING_ZIPLIST "ziplist"

下面針對每種數據類型分別介紹其內部編碼規則及優化方式。


2、字符串類型優化方式

127.0.0.1:6379> set a_string aOK127.0.0.1:6379> set a_int 1OK127.0.0.1:6379> set a_long_string aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaOK127.0.0.1:6379> type a_stringstring127.0.0.1:6379> type a_intstring127.0.0.1:6379> type a_long_stringstring127.0.0.1:6379> object encoding a_string"embstr"127.0.0.1:6379> object encoding a_int"int"127.0.0.1:6379> object encoding a_long_string"raw"127.0.0.1:6379>

Redis使用一個sdshdr類型的變量來存儲字符串,而redisObject的ptr字段指向的是該變量的地址。sdshdr的定義以下:

// redis3.2以前版本struct sdshdr {   int len; /* 表示的是字符串的長度 */ int free; /* 表示buf中的剩餘空間 */ char buf[]; /* 字符串的內容 */};
// redis3.2以後版本typedef char *sds;
struct sdshdr5 {   unsigned char flags; /*3 lsb of type, and 5 msb of string length*/ char buf[]; };
struct 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 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 sdshdr32 { 
... };
struct sdshdr64 { 
...};

redis根據字符串大小選擇合適的數據存儲結構:

#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
static inline char sdsReqType(size_t_string_size) {  if (string_size < 32)    return SDS_TYPE_5;  if (string_size < 0xff) // 2^8 - 1    return SDS_TYPE_8;  if (string_size < 0xffff// 2^16 - 1    return SDS_TYPE_16;  if (string_size < 0xffffffff// 2^32 - 1    return SDS_TYPE_32;  return SDS_TYPE_64;}

3.2以前:

  • 能夠動態擴容的數據結構

  • free表明可用空間,分配空間能夠分配稍微大一點的空間,下次進行數據修改的時候就不用每次都分配內存,提高總體性能

3.2以後:

  • 變得豐富多樣

  • 節省存儲空間,好比就存一個字符串【i】,使用sdshdr數據結構須要len+free=4+4=8字節

  • sdshdr5只會使用一個字節flags,表示數據特性。以下:

  • flags+buf,一個flags字節的低3位表示類型type,len表示數據長度(2^5-1 < 32)

  • buf表示真實數據

缺點是沒法動態擴容,沒有free字段,因此redis也沒有使用sdshdr5這種數據結構,never used,因此一般狀況下,使用下面sdshdr8:

  • type定義的0,1,2表示type佔用的bit位,能夠減小空間佔用


接下來咱們分別介紹下string類型的raw、int和embstr。

embstr

好比當執行SET key foobar時,在64位linux系統下,存儲鍵值須要佔用的空間是 sizeof(redisObject)+sizeof(sdshdr8)+strlen("foobar")=16字節+4字節+6字節=26字節存儲結構以下:

在linux操做系統,cpu緩存行大小佔64byte,而redisObject和sdshdr8正好佔用20個字節,因此當業務數據大小在64-20=44字節以內的話,能夠利用cpu緩存行特性:linux分配內存的時候,就會挨着redisObject進行分配,開闢一塊連續的空間存儲,利用cpu的緩存行一次讀取到數據,減小內存IO,這樣數據整合就在cpu緩存行範圍內,這樣在進行數據讀取的時候,cpu第一次尋址到var,經過var找到redisObject,經過redisObject咱們能夠直接拿到值,而不用經過指針再一次尋址去拿數據,這就是embstr作的事情。


raw類型是和redisObject不在一塊連續的內存空間,以下:

咱們能夠對embstr進行驗證:

127.0.0.1:6379> set a_string_short aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-OK127.0.0.1:6379> STRLEN a_string_short(integer) 44127.0.0.1:6379> object encoding a_string_short"embstr"127.0.0.1:6379> set b_string_short aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aOK127.0.0.1:6379> STRLEN b_string_short(integer) 45127.0.0.1:6379> object encoding b_string_short"raw"127.0.0.1:6379> 
  • 當字符串長度大於44時就變成了raw

使用append追加字符串方式說明:

127.0.0.1:6379> set a aOK127.0.0.1:6379> object encoding a"embstr"127.0.0.1:6379> APPEND a b(integer) 2127.0.0.1:6379> object encoding a"raw"127.0.0.1:6379
  • 使用append等命令會修改redis內部編碼,就不適用cpu緩存行優化的方式了


int

當鍵值內容能夠用一個64位有符號整數表示時,Redis會將鍵值轉換成long類型來存儲。如SET key 123456,實際佔用的空間是sizeof(redisObject)=16字節,比存儲"foobar"節省了 一半的存儲空間,以下所示:

redisObject中的refcount字段存儲的是該鍵值被引用數量,即一個鍵值能夠被多個鍵引用。Redis啓動後會預先創建10000個分別存儲從0到9999這些數字的redisObject類型變量做爲共享 對象,若是要設置的字符串鍵值在這10000個數字內(如SET key1 123)則能夠直接引用共享對象而不用再創建一個redisObject了,也就是說存儲鍵值佔用的空間是0字節,以下所示:

因而可知,使用字符串類型鍵存儲對象ID這種小數字是很是節省存儲空間的,Redis只需存儲鍵名和一個對共享對象的引用便可。 雖然整形底層存儲encoding是int類型,可是在獲取長度計算時會轉換爲字符串計算長度。


注意:當經過配置文件參數maxmemory設置了Redis可用的最大空間大小時,Redis不會使用共享對象,由於對於每個鍵值都須要使用一個redisObject來記錄其LRU信息。


字符串擴容的原理:

  • 當字符串大小小於1M時,每次擴容一倍

  • 大於1M時,每次增長1M,好比如今5M,擴容後就是6M


3、散列類型優化方式

散列類型的內部編碼方式多是REDIS_ENCODING_HT或REDIS_ENCODING_ZIPLIST。當數據量比較小或者單個元素比較小時,底層用ziplist存儲。能夠在配置文件中能夠定義使用REDIS_ENCODING_ZIPLIST方式編碼散列類型的時機:

hash-max-ziplist-entries 512 hash-max-ziplist-value 64

當散列類型鍵的字段個數少於hash-max-ziplist-entries參數值且每一個字段名和字段值的長度都小於hash-max-ziplist-value參數值(單位爲字節)時,Redis就會使用REDIS_ ENCODING_ZIPLIST來存儲該鍵,不然就會使用REDIS_ENCODING_HT。轉換過程是透明的,每當鍵值變動後Redis都會自動判斷是否知足條件來完成轉換。以下演示:

127.0.0.1:6379> hset user name duan age 27 f1 v1 f2 v2 f3 v3(integer) 5127.0.0.1:6379> HGETALL user 1) "name" 2) "duan" 3) "age" 4) "27" 5) "f1" 6) "v1" 7) "f2" 8) "v2" 9) "f3"10) "v3"127.0.0.1:6379> hset user f4 vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv(integer) 1127.0.0.1:6379> HGETALL user 1) "f3" 2) "v3" 3) "name" 4) "duan" 5) "f4" 6) "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" 7) "f1" 8) "v1" 9) "f2"10) "v2"11) "age"12) "27"127.0.0.1:6379> 

超過64個字節變成hash,hash是無序的。


REDIS_ENCODING_HT編碼即散列表,能夠實現O(1)時間複雜度的賦值取值等操做,其字段和字段值都是使用redisObject存儲的,因此前面講到的字符串類型鍵值的優化方法一樣適用於散列類型鍵的字段和字段值。


注意:Redis的鍵值對存儲也是經過散列表實現的,與REDIS_ENCODING_HT編碼方式相似,但鍵名並不是使用redisObject存儲,因此鍵名"123456"並不會比"abcdef"佔用更少的空間。之因此不對鍵名進行優化是由於絕大多數狀況下鍵名都不會是純數字。

Redis支持多數據庫,每一個數據庫中的數據都是經過結構體redisDb存儲的。redisDb的定義以下:

typedef struct redisDb { dict * dict; /** The keyspace for this DB */ dict * expires; /** Timeout of keys with a timeout set */ dict * blocking_keys; /** Keys with clients waiting for data (BLPOP) */ dict * ready_keys; /** Blocked keys that received a PUSH */ dict * watched_keys; /** WATCHED keys for MULTI/EXEC CAS */ int id; } redisDb;
  • dict類型就是散列表結構

  • expires存儲的是數據的過時時間

當Redis啓動時會根據配置文件中databases參數指定的數量建立若干個redisDb類型變量存儲不一樣數據庫中的數據。


REDIS_ENCODING_ZIPLIST編碼類型是一種緊湊的編碼格式,它犧牲了部分讀取性能以換取極高的空間利用率,適合在元素較少時使用。該編碼類型一樣還在列表類型和有序集合類型中使用。REDIS_ENCODING_ZIPLIST編碼結構以下所示:

  • zlbytes是uint32_t類型, 表示整個結構佔用的空間

  • zltail也是uint32_t類型,表示到最後一個元素的偏移,記錄zltail使得程序能夠直接定位到尾部元素而無需遍歷整個結構,執行從尾部彈出(對列表類型而言)等操做時速度更快

  • zllen是uint16_t類型,存儲的是元素的數量

  • zlend是一個單字節標識,標記結構的末尾,值永遠是255


散列類型的ziplist數據結構以下圖所示:

在REDIS_ENCODING_ZIPLIST中每一個元素由4個部分組成:

  • 第一個部分用來存儲前一個元素的大小以實現倒序查找,當前一個元素的大小小於254字節時第一個部分佔用1個字節,不然會佔用5個字節

  • 第2、三個部分分別是元素的編碼類型和元素的大小,當元素的大小小於或等於63個字 節時,元素的編碼類型是ZIP_STR_06B(即0<<6),同時第三個部分用6個二進制位來記錄元素的長度,因此第2、三個部分總佔用空間是1字節。當元素的大小大於63且小於或等於16383字節時,第2、三個部分總佔用空間是2字節。當元素的大小大於16383字節時,第2、三個部 分總佔用空間是5字節

  • 第四個部分是元素的實際內容,若是元素能夠轉換成數字的話Redis會使用相應的數字類型來存儲以節省空間,並用第2、三個部分來表示數字的類型(int16_t、int32_t等)

使用REDIS_ENCODING_ZIPLIST編碼存儲散列類型時元素的排列方式是:元素1存儲字段1,元素2存儲字段值2,依次類推,以下所示:

例如,當執行命令HSET hkey foo bar命令後,hkey鍵值的內存結構以下所示:

下次須要執行HSET hkey foo anothervalue時Redis須要從頭開始找到值爲foo的元素(查找 時每次都會跳過一個元素以保證只查找字段名),找到後刪除其下一個元素,並將新值 anothervalue插入。刪除和插入都須要移動後面的內存數據,並且查找操做也須要遍歷才能完 成,可想而知當散列鍵中數據多時性能將很低,因此不宜將hash-max-ziplist-entries和hash-max- ziplist-value兩個參數設置得很大。


4、列表類型優化方式

列表類型內部編碼方式是REDIS_ENCODING_QUICKLIST或REDIS ENCODINGZIPLIST

127.0.0.1:6379> lpush queue-task a b c(integer) 3127.0.0.1:6379> type queue-tasklist127.0.0.1:6379> object encoding queue-task"quicklist"127.0.0.1:6379

一樣在配置文件中能夠設置每一個ziplist的最大容量和quickList的數據壓縮範圍,提高數據存取效率。

list-max-ziplist-size -2list-compress-depth 0
  • 0默認不壓縮

  • list不關注中間數據,1表示不壓縮頭尾節點,壓縮中間數據

  • 2表示頭尾節點和頭尾相鄰的一個節點不壓縮,壓縮初次以外中間的


注意:列表類型實現阻塞隊列使用的是redisDb結構中的字段blocking_keys,維護的是key與客戶端的關係,不會阻塞redis進程。以下:

typedef struct redisDb {  dict *dict;  ... dict *blocking_keys;  ...}redisDb


ZIPLIST數據結構

ziplist數據結構說明:

  • zlbytes:32bit表示ziplist佔用的字節總數

  • zltail:32bit表示ziplist表中最後一項entry在ziplist中的偏移字節數。經過zltail咱們能夠很方便地找到最後一項,從而能夠在ziplist尾端快速地執行push或pop操做

  • zlen:16bit表示ziplist中數據項entry的個數

  • entry:表示真正存放數據的數據項,長度不定

  • zlend:ziplist最後一個字節,是一個結束標記,值固定等於255

  • prerawlen:前一個entry的數據長度

  • len:entry中數據的長度

  • data:真實數據存儲

根據len字段的第一個字節分的9種狀況:

  • 00xxxxxx:len字段前2個高位 bit爲0,剩餘的6個bit用來表示長度,即最大長度能夠到2^6 - 1

  • 01xxxxxx xxxxxxxx:len字段的前2個高位是01,則len字段佔2個byte,共有14個bit表示,數據長度最多2^14 - 1

  • 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx:len字段前2個高位bit是10,則len字段佔5個byte,共有32個bit表示,數據長度最多2^32 - 1,第一個字節的剩餘6個bit捨棄不用

  • 11000000:len字段前2個高位bit是11,值爲OXC0,則len字段佔1個byte,後面的data爲2字節的int16_t類型

  • 11010000:len字段前4個高位bit是1101,值爲OXD0,則len字段佔1個byte,後面的data爲4字節的int32_t類型

  • 11100000:len字段前4個高位bit是1110,值爲OXE0,則len字段佔1個byte,後面的data爲8字節的int64_t類型

  • 11110000:len字段前4個高位bit是1111,值爲OXF0,則len字段佔1個byte,後面的data爲3字節的整數

  • 11111110:len字段前7個高位bit是1111111,值爲OXFE,則len字段佔1個byte,後面的data爲1字節的整數

  • 1111xxxx:len字段前4個字節是1111,後4個bit的範圍是(0001-1101),這時xxxx從1到13,一共13個值,這時就用這13個值來表示data的數據,真正的數值大小爲對應的bit位數值-1,表明真實的業務數據


ziplist是很是緊湊的一種數據類型,爲了節省內存空間。而很是緊湊的數據結構的缺點是:

  • 空間必須是連續的

  • 數據量很是大的時候往裏面加元素,數據遷移很麻煩

  • 頻繁的內存分配與釋放是不划算的,因此redis針對這個問題進行了優化,quicklist


QUICKLIST數據結構

quicklist的優化是後續有數據修改,都是在一個小的ziplist中。


5、集合類型優化方式

集合類型的內部編碼方式多是REDIS_ENCODING_HT或REDIS_ENCODING_INTSET。

127.0.0.1:6379> sadd aset a b c d e f(integer) 6127.0.0.1:6379> sadd bset 1 2 3 4 5 6(integer) 6127.0.0.1:6379> object encoding aset"hashtable"127.0.0.1:6379> object encoding bset"intset"127.0.0.1:6379> sadd bset a(integer) 1127.0.0.1:6379> SMEMBERS bset1) "a"2) "5"3) "3"4) "1"5) "6"6) "4"7) "2"127.0.0.1:6379

當集合中的全部元素都是整數且元素的個數小於配置文件中的set-max-intset-entries參數指定值(默認是512)時Redis會使用REDIS_ENCODING_INTSET編碼存儲該集合,不然會使用 REDIS_ENCODING_HT來存儲。 REDIS_ENCODING_INTSET編碼存儲結構體intset的定義是:

typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;

其中contents存儲的就是集合中的元素值,根據encoding的不一樣,每一個元素佔用的字節大小 不一樣。默認的encoding是INTSET_ENC_INT16(即2個字節),當新增長的整數元素沒法使用2個字節表示時,Redis會將該集合的encoding升級爲INTSET_ENC_INT32(即4個字節)並調整以前全部元素的位置和長度,一樣集合的encoding還可升級爲INTSET_ENC_INT64(即8個字節)。 而且contents[]內存儲的整數元素是順序存儲的。


REDIS_ENCODING_INTSET編碼以有序的方式存儲元素(因此使用SMEMBERS命令得到的結果是有序的),使得可使用二分算法查找元素。可是不管是添加仍是刪除元素,Redis都須要調整後面元素的內存位置,因此當集合中的元素太多時性能較差。當新增長的元素不是整數或集合中的元素數量超過了set-max-intset-entries參數指定值時,Redis會自動將該集合的存儲結構轉換成REDIS_ENCODING_HT。


注意 當集合的存儲結構轉換成REDIS_ENCODING_HT後,即便將集合中的全部非整數元素刪除,Redis也不會自動將存儲結構轉換回REDIS_ENCODING_INTSET。由於若是要支持自動迴轉,就意味着Redis在每次刪除元素時都須要遍歷集合中的鍵來判斷是否能夠轉換回原來的編碼,這會使得刪除元素變成了時間複雜度爲0(n)的操做。


6、有序集合類型優化方式

有序集合類型編碼方式多是REDIS_ENCODING_SKIPLIST或REDIS_ENCODING_ZIPLIST

當數據比較少時採用ziplist編碼結構存儲,在配置文件中能夠定使用REDIS_ENCODING_ZIPLIST編碼機:

 zset-max-ziplist-entries 128  zset-max-ziplist-value 64 

有序集合的ziplist數據結構以下圖:

當數據大小超過128字節,使用跳錶存儲,單個元素大小超多64個字節也是跳錶結構。有序集合的跳錶結構以下圖:

  • *forward:前進指針

  • span:跨越元素,好比rank操做就是經過span跨越元素來計算的

  • 頭結點不存儲數據,起到索引的做用,中間和尾結點存儲數據

  • L2找到了120,若是找150,降低一層,找到了200,則數據就在150就在120~200之間


具體規則和散列型及列表型一編碼方式是REDIS_ENCODING_SKIPLISTRedis使用散列表和跳列表(skiplist)兩種數據構來存有序集合鍵值,其中散列表用來存元素與元素分數的映射關係以實現0(1)時間度的ZSCORE等命令。列表用來存元素的分數及其到元素的映射以實現排序的功能。


Redis列表的實現進行了幾點修改,其中包括允列表中的元素(即分數)相同,躍鏈表每一個點增長了指向前一個元素的指實現倒序找。 採用此種編碼方式,元素是使用redisObject的,因此可使用字符串鍵值化方式化元素,而元素的分數是使用double型存的。 使用REDIS_ENCODING_ZIPLIST編碼時有序集合存的方式按照"元素1,元素1分數,元素2,元素2的分數"這樣序排列,而且分數是有序的。

本文分享自微信公衆號 - 碼農沉思錄(code-thinker)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索