對於Redis的使用者來講, Redis做爲Key-Value型的內存數據庫, 其Value有多種類型.node
這些Value的類型, 只是"Redis的用戶認爲的, Value存儲數據的方式". 而在具體實現上, 各個Type的Value到底如何存儲, 這對於Redis的使用者來講是不公開的.redis
舉個粟子: 使用下面的命令建立一個Key-Value算法
$ SET "Hello" "World"
對於Redis的使用者來講, Hello
這個Key, 對應的Value是String類型, 其值爲五個ASCII字符組成的二進制數據. 但具體在底層實現上, 這五個字節是如何存儲的, 是不對用戶公開的. 即, Value的Type, 只是表象, 具體數據在內存中以何種數據結構存放, 這對於用戶來講是沒必要要了解的.數據庫
Redis對使用者暴露了五種Value Type, 其底層實現的數據結構有8種, 分別是:數組
而銜接"底層數據結構"與"Value Type"的橋樑的, 則是Redis實現的另一種數據結構: redisObject
. Redis中的Key與Value在表層都是一個redisObject
實例, 故該結構有所謂的"類型", 便是ValueType
. 對於每一種Value Type
類型的redisObject
, 其底層至少支持兩種不一樣的底層數據結構來實現. 以應對在不一樣的應用場景中, Redis的運行效率, 或內存佔用.安全
這是一種用於存儲二進制數據的一種結構, 具備動態擴容的特色. 其實現位於src/sds.h
與src/sds.c
中, 其關鍵定義以下:服務器
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[]; };
SDS的整體概覽以下圖:數據結構
其中sdshdr
是頭部, buf
是真實存儲用戶數據的地方. 另外注意, 從命名上能看出來, 這個數據結構除了能存儲二進制數據, 顯然是用於設計做爲字符串使用的, 因此在buf
中, 用戶數據後總跟着一個\0
. 即圖中 "數據" + "\0" 是爲所謂的buf
app
SDS有五種不一樣的頭部. 其中sdshdr5
實際並未使用到. 因此實際上有四種不一樣的頭部, 分別以下:dom
len
分別以uint8
, uint16
, uint32
, uint64
表示用戶數據的長度(不包括末尾的\0
)alloc
分別以uint8
, uint16
, uint32
, uint64
表示整個SDS, 除過頭部與末尾的\0
, 剩餘的字節數.flag
始終爲一字節, 以低三位標示着頭部的類型, 高5位未使用.當在程序中持有一個SDS實例時, 直接持有的是數據區的頭指針, 這樣作的用意是: 經過這個指針, 向前偏一個字節, 就能取到flag
, 經過判斷flag低三位的值, 能迅速判斷: 頭部的類型, 已用字節數, 總字節數, 剩餘字節數. 這也是爲何sds
類型便是char *
指針類型別名的緣由.
建立一個SDS實例有三個接口, 分別是:
// 建立一個不含數據的sds: // 頭部 3字節 sdshdr8 // 數據區 0字節 // 末尾 \0 佔一字節 sds sdsempty(void); // 帶數據建立一個sds: // 頭部 按initlen的值, 選擇最小的頭部類型 // 數據區 從入參指針init處開始, 拷貝initlen個字節 // 末尾 \0 佔一字節 sds sdsnewlen(const void *init, size_t initlen); // 帶數據建立一個sds: // 頭部 按strlen(init)的值, 選擇最小的頭部類型 // 數據區 入參指向的字符串中的全部字符, 不包括末尾 \0 // 末尾 \0 佔一字節 sds sdsnew(const char *init);
sdsnewlen
用於帶二進制數據建立sds實例, sdsnew
用於帶字符串建立sds實例. 接口返回的sds能夠直接傳入libc中的字符串輸出函數中進行操做, 因爲不管其中存儲的是用戶的二進制數據, 仍是字符串, 其末尾都帶一個\0, 因此至少調用libc中的字符串輸出函數是安全的.在對SDS中的數據進行修改時, 若剩餘空間不足, 會調用sdsMakeRoomFor
函數用於擴容空間, 這是一個很低級的API, 一般狀況下不該當由SDS的使用者直接調用. 其實現中核心的幾行以下:
sds sdsMakeRoomFor(sds s, size_t addlen) { ... /* 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; ... }
能夠看到, 在擴充空間時
addlen
可用SDS_MAC_PREALLOC
時, 申請空間再翻一倍. 若整體空間已經超過了閾值, 則步進增加SDS_MAC_PREALLOC
. 這個閾值的默認值爲 1024 * 1024
SDS也提供了接口用於移除全部未使用的內存空間. sdsRemoveFreeSpace
, 該接口沒有間接的被任何SDS其它接口調用, 即默認狀況下, SDS不會自動回收預留空間. 在SDS的使用者須要節省內存時, 由使用者自行調用:
sds sdsRemoveFreeSpace(sds s);
總結:
sdsMakeRoomFor
, 每一次擴充空間, 都會預留大量的空間. 這樣作的考量是: 若是一個SDS實例中的數據被變動了, 那麼頗有可能會在後續發生屢次變動.這是普通的鏈表實現, 鏈表結點不直接持有數據, 而是經過void *
指針來間接的指向數據. 其實現位於 src/adlist.h
與src/adlist.c
中, 關鍵定義以下:
typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; typedef struct listIter { listNode *next; int direction; } listIter; typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned long len; } list;
其內存佈局以下圖所示:
這是一個平平無奇的鏈表的實現. list
在Redis除了做爲一些Value Type的底層實現外, 還普遍用於Redis的其它功能實現中, 做爲一種數據結構工具使用. 在list
的實現中, 除了基本的鏈表定義外, 還額外增長了:
listIter
的定義, 與相關接口的實現.list
中的鏈表結點自己並不直接持有數據, 而是經過value
字段, 以void *
指針的形式間接持有, 因此數據的生命週期並不徹底與鏈表及其結點一致. 這給了list
的使用者至關大的靈活性. 好比能夠多個結點持有同一份數據的地址. 但與此同時, 在對鏈表進行銷燬, 結點複製以及查找匹配時, 就須要list
的使用者將相關的函數指針賦值於list.dup
, list.free
, list.match
字段.dict
是Redis底層數據結構中實現最爲複雜的一個數據結構, 其功能相似於C++標準庫中的std::unordered_map
, 其實現位於 src/dict.h
與 src/dict.c
中, 其關鍵定義以下:
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry; typedef struct dictType { uint64_t (*hashFunction)(const void *key); void *(*keyDup)(void *privdata, const void *key); void *(*valDup)(void *privdata, const void *obj); int (*keyCompare)(void *privdata, const void *key1, const void *key2); void (*keyDestructor)(void *privdata, void *key); void (*valDestructor)(void *privdata, void *obj); } dictType; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht; typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict; /* If safe is set to 1 this is a safe iterator, that means, you can call * dictAdd, dictFind, and other functions against the dictionary even while * iterating. Otherwise it is a non safe iterator, and only dictNext() * should be called while iterating. */ typedef struct dictIterator { dict *d; long index; int table, safe; dictEntry *entry, *nextEntry; /* unsafe iterator fingerprint for misuse detection. */ long long fingerprint; } dictIterator;
其內存佈局以下所示:
dict
中存儲的鍵值對, 是經過dictEntry
這個結構間接持有的, k
經過指針間接持有鍵, v
經過指針間接持有值. 注意, 若值是整數值的話, 是直接存儲在v字段中的, 而不是間接持有. 同時next
指針用於指向, 在bucket索引值衝突時, 以鏈式方式解決衝突, 指向同索引的下一個dictEntry
結構.dictht.table
中, 結點自己是散佈在內存中的, 順序表中存儲的是dictEntry
的指針dictht
結構, 其經過table
字段間接的持有順序表形式的bucket, bucket的容量存儲在size
字段中, 爲了加速將散列值轉化爲bucket中的數組索引, 引入了sizemask
字段, 計算指定鍵在哈希表中的索引時, 執行的操做相似於dict->type->hashFunction(鍵) & dict->ht[x].sizemask
. 從這裏也能夠看出來, bucket的容量適宜於爲2的冪次, 這樣計算出的索引值能覆蓋到全部bucket索引位.dict
即爲字典. 其中type
字段中存儲的是本字典使用到的各類函數指針, 包括散列函數, 鍵與值的複製函數, 釋放函數, 以及鍵的比較函數. privdata
是用於存儲用戶自定義數據. 這樣, 字典的使用者能夠最大化的自定義字典的實現, 經過自定義各類函數實現, 以及能夠附帶私有數據, 保證了字典有很大的調優空間.ht[2]
這個數組字段. 其用意是這樣的:
dict
僅持有一個哈希表dictht
的實例, 即整個字典由一個bucket實現.dictht
的實例, ht[0]
指向舊哈希表, ht[1]
指向擴容後的新哈希表. 平滑擴容的重點在於兩個策略:
ht[1]
指向的哈希表中ht[0]
中的一個bucket索引位持有的結點鏈表, 遷移到ht[1]
中去. 遷移的進度保存在rehashidx
這個字段中.在舊錶中因爲衝突而被連接在同一索引位上的結點, 遷移到新表後, 可能會散佈在多個新表索引中去.ht[0]
指向的舊錶會被釋放, 以後會將新表的持有權轉交給ht[0]
, 再重置ht[1]
指向NULL
dict->ht[0]->table[rehashindex]->k
與dict->ht[0]->table[rehashindex]->v
分別指向的實際數據, 內存地址都不會變化. 沒有發生鍵數據與值數據的拷貝或移動, 擴容整個過程僅是各類指針的操做. 速度很是快dict
的使用者是無感知的. 若擴容是一次性的, 當新舊bucket容量特別大時, 遷移全部結點必然會致使耗時陡增.除了字典自己的實現外, 其中還順帶實現了一個迭代器, 這個迭代器中有字段safe
以標示該迭代器是"安全迭代器"仍是"非安全迭代器", 所謂的安全與否, 指是的這種場景:
設想在運行迭代器的過程當中, 字典正處於平滑擴容的過程當中. 在平滑擴容的過程當中時, 舊錶一個索引位上的, 由衝突而鏈起來的多個結點, 遷移到新表後, 可能會散佈到新表的多個索引位上. 且新的索引位的值可能比舊的索引位要低.
遍歷操做的重點是, 保證在迭代器遍歷操做開始時, 字典中持有的全部結點, 都會被遍歷到. 而若在遍歷過程當中, 一個未遍歷的結點, 從舊錶遷移到新表後, 索引值減少了, 那麼就可能會致使這個結點在遍歷過程當中被遺漏.
因此, 所謂的"安全"迭代器, 其在內部實現時: 在迭代過程當中, 若字典正處於平滑擴容過程, 則暫停結點遷移, 直至迭代器運行結束. 這樣雖然不能保證在迭代過程當中插入的結點會被遍歷到, 但至少保證在迭代起始時, 字典中持有的全部結點都會被遍歷到.
這也是爲何dict
結構中有一個iterators
字段的緣由: 該字段記錄了運行於該字典上的安全迭代器的數目. 若該數目不爲0, 字典是不會繼續進行結點遷移平滑擴容的.
下面是字典的擴容操做中的核心代碼, 咱們以插入操做引發的擴容爲例:
先是插入操做的外部邏輯:
int dictAdd(dict *d, void *key, void *val) { dictEntry *entry = dictAddRaw(d,key,NULL); // 調用dictAddRaw if (!entry) return DICT_ERR; dictSetVal(d, entry, val); return DICT_OK; } dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); // 若在平滑擴容過程當中, 先步進遷移一個bucket索引 /* Get the index of the new element, or -1 if * the element already exists. */ // 在計算鍵在bucket中的索引值時, 內部會檢查是否須要擴容 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) return NULL; /* Allocate the memory and store the new entry. * Insert the element in top, with the assumption that in a database * system it is more likely that recently added entries are accessed * more frequently. */ ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; /* Set the hash entry fields. */ dictSetKey(d, entry, key); return entry; }
下面是計算bucket索引值的函數, 內部會探測該哈希表是否須要擴容, 若是須要擴容(結點數目與bucket數組長度比例達到1:1), 就使字典進入平滑擴容過程:
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing) { unsigned long idx, table; dictEntry *he; if (existing) *existing = NULL; /* Expand the hash table if needed */ if (_dictExpandIfNeeded(d) == DICT_ERR) // 探測是否須要擴容, 若是須要, 則開始擴容 return -1; for (table = 0; table <= 1; table++) { idx = hash & d->ht[table].sizemask; /* Search if this slot does not already contain the given key */ he = d->ht[table].table[idx]; while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) { if (existing) *existing = he; return -1; } he = he->next; } if (!dictIsRehashing(d)) break; } return idx; } /* Expand the hash table if needed */ static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ if (dictIsRehashing(d)) return DICT_OK; // 若是正在擴容過程當中, 則什麼也不作 /* If the hash table is empty expand it to the initial size. */ // 若字典中本無元素, 則初始化字典, 初始化時的bucket數組長度爲4 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling * the number of buckets. */ // 若字典中元素的個數與bucket數組長度比值大於1:1時, 則調用dictExpand進入平滑擴容狀態 if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, d->ht[0].used*2); } return DICT_OK; } int dictExpand(dict *d, unsigned long size) { dictht n; /* the new hash table */ // 新建一個dictht結構 unsigned long realsize = _dictNextPower(size); /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; /* Rehashing to the same table size is not useful. */ if (realsize == d->ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*));// 初始化dictht下的table, 即bucket數組 n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ // 如果新字典初始化, 直接把dictht結構掛在ht[0]中 if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } // 不然, 把新dictht結構掛在ht[1]中, 並開啓平滑擴容(置rehashidx爲0, 字典處於非擴容狀態時, 該字段值爲-1) /* Prepare a second hash table for incremental rehashing */ d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
下面是平滑擴容的實現:
static void _dictRehashStep(dict *d) { // 若字典上還運行着安全迭代器, 則不遷移結點 // 不然每次遷移一箇舊bucket索引上的全部結點 if (d->iterators == 0) dictRehash(d,1); } int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashidx); // 在舊bucket中, 找到下一個非空的索引位 while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } // 取出該索引位上的結點鏈表 de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ // 把全部結點遷移到新bucket中去 while(de) { uint64_t h; nextde = de->next; /* Get the index in the new hash table */ h = dictHashKey(d, de->key) & d->ht[1].sizemask; de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used--; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } /* Check if we already rehashed the whole table... */ // 檢查是否舊錶中的全部結點都被遷移到了新表 // 若是是, 則置先釋放原舊bucket數組, 再置ht[1]爲ht[0] // 最後再置rehashidx=-1, 以示字典不處於平滑擴容狀態 if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* More to rehash... */ return 1; }
總結:
dictEntry
結構持有, 故在平滑擴容過程當中, 不涉及用戶數據的拷貝dictType
結構與dict.privdata
字段), 對於一些特定場合使用的鍵數據, 用戶能夠自行選擇更高效更特定化的散列函數zskiplist
是Redis實現的一種特殊的跳躍表. 跳躍表是一種基於線性表實現簡單的搜索結構, 其最大的特色就是: 實現簡單, 性能能逼近各類搜索樹結構. 血統純正的跳躍表的介紹在維基百科中便可查閱. 在Redis中, 在原版跳躍表的基礎上, 進行了一些小改動, 便是如今要介紹的zskiplis
t結構.
其定義在src/server.h
中, 以下:
/* ZSETs use a specialized version of Skiplists */ typedef struct zskiplistNode { sds ele; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist;
其內存佈局以下圖:
zskiplist
的核心設計要點爲:
level[]
的長度爲32ele
字段, 還有一個字段score
, 其標示着結點的得分, 結點之間憑藉得分來判斷前後順序, 跳躍表中的結點按結點的得分升序排列.backward
指針, 這是原版跳躍表中所沒有的. 該指針指向結點的前一個緊鄰結點.zskiplistLevel
結構. 實際數量在結點建立時, 按冪次定律隨機生成(不超過32). 每一個zskiplistLevel
中有兩個字段.
forward
字段指向比本身得分高的某個結點(不必定是緊鄰的), 而且, 若當前zskiplistLevel
實例在level[]
中的索引爲X
, 則其forward
字段指向的結點, 其level[]
字段的容量至少是X+1
. 這也是上圖中, 爲何forward
指針老是畫的水平的緣由.span
字段表明forward
字段指向的結點, 距離當前結點的距離. 緊鄰的兩個結點之間的距離定義爲1.zskiplist
中持有字段level
, 用以記錄全部結點(除過頭結點外), level[]
數組最長的長度.跳躍表主要用於, 在給定一個分值的狀況下, 查找與該分值最接近的結點. 搜索時, 僞代碼以下:
int level = zskiplist->level - 1; zskiplistNode p = zskiplist->head; while(1 && p) { zskiplistNode q = (p->level)[level]->forward: if(q->score > 分值) { if(level > 0) { level--; } else { return : q爲整個跳躍表中, 分值大於指定分值的第一個結點 q->backward爲整個跳躍表中, 分值小於或等於指定分值的最後一個結點 } } else { p = q; } }
跳躍表的實現比較簡單, 最複雜的操做便是插入與刪除結點, 須要仔細處理鄰近結點的全部level[]
中的全部zskiplistLevel
結點中的forward
與span
的值的變動.
另外, 關於新建立的結點, 其level[]
數組長度的隨機算法, 在接口zslInsert
的實現中, 核心代碼片段以下:
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) { //... level = zslRandomLevel(); // 隨機生成新結點的, level[]數組的長度 if (level > zsl->level) { // 若生成的新結點的level[]數組的長度比當前表中全部結點的level[]的長度都大 // 那麼頭結點中須要新增幾個指向該結點的指針 // 並刷新ziplist中的level字段 for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } zsl->level = level; } x = zslCreateNode(level,score,ele); // 建立新結點 //... 執行插入操做 } // 按冪次定律生成小於32的隨機數的函數 // 宏 ZSKIPLIST_MAXLEVEL 的定義爲32, 宏 ZSKIPLIST_P 被設定爲 0.25 // 即 // level == 1的機率爲 75% // level == 2的機率爲 75% * 25% // level == 3的機率爲 75% * 25% * 25% // ... // level == 31的機率爲 0.75 * 0.25^30 // 而 // level == 32的機率爲 0.75 * sum(i = 31 ~ +INF){ 0.25^i } int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }
這是一個用於存儲在序的整數的數據結構, 也底層數據結構中最簡單的一個, 其定義與實如今src/intest.h
與src/intset.c
中, 關鍵定義以下:
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset; #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
inset
結構中的encoding
的取值有三個, 分別是宏INTSET_ENC_INT16
, INTSET_ENC_INT32
, INTSET_ENC_INT64
. length
表明其中存儲的整數的個數, contents
指向實際存儲數值的連續內存區域. 其內存佈局以下圖所示:
intset
中各字段, 包括contents
中存儲的數值, 都是以主機序(小端字節序)存儲的. 這意味着Redis若運行在PPC這樣的大端字節序的機器上時, 存取數據都會有額外的字節序轉換開銷encoding == INTSET_ENC_INT16
時, contents
中以int16_t
的形式存儲着數值. 相似的, 當encoding == INTSET_ENC_INT32
時, contents
中以int32_t
的形式存儲着數值.int32_t
的取值範圍, 整個intset
都要進行升級, 即全部的數值都須要以int64_t
的形式存儲. 顯然升級的開銷是很大的.intset
中的數值是以升序排列存儲的, 插入與刪除的複雜度均爲O(n). 查找使用二分法, 複雜度爲O(log_2(n))intset
的代碼實現中, 不預留空間, 即每一次插入操做都會調用zrealloc
接口從新分配內存. 每一次刪除也會調用zrealloc
接口縮減佔用的內存. 省是省了, 但內存操做的時間開銷上升了.intset
的編碼方式一經升級, 不會再降級.總之, intset
適合於以下數據的存儲:
int16_t
或int32_t
的取值範圍中ziplist
是Redis底層數據結構中, 最苟的一個結構. 它的設計宗旨就是: 省內存, 從牙縫裏省內存. 設計思路和TLV一致, 但爲了從牙縫裏節省內存, 作了不少額外工做.
ziplist
的內存佈局與intset
同樣: 就是一塊連續的內存空間. 但區域劃分比較複雜, 概覽以下圖:
intset
同樣, ziplist
中的全部值都是以小端序存儲的zlbytes
字段的類型是uint32_t
, 這個字段中存儲的是整個ziplist
所佔用的內存的字節數zltail
字段的類型是uint32_t
, 它指的是ziplist
中最後一個entry
的偏移量. 用於快速定位最後一個entry
, 以快速完成pop
等操做zllen
字段的類型是uint16_t
, 它指的是整個ziplit
中entry
的數量. 這個值只佔16位, 因此蛋疼的地方就來了: 若是ziplist
中entry
的數目小於65535, 那麼該字段中存儲的就是實際entry
的值. 若等於或超過65535, 那麼該字段的值固定爲65535, 但實際數量須要一個個entry
的去遍歷全部entry
才能獲得.zlend
是一個終止字節, 其值爲全F, 即0xff
. ziplist
保證任何狀況下, 一個entry
的首字節都不會是255
在畫圖展現entry
的內存佈局以前, 先講一下entry
中都存儲了哪些信息:
entry
中存儲了它前一個entry
所佔用的字節數. 這樣支持ziplist
反向遍歷.entry
用單獨的一塊區域, 存儲着當前結點的類型: 所謂的類型, 包括當前結點存儲的數據是什麼(二進制, 仍是數值), 如何編碼(若是是數值, 數值如何存儲, 若是是二進制數據, 二進制數據的長度)entry
的內存佈局以下所示:
prevlen
便是"前一個entry所佔用的字節數", 它自己是一個變長字段, 規約以下:
entry
佔用的字節數小於 254, 則prevlen
字段佔一字節entry
佔用的字節數等於或大於 254, 則prevlen
字段佔五字節: 第一個字節值爲 254, 即0xfe
, 另外四個字節, 以uint32_t
存儲着值.encoding
字段的規約就複雜了許多
encoding
佔一字節. 在這一字節中, 高兩位值固定爲0, 低六位值以無符號整數的形式存儲着二進制數據的長度. 即 00xxxxxx
, 其中低六位bitxxxxxx
是用二進制保存的數據長度.encoding
佔用兩個字節. 在這兩個字節16位中, 第一個字節的高兩位固定爲01
, 剩餘的14個位, 以小端序無符號整數的形式存儲着二進制數據的長度, 即 01xxxxxx, yyyyyyyy
, 其中yyyyyyyy
是高八位, xxxxxx
是低六位.encoding
佔用五個字節. 第一個字節是固定值10000000
, 剩餘四個字節, 按小端序uint32_t
的形式存儲着二進制數據的長度. 這也是ziplist
能存儲的二進制數據的最大長度, 超過2^32-1
字節的二進制數據, ziplist
沒法存儲.encoding
和data
的規約以下:
entry
, 其encoding
都僅佔用一個字節. 而且最高兩位均是11
[0, 12]
中, 則encoding
和data
擠在同一個字節中. 即爲1111 0001
~1111 1101
, 高四位是固定值, 低四位的值從0001
至1101
, 分別表明 0 ~ 12這十五個數值[-128, -1] [13, 127]
中, 則encoding == 0b 1111 1110
. 數值存儲在緊鄰的下一個字節, 以int8_t
形式編碼[-32768, -129] [128, 32767]
中, 則encoding == 0b 1100 0000
. 數值存儲在緊鄰的後兩個字節中, 以小端序int16_t
形式編碼[-8388608, -32769] [32768, 8388607]
中, 則encoding == 0b 1111 0000
. 數值存儲在緊鄰的後三個字節中, 以小端序存儲, 佔用三個字節.[-2^31, -8388609] [8388608, 2^31 - 1]
中, 則encoding == 0b 1101 0000.
數值存儲在緊鄰的後四個字節中, 以小端序int32_t
形式編碼int64_t
所能表達的範圍內, 則encoding == 0b 1110 0000
, 數值存儲在緊鄰的後八個字節中, 以小端序int64_t
形式編碼在大規模數值存儲中, ziplist
幾乎不浪費內存空間, 其苟的程序到達了字節級別, 甚至對於[0, 12]
區間的數值, 連data
裏的那一個字節也要省下來. 顯然, ziplist
是一種特別節省內存的數據結構, 但它的缺點也十分明顯:
intset
同樣, ziplist
也不預留內存空間, 而且在移除結點後, 也是當即縮容, 這表明每次寫操做都會進行內存分配操做.ziplist
最蛋疼的一個問題是: 結點若是擴容, 致使結點佔用的內存增加, 而且超過254字節的話, 可能會致使鏈式反應: 其後一個結點的entry.prevlen
須要從一字節擴容至五字節. 最壞狀況下, 第一個結點的擴容, 會致使整個ziplist
表中的後續全部結點的entry.prevlen
字段擴容. 雖然這個內存重分配的操做依然只會發生一次, 但代碼中的時間複雜度是o(N)級別, 由於鏈式擴容只能一步一步的計算. 但這種狀況的機率十分的小, 通常狀況下鏈式擴容能連鎖反映五六次就很不幸了. 之因此說這是一個蛋疼問題, 是由於, 這樣的壞場景下, 其實時間複雜度並不高: 依次計算每一個entry
新的空間佔用, 也就是o(N), 整體佔用計算出來後, 只執行一次內存重分配, 與對應的memmove
操做, 就能夠了. 蛋疼說的是: 代碼特別難寫, 難讀. 下面放一段處理插入結點時處理鏈式反應的代碼片段, 你們自行感覺一下:unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen; unsigned int prevlensize, prevlen = 0; size_t offset; int nextdiff = 0; unsigned char encoding = 0; long long value = 123456789; /* initialized to avoid warning. Using a value that is easy to see if for some reason we use it uninitialized. */ zlentry tail; /* Find out prevlen for the entry that is inserted. */ if (p[0] != ZIP_END) { ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); } else { unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); if (ptail[0] != ZIP_END) { prevlen = zipRawEntryLength(ptail); } } /* See if the entry can be encoded */ if (zipTryEncoding(s,slen,&value,&encoding)) { /* 'encoding' is set to the appropriate integer encoding */ reqlen = zipIntSize(encoding); } else { /* 'encoding' is untouched, however zipStoreEntryEncoding will use the * string length to figure out how to encode it. */ reqlen = slen; } /* We need space for both the length of the previous entry and * the length of the payload. */ reqlen += zipStorePrevEntryLength(NULL,prevlen); reqlen += zipStoreEntryEncoding(NULL,encoding,slen); /* When the insert position is not equal to the tail, we need to * make sure that the next entry can hold this entry's length in * its prevlen field. */ int forcelarge = 0; nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; if (nextdiff == -4 && reqlen < 4) { nextdiff = 0; forcelarge = 1; } /* Store offset because a realloc may change the address of zl. */ offset = p-zl; zl = ziplistResize(zl,curlen+reqlen+nextdiff); p = zl+offset; /* Apply memory move when necessary and update tail offset. */ if (p[0] != ZIP_END) { /* Subtract one because of the ZIP_END bytes */ memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); /* Encode this entry's raw length in the next entry. */ if (forcelarge) zipStorePrevEntryLengthLarge(p+reqlen,reqlen); else zipStorePrevEntryLength(p+reqlen,reqlen); /* Update offset for tail */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); /* When the tail contains more than one entry, we need to take * "nextdiff" in account as well. Otherwise, a change in the * size of prevlen doesn't have an effect on the *tail* offset. */ zipEntry(p+reqlen, &tail); if (p[reqlen+tail.headersize+tail.len] != ZIP_END) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff); } } else { /* This element will be the new tail. */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl); } /* When nextdiff != 0, the raw length of the next entry has changed, so * we need to cascade the update throughout the ziplist */ if (nextdiff != 0) { offset = p-zl; zl = __ziplistCascadeUpdate(zl,p+reqlen); p = zl+offset; } /* Write the entry */ p += zipStorePrevEntryLength(p,prevlen); p += zipStoreEntryEncoding(p,encoding,slen); if (ZIP_IS_STR(encoding)) { memcpy(p,s,slen); } else { zipSaveInteger(p,value,encoding); } ZIPLIST_INCR_LENGTH(zl,1); return zl; } unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize; size_t offset, noffset, extra; unsigned char *np; zlentry cur, next; while (p[0] != ZIP_END) { zipEntry(p, &cur); rawlen = cur.headersize + cur.len; rawlensize = zipStorePrevEntryLength(NULL,rawlen); /* Abort if there is no next entry. */ if (p[rawlen] == ZIP_END) break; zipEntry(p+rawlen, &next); /* Abort when "prevlen" has not changed. */ if (next.prevrawlen == rawlen) break; if (next.prevrawlensize < rawlensize) { /* The "prevlen" field of "next" needs more bytes to hold * the raw length of "cur". */ offset = p-zl; extra = rawlensize-next.prevrawlensize; zl = ziplistResize(zl,curlen+extra); p = zl+offset; /* Current pointer and offset for next element. */ np = p+rawlen; noffset = np-zl; /* Update tail offset when next element is not the tail element. */ if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra); } /* Move the tail to the back. */ memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1); zipStorePrevEntryLength(np,rawlen); /* Advance the cursor */ p += rawlen; curlen += extra; } else { if (next.prevrawlensize > rawlensize) { /* This would result in shrinking, which we want to avoid. * So, set "rawlen" in the available bytes. */ zipStorePrevEntryLengthLarge(p+rawlen,rawlen); } else { zipStorePrevEntryLength(p+rawlen,rawlen); } /* Stop here, as the raw length of "next" has not changed. */ break; } } return zl; }
這種代碼的特色就是: 最好由做者去維護, 最好一次性寫對. 由於讀起來真的費勁, 改起來也很費勁.
若是說ziplist
是整個Redis中爲了節省內存, 而寫的最苟的數據結構, 那麼稱quicklist
就是在最苟的基礎上, 再苟了一層. 這個結構是Redis在3.2版本後新加的, 在3.2版本以前, 咱們能夠講, dict
是最複雜的底層數據結構, ziplist
是最苟的底層數據結構. 在3.2版本以後, 這兩個記錄被雙雙刷新了.
這是一種, 以ziplist
爲結點的, 雙端鏈表結構. 宏觀上, quicklist
是一個鏈表, 微觀上, 鏈表中的每一個結點都是一個ziplist
.
它的定義與實現分別在src/quicklist.h
與src/quicklist.c
中, 其中關鍵定義以下:
/* Node, quicklist, and Iterator are the only data structures used currently. */ /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporarry decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; /* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'. * 'sz' is byte length of 'compressed' field. * 'compressed' is LZF data with total (compressed) length 'sz' * NOTE: uncompressed length is stored in quicklistNode->sz. * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */ typedef struct quicklistLZF { unsigned int sz; /* LZF size in bytes*/ char compressed[]; } quicklistLZF; /* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned long len; /* number of quicklistNodes */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */ } quicklist; typedef struct quicklistIter { const quicklist *quicklist; quicklistNode *current; unsigned char *zi; long offset; /* offset in current ziplist */ int direction; } quicklistIter; typedef struct quicklistEntry { const quicklist *quicklist; quicklistNode *node; unsigned char *zi; unsigned char *value; long long longval; unsigned int sz; int offset; } quicklistEntry;
這裏定義了五個結構體:
quicklistNode
, 宏觀上, quicklist
是一個鏈表, 這個結構描述的就是鏈表中的結點. 它經過zl
字段持有底層的ziplist
. 簡單來說, 它描述了一個ziplist
實例quicklistLZF
, ziplist
是一段連續的內存, 用LZ4算法壓縮後, 就能夠包裝成一個quicklistLZF
結構. 是否壓縮quicklist
中的每一個ziplist
實例是一個可配置項. 若這個配置項是開啓的, 那麼quicklistNode.zl
字段指向的就不是一個ziplist
實例, 而是一個壓縮後的quicklistLZF
實例quicklist
. 這就是一個雙鏈表的定義. head, tail
分別指向頭尾指針. len
表明鏈表中的結點. count
指的是整個quicklist
中的全部ziplist
中的entry
的數目. fill
字段影響着每一個鏈表結點中ziplist
的最大佔用空間, compress
影響着是否要對每一個ziplist
以LZ4算法進行進一步壓縮以更節省內存空間.quicklistIter
是一個迭代器quicklistEntry
是對ziplist
中的entry
概念的封裝. quicklist
做爲一個封裝良好的數據結構, 不但願使用者感知到其內部的實現, 因此須要把ziplist.entry
的概念從新包裝一下.quicklist
的內存佈局圖以下所示:
下面是有關quicklist
的更多額外信息:
quicklist.fill
的值影響着每一個鏈表結點中, ziplist
的長度.
ziplist
的最大長度. 具體爲:
-1
不超過4kb-2
不超過 8kb-3
不超過 16kb-4
不超過 32kb-5
不超過 64kbentry
數目限制單個ziplist
的長度. 值即爲數目. 因爲該字段僅佔16位, 因此以entry
數目限制ziplist
的容量時, 最大值爲2^15個quicklist.compress
的值影響着quicklistNode.zl
字段指向的是原生的ziplist
, 仍是通過壓縮包裝後的quicklistLZF
0
表示不壓縮, zl
字段直接指向ziplist
1
表示quicklist
的鏈表頭尾結點不壓縮, 其他結點的zl
字段指向的是通過壓縮後的quicklistLZF
2
表示quicklist
的鏈表頭兩個, 與末兩個結點不壓縮, 其他結點的zl
字段指向的是通過壓縮後的quicklistLZF
2^16
quicklistNode.encoding
字段, 以指示本鏈表結點所持有的ziplist
是否通過了壓縮. 1
表明未壓縮, 持有的是原生的ziplist
, 2
表明壓縮過quicklistNode.container
字段指示的是每一個鏈表結點所持有的數據類型是什麼. 默認的實現是ziplist
, 對應的該字段的值是2
, 目前Redis沒有提供其它實現. 因此實際上, 該字段的值恆爲2quicklistNode.recompress
字段指示的是當前結點所持有的ziplist
是否通過了解壓. 若是該字段爲1
即表明以前被解壓過, 且須要在下一次操做時從新壓縮.quicklist
的具體實現代碼篇幅很長, 這裏就不貼代碼片段了, 從內存佈局上也能看出來, 因爲每一個結點持有的ziplist
是有上限長度的, 因此在與操做時要考慮的分支狀況比較多. 想一想都蛋疼.
quicklist
有本身的優勢, 也有缺點, 對於使用者來講, 其使用體驗相似於線性數據結構, list
做爲最傳統的雙鏈表, 結點經過指針持有數據, 指針字段會耗費大量內存. ziplist
解決了耗費內存這個問題. 但引入了新的問題: 每次寫操做整個ziplist
的內存都須要重分配. quicklist
在二者之間作了一個平衡. 而且使用者能夠經過自定義quicklist.fill
, 根據實際業務狀況, 經驗主義調參.
dict
做爲字典結構, 優勢不少, 擴展性強悍, 支持平滑擴容等等, 但對於字典中的鍵值均爲二進制數據, 且長度都很小時, dict
的中的一坨指針會浪費很多內存, 所以Redis又實現了一個輕量級的字典, 即爲zipmap
.
zipmap
適合使用的場合是:
dict
支持各類嵌套, 字典自己並不持有數據, 而僅持有數據的指針. 但zipmap
是直接持有數據的.zipmap
的定義與實如今src/zipmap.h
與src/zipmap.c
兩個文件中, 其定義與實現均未定義任何struct結構體, 由於zipmap
的內存佈局就是一塊連續的內存空間. 其內存佈局以下所示:
zipmap
起始的第一個字節存儲的是zipmap
中鍵值對的個數. 若是鍵值對的個數大於254的話, 那麼這個字節的值就是固定值254, 真實的鍵值對個數須要遍歷才能得到.zipmap
的最後一個字節是固定值0xFF
zipmap
中的每個鍵值對, 稱爲一個entry
, 其內存佔用如上圖, 分別六部分:
len_of_key
, 一字節或五字節. 存儲的是鍵的二進制長度. 若是長度小於254, 則用1字節存儲, 不然用五個字節存儲, 第一個字節的值固定爲0xFE
, 後四個字節以小端序uint32_t
類型存儲着鍵的二進制長度.key_data
爲鍵的數據len_of_val
, 一字節或五字節, 存儲的是值的二進制長度. 編碼方式同len_of_key
len_of_free
, 固定值1字節, 存儲的是entry
中未使用的空間的字節數. 未使用的空間即爲圖中的free
, 它通常是因爲鍵值對中的值被替換髮生的. 好比, 鍵值對hello <-> word
被修改成hello <-> w
後, 就空了四個字節的閒置空間val_data
, 爲值的數據free
, 爲閒置空間. 因爲len_of_free
的值最大隻能是254, 因此若是值的變動致使閒置空間大於254的話, zipmap
就會回收內存空間.銜接底層數據結構, 與五種Value Type之間的橋樑就是redisObject
這個結構. 該結構的關鍵定義以下(位於src/server.h
中):
/*----------------------------------------------------------------------------- * Data types *----------------------------------------------------------------------------*/ /* A redis object, that is a type able to hold a string / list / set */ /* The actual Redis Object */ #define OBJ_STRING 0 #define OBJ_LIST 1 #define OBJ_SET 2 #define OBJ_ZSET 3 #define OBJ_HASH 4 /* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ #define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */ #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ #define LRU_BITS 24 #define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */ #define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */ #define OBJ_SHARED_REFCOUNT INT_MAX typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; void *ptr; } robj;
redisObject
的內存佈局以下:
從定義上來看, redisObject
有:
type
字段encoding
字段, 代表對象底層使用的數據結構類型lru
字段refcount
ptr
字段redisObject
的通用操做API以下:
API | 功能 |
---|---|
char *strEncoding(int encoding) |
返回各類編碼的可讀字符串表達 |
void decrRefCount(robj *o); |
引用計數-1. 若減後引用計數會降爲0, 則會自動調用 freeXXXObject 函數釋放對象 |
void decrRefCountVoid(void *o); |
功能同decrRefCount , 只不過接收的是void * 型參數 |
void incrRefCount(robj *o); |
引用計數+1 |
robj *makeObjectShared(robj *o); |
將對象置爲"全局共享對象", 所謂的"全局只讀共享對象", 有如下特徵 0. 內部引用計數爲 INT_MAX 0. 引用計數操做函數對其不起做用 0. 多純種共享讀是安全的, 不須要加鎖 0. 禁止寫操做 |
robj *resetRefCount(robj *obj); |
將引用計數置爲0, 但不會調用freeXXXObject 函數釋放對象 |
robj *createObject(int type, void *ptr); |
建立一個對象, 對象類型由參數指定, 對象底層編碼指定爲RAW , 底層數據由參數提供, 對象引用計數爲1.並初始化 lru 字段. 若服務器採用LRU算法, 則置該字段的值爲當前分鐘級別的一個時間戳. 若服務器採用LFU算法, 則置爲一個計數值. |
unsigned long long estimateObjectIdleTime(robj *o) |
獲取一個對象未被訪問的時間, 單位爲毫秒. 因爲 redisObject 中lru 字段有24位, 並非無限長, 因此有循環溢出的風險, 當發生循環溢出時(即當前LRU時鐘計數比對象中的lru 字段小), 那麼該函數始終保守認爲循環溢出只發生了一次 |
字符串對象支持三種編碼方式: INT
, RAW
, EMBSTR
, 三種方式的內存佈局分別以下:
字符串對象的相關接口以下:
分類 | API名 | 功能 |
---|---|---|
建立接口 | robj *createEmbeddedStringObject(const char *ptr,size_t len) |
建立一個編碼爲EMBSTR 的字符串對象.即底層使用SDS, 且SDS與RedisObject位於同一塊連續內存上 |
-- | robj *createRawStringObject(const char *ptr,size_t len) |
建立一個編碼爲RAW 的字符串對象.即底層使用SDS, 且SDS由RedisObject間接持有 內部是先用入參建立一個SDS, 而後用這個SDS再去調用 createObject |
-- | robj *createStringObject(const char *ptr,size_t len) |
建立一個字符串對象. 當 len 參數的值小於或等於OBJ_ENCODING_EMBSTR_SIZE_LIMIT 時, 編碼方式爲EMBSTR , 不然爲RAW 內部是經過調用 createRawStringObject 與createEmbeddedStringObject 來建立不一樣編碼的字符串對象的 |
-- | robj *createStringObjectFromLongLong(long long value) |
根據整數值, 建立一個字符串對象. 若可複用全局共享字符串對象池中的對象, 則會盡可能複用. 不然以最節省內存的原則, 來決定對象的編碼 |
-- | robj *createStringObjectFromLongDouble(long double value,int humanfriendly) |
根據浮點數值, 建立一個字符串對象 其中參數 humanfriendly 不爲0, 則字符串以小數形式表達. 不然以exp計數法表達.根據字符串表達的長短, 編碼多是RAW , 或EMBSTR |
釋放接口 | void freeStringObject(robj *o) |
釋放字符串對象. 若字符串對象底層使用SDS, 則調用 sdsfree 釋放這個SDS.不然什麼也不作 |
讀寫接口 | robj *dupStringObject(const robj *o) |
建立一個字符串對象的深拷貝副本. 不影響原字符串對象的引用計數. 建立的副本與原字符串毫無關聯 |
-- | int isSdsRepresentableAsLongLong(sds s,long long *llval) |
判斷SDS字符串是不是一個取值在long long 數值範圍內的數值的字符串表達. 若是是, 就把相應的數值置在出參中內部調用的是 string2ll 來判斷嚴格來說這不該該算是 RedisObject 的接口函數, 而應當算是SDS 的接口函數" |
-- | int isObjectRepresentableAsLongLong(robj *o,long long *llval) |
判斷字符串對象是不是一個取值在long long 數值範圍內的數值的字符串表達. 若是是, 就把相應的數值置在出參中. |
-- | robj *tryObjectEncoding(robj *o) |
嘗試縮減這個字符串對象的內存佔用. 策略爲: 若是字符串對象表明的是一個位於 long 取值範圍內的數值, 則嘗試返回全局共享字符串對象池裏的等價對象. 若因爲服務器配置等緣由不成功, 則嘗試將對象編碼改成INT 若是以上都不成功, 則嘗試將對象的編碼改成 EMBSTR 若以上都不成功, 則在對象的編碼爲 RAW 的狀態下, 至少調用sdsRemoveFreeSpace 來移除掉內部SDS中, 閒置的內存空間 |
-- | robj *getDecodedObject(robj *o) |
返回字符串對象的一個淺拷貝. 在編碼爲 RAW 或EMBSTR 時, 底層數據引用計數+1, 返回一個共享句柄在編碼爲 INT 時, 返回一個編碼爲RAW 或EMBSTR 的新副本的句柄. 新舊對象之間無關 |
-- | size_t stringObjectLen(robj *o) |
返回字符串對象中的字符個數 |
-- | int getDoubleFromObject(const robj *o,double *target) |
從字符串對象中解析出數值, 兼容整數值 |
-- | int getLongLongFromObject(robj *o,long long *target) |
從字符串對象中解析出整數值, 不兼容浮點數值 |
-- | int getLongDoubleFromObject(robj *o,long double *target) |
從字符串對象中解析出數值, 兼容整數值 |
-- | int compareStringObjects(robj *a, robj *b) |
二進制比較兩個字符串對象. 如有字符串對象使用的是INT 編碼, 則先會把ptr中的數值轉化爲字符串表達, 而後再去比較 |
-- | int collateStringObjects(robj *a, robj *b) |
底層調用strcoll去比較兩個字符串對象. 比較的大小結果受LC_LOCALE 的影響 |
-- | int equalStringObjects(robj *a, robj *b) |
字符串判等 |
-- | #define sdsEncodedObject(objptr) |
宏, 判斷字符串對象的內部是否爲SDS實現. 即編碼爲RAW 或EMBSTR |
哈希對象的底層實現有兩種, 一種是dict
, 一種是ziplist
. 分別對應編碼HT
與ZIPLIST
. 而以前介紹的zipmap
這種結構, 雖然也是一種輕量級的字典結構, 且縱使在源代碼中有相應的編碼宏值, 但遺憾的是, 至Redis 4.0.10, 目前哈希對象的底層編碼仍然只有ziplist
與dict
兩種
dict
自沒必要說, 自己就是字典類型, 存儲鍵值對的. 用ziplist
做爲底層數據結構時, 是將鍵值對以<key1><value1><key2><value2>...<keyn><valuen>
這樣的形式存儲在ziplist
中的. 兩種編碼內存佈局分別以下:
上圖中不嚴謹的地方有:
ziplist
中每一個entry, 除了鍵與值自己的二進制數據, 還包括其它字段, 圖中沒有畫出來dict
底層可能持有兩個dictht
實例dict
的哈希衝突須要注意的是: 當採用HT
編碼, 即便用dict
做爲哈希對象的底層數據結構時, 鍵與值均是以sds的形式存儲的.
哈希對象的相關接口以下:
分類 | API名 | 功能 |
---|---|---|
建立接口 | robj *createHashObject(void) |
建立一個空哈希對象 底層編碼使用 ZIPLIST , 即底層使用ziplist |
釋放接口 | void freeHashObject(robj *o) |
釋放哈希對象 若哈希對象底層使用的是dict, 則調用 dictRelease 釋放這個dict若哈希對象底層使用的是ziplist, 則直接釋放掉這個ziplist佔用的連續內存空間 |
編碼轉換接口 | void hashTypeConvertZiplist(robj *o, int enc) |
將哈希對象的編碼從ZIPLIST 轉換爲HT , 即底層實現從ziplist轉爲dict |
-- | void hashTypeConvert(robj *o, int enc) |
轉換哈希對象的編碼. 雖然接口設計的好像能夠在底層編碼之間互相轉換, 但實際上這個接口的實現, 目前僅支持從 ZIPLIST 轉向HT |
-- | void hashTypeTryConversion(robj *o,robj **argv,int start,int end) |
o 是一個哈希對象. argv 是其它對象的數組.(最好是字符串對象, 且爲SDS實現)這個函數會檢查 argv 數組中, 從start 到end 之間的全部對象, 若是這些對象中, 但凡是有一個對象是字符串對象, 且長度超過了用ziplist實現哈希對象時, ziplist的限長那麼 o 這個哈希對象的編碼就會從ZIPLIST 轉爲HT |
讀寫接口 | int hashTypeSet(robj *o,sds field,sds value,int flags) |
向哈希對象寫入一個鍵值對. 在底層編碼爲 HT 時, flag 將影響插入鍵值對時的具體行爲. flag 可有標誌位 HASH_SET_TAKE_VALUE 與HASH_SET_TAKE_FIELD , 若對應位置1, 表明鍵與值直接引用參數值. 不然表明要調用sdsdup 接口拷貝鍵與值.在底層編碼爲 ZIPLIST 時, 鍵與值必然會被拷貝 |
-- | int hashTypeExists(robj *o, sds field) |
查詢指定鍵在哈希對象中是否存在 |
-- | unsigned long hashTypeLength(const robj *o) |
查詢哈希對象中的鍵值對總數 |
-- | int hashTypeGetFromZiplist(robj *o, sds field,unsigned char **vstr,unsigned int *vlen,long long *vll) |
從編碼爲ZIPLIST 的哈希對象中, 取出一個鍵對應的值. 鍵從 field 傳入, 當值爲數值類型時, 值以*vll 傳出, 當值爲二進制類型時, 值以*vstr 與*vlen 傳出 |
-- | sds hashTypeGetFromHashTable(robj *o, sds field) |
從編碼爲HT 的哈希對象中, 取出一個鍵對應的值.鍵從 field 傳入, 值以返回值傳出. 若值不存在, 返回NULL" |
-- | "int hashTypeGetValue(robj *o,sds field,unsigned char **vstr,unsigned int *vlen,long long *vll) |
取出哈希對象中指定鍵對應的值. 若值是數值類型, 則以*vll 傳出, 不然以*vstr 與*vlen 傳出 |
-- | robj *hashTypeGetValueObject(robj *o, sds field) |
取出哈希對象中指定鍵對應的值, 幷包裝成RedisObject 返回. 返回的對象爲字符串對象 |
-- | size_t hashTypeGetValueLength(robj *o, sds field) |
取出哈希對象中指定鍵對應的值的長度 |
-- | int hashTypeDelete(robj *o, sds field) |
刪除哈希對象中的一個鍵值對. 鍵不存在時返回0, 成功刪除返回1 |
迭代器接口 | hashTypeIterator *hashTypeInitIterator(robj *subject) |
在指定哈希對象上建立一個迭代器 |
-- | void hashTypeReleaseIterator(hashTypeIterator *hi) |
釋放哈希對象的迭代器 |
-- | int hashTypeNext(hashTypeIterator *hi) |
讓哈希迭代器步進一步 |
-- | void hashTypeCurrentFromZiplist(hashTypeIterator *hi,int what,unsigned char **vstr,unsigned int *vlen,long long *vll) |
取出哈希對象迭代器當前指向的鍵 或值. 當what 傳入OBJ_HASH_KEY 時, 取的是鍵, 不然取的是值.注意, 該函數僅在哈希對象的編碼爲 ZIPLIST 時才能正確運行 |
-- | sds hashTypeCurrentFromHashTable(hashTypeIterator *hi,int what) |
取出哈希對象迭代器當前指向的鍵 或值. 當what 傳入OBJ_HASH_KEY 時, 取的是鍵, 不然取的是值.注意, 該函數僅在哈希對象的編碼爲 HT 時才能正確運行 |
-- | void hashTypeCurrentObject(hashTypeIterator *hi,int what,unsigned char **vstr,unsigned int *vlen,long long *vll) |
取出哈希對象迭代器當前指向的鍵或值. 當what 傳入OBJ_HASH_KEY 時, 取的是鍵, 不然取的是值. |
-- | sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi,int what) |
取出哈希對象迭代器當前指向的鍵或值. 且把鍵或值以一個全新的SDS字符串返回. 當what 傳入OBJ_HASH_KEY 時, 取的是鍵, 不然取的是值. |
列表對象的底層實現, 歷史上是有兩種的, 分別是ziplist
與list
, 但截止Redis 4.0.10版本, 全部的列表對象API都再也不支持除去quicklist
以外的任何底層實現. 也就是說, 目前(Redis 4.0.10), 列表對象支持的底層實現實質上只有一種, 便是quicklist
.
列表對象的建立API依然支持從ziplist
的實例建立一個列表對象, 即你能夠建立一個底層編碼爲ZIPLIST
的列表對象, 但若是用該列表對象去調用任何其它列表對象的API, 都會致使panic. 在使用以前, 你只能再次調用相關的底層編碼轉換接口, 將這個列表對象的底層編碼轉換爲QUICKLIST
.
而且遺憾的是, LINKEDLIST
這種編碼, 即底層爲list
的列表, 被完全淘汰了. 也就是說, 截止目前(Redis 4.0.10), Redis定義的10個對象編碼方式宏名中, 有兩個被徹底閒置了, 分別是: OBJ_ENCODING_ZIPMAP
與OBJ_ENCODING_LINKEDLIST
. 從Redis的演進歷史上來看, 前者是後續可能會獲得支持的編碼值, 後者則應該是被完全淘汰了.
列表對象的內存佈局以下圖所示:
列表對象的API接口以下:
分類 | API名 | 功能 |
---|---|---|
建立接口 | robj *createQuicklistObject(void) |
建立一個列表對象. 內部編碼爲QUICKLIST 即內部使用quicklist實現的列表對象 |
-- | robj *createZiplistObject(void) |
建立一個列表對象. 內部編碼爲ZIPLIST 即內部使用ziplist實現的列表對象 |
釋放接口 | void freeListObject(robj *o) |
釋放一個列表對象 |
編碼轉換接口 | void listTypeConvert(robj *subject, int enc) |
轉換列表對象的內部編碼. 雖然接口設計的好你能夠在底層編碼之間互相轉換, 但實際上這個接口的實現, 目前僅支持從 ZIPLIST 轉換爲QUICKLIST 而且蛋疼的是, 4.0.10這個版本中, 全部的列表對象操做API內部實現都僅支持編碼方式爲 QUICKLIST 的列表對象, 其它編碼方式會panic.因此目前爲止, 這個API的惟一做用, 就是配合 createZiplistObject 接口, 來使用一個ziplist建立一個內部編碼爲QUICKLIST 的列表對象. |
讀寫接口 | void listTypePush(robj *subject,robj *value,int where) |
向列表對象中添加一個數據. 由 where 參數的值控制是在頭部添加, 仍是尾部添加.where 可選的值爲LIST_HEAD , LIST_TAIL |
-- | robj *listTypePop(robj *subject,int where) |
從列表對象的頭部或尾部取出一個數據. 取出的數據經過被包裝成字符串對象後返回. 具體取出位置經過參數 where 控制 |
-- | unsigned long listTypeLength(const robj *subject) |
獲取列表對象中保存的數據的個數 |
-- | void listTypeInsert(listTypeEntry *entry,robj *value, int where) |
將字符串對象中的數據插入到列表對象的頭部或尾部. 插入過程當中不會拷貝字符串對象持有的數據自己. 但會縮減字符串對象的引用計數. |
-- | int listTypeEqual(listTypeEntry *entry, robj *o) |
判斷字符串對象o 與列表對象中指定位置上存儲的數據是否相同. |
-- | robj *listTypeGet(listTypeEntry *entry) |
獲取列表對象中指定位置的數據. 位置信息經過 entry 傳入, 這是一個入參. 數據將拷貝一份後經過SDS形式返回 |
迭代器接口 | listTypeIterator *listTypeInitIterator(robj *subject,long index,unsigned char direction) |
建立一個列表對象迭代器 |
-- | void listTypeReleaseIterator(listTypeIterator *li) |
釋放一個列表對象迭代器 |
-- | int listTypeNext( listTypeIterator *li, listTypeEntry *entry) |
讓列表對象迭代器步進一步, 並將步進以前迭代器所指向的數據保存在entry 中 |
-- | void listTypeDelete( listTypeIterator *iter, listTypeEntry *entry) |
刪除列表迭代器當前指向的列表對象中存儲的數據. 被刪除的數據經過 entry 返回 |
集合對象的底層實現有兩種, 分別是intset
和dict
. 分別對應編碼宏中的INTSET
和HT
. 顯然當使用intset
做爲底層實現的數據結構時, 集合中存儲的只能是數值數據, 且必須是整數. 而當使用dict
做爲集合對象的底層實現時, 是將數據所有存儲於dict
的鍵中, 值字段閒置不用.
集合對象的內存佈局以下圖所示:
集合對象的API接口以下:
分類 | API名 | 功能 |
---|---|---|
建立接口 | robj *createSetObject(void) |
建立一個空集合對象. 底層編碼使用 HT , 即底層使用dict |
-- | robj *createIntsetObject(void) |
建立一個空集合對象. 底層編碼使用 INTSET , 即底層使用intset |
-- | robj *setTypeCreate(sds value) |
建立一個空集合對象. 注意入參雖然攜帶了一個數據, 但這個數據並不會存儲在集合中 這個數據只起到決定編碼方式的做用, 若這個數據是數值的字符串表達, 則底層編碼則爲 INTSET , 不然爲HT |
釋放接口 | void freeSetObject(robj *o) |
釋放集合對象. 若集合對象底層使用的是dict, 則調用 dictRelease 釋放這個dict若集合對象底層使用的是intset, 則直接釋放這個intset佔用的連續內存 |
編碼轉換接口 | void setTypeConvert(robj *setobj, int enc) |
轉換集合對象的內部編碼 雖然接口設計的好你能夠在底層編碼之間互相轉換, 但實際上這個接口的實現, 目前僅支持從 INTSET 轉換爲HT |
讀寫接口 | int setTypeAdd(robj *subject, sds value) |
向集合對象中寫入一個數據 |
-- | int setTypeRemove(robj *setobj, sds value) |
刪除集合對象中的一個數據 |
-- | int setTypeIsMember(robj *subject, sds value) |
判斷指定數據是否在集合對象中 |
-- | int setTypeRandomElement(robj *setobj, sds *sdsele, int64_t *llele) |
從集合對象中, 隨機選出一個數據, 將其數據經過出參返回. 若數據是數值類型, 則從 *llele 返回, 不然, 從*sdsele 返回.注意該接口若取得二進制數據, 則 *sdsele 是直接引用集合內的數據, 而不是拷貝一份 |
-- | unsigned long setTypeSize(const robj *subject) |
返回集合中數據的個數 |
迭代器接口 | setTypeIterator *setTypeInitIterator(robj *subject) |
建立一個集合對象迭代器 |
-- | void setTypeReleaseIterator(setTypeIterator *si) |
釋放集合對象迭代器 |
-- | int setTypeNext( setTypeIterator *si, sds *sdsele, int64_t *llele) |
讓集合迭代器步進一步, 並從出參中返回步進前迭代器所指向的數據. 若數據是數值類型, 則從 *llele 返回, 不然, 從*sdsele 返回注意該接口若取得二進制數據, 則 *sdsele 是直接引用集合內的數據, 而不是拷貝一份 |
-- | sds setTypeNextObject(setTypeIterator *si) |
讓集合迭代器步進一步, 並把步進前所指向的數據, 拷貝一份, 構形成一個新的SDS, 做爲返回值返回 |
有序集合的底層實現依然有兩種, 一種是使用ziplist
做爲底層實現, 另一種比較特殊, 底層使用了兩種數據結構: dict
與skiplist
. 前者對應的編碼值宏爲ZIPLIST
, 後者對應的編碼值宏爲SKIPLIST
使用ziplist
來實如今序集合很容易理解, 只須要在ziplist
這個數據結構的基礎上作好排序與去重就能夠了. 使用zskiplist
來實現有序集合也很容易理解, Redis中實現的這個跳躍表彷佛自然就是爲了實現有序集合對象而實現的, 那麼爲何還要輔助一個dict
實例呢? 咱們先看來有序集合對象在這兩種編碼方式下的內存佈局, 而後再作解釋:
首先是編碼爲ZIPLIST
時, 有序集合的內存佈局以下:
而後是編碼爲SKIPLIST
時, 有序集合的內存佈局以下:
在使用dict
與skiplist
實現有序集合時, 跳躍表負責按分數索引, 字典負責按數據索引. 跳躍表按分數來索引, 查找時間複雜度爲O(lgn). 字典按數據索引時, 查找時間複雜度爲O(1). 設想若是沒有字典, 若是想按數據查分數, 就必須進行遍歷. 兩套底層數據結構均只做爲索引使用, 即不直接持有數據自己. 數據被封裝在SDS中, 由跳躍表與字典共同持有. 而數據的分數則由跳躍表結點直接持有(double類型數據), 由字典間接持有.
有序集合對象的API接口以下:
分類 | API名 | 功能 |
---|---|---|
建立接口 | robj *createZsetObject(void) |
建立一個有序集合對象 默認內部編碼爲 SKIPLIST , 即內部使用zskiplist與dict來實現有序集合 |
-- | robj *createZsetZiplistObject(void) |
建立一個有序集合對象 指定內部編碼爲 ZIPLIST , 即內部使用ziplist來實現有序集合 |
釋放接口 | void freeZsetObject(robj *o) |
釋放一個有序集合對象 |
編碼轉換接口 | void zsetConvert(robj *zobj, int encoding) |
轉換有序集合對象的內部編碼 能夠在 ZIPLIST 與SKIPLIST 兩種編碼間轉換 |
-- | void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen) |
判斷當前有序集合對象是否有必要將底層編碼轉換爲ZIPLIST , 若是有必要, 就執行轉換 |
讀寫接口 | int zsetScore(robj *zobj, sds member, double *score) |
獲取有序集合中, 指定數據的得分. 數據由 member 參數攜帶, 經過二進制判等的方式匹配 |
-- | int zsetAdd( robj *zobj, double score, sds ele, int *flags, double *newscore) |
向有序集合中添加數據, 或更新已存在的數據的得分.flag 是一個in-out參數, 其做爲入參, 控制函數的具體行爲, 其做爲出參, 報告函數執行的結果.做爲入參時, *flags 的語義以下:ZADD_INCR 遞增已存在的數據的得分. 若是數據不存在, 則添加數據, 並設置得分. 且若newscore != NULL , 執行操做後, 數據的得分還會賦值給*newscore ZADD_NX 僅當數據不存在時, 執行添加數據並設置得分, 不然什麼也不作ZADD_XX 僅當數據存在時, 執行重置數據得分. 不然什麼也不作做爲出參, *flags 的語義以下:ZADD_NAN 數據的得分不是一個數值, 表明內部出現的異常ZADD_ADDED 新數據已經添加至集合中ZADD_UPDATED 數據的得分已經更新ZADD_NOP 函數什麼也沒作 |
-- | int zsetDel(robj *zobj, sds ele) |
從有序集合中移除一個數據 |
-- | long zsetRank(robj *zobj, sds ele, int reverse) |
獲取有序集合中, 指定數據的排名. 若 reverse==0 , 排名以得分升序排列. 不然排名以得分降序排列.第一個數據的排名爲0, 而不是1 |
-- | unsigned int zsetLength(const robj *zobj) |
獲取有序集合對象中存儲的數據個數 |