redis是一种基于key-value的内存数据库。所谓kv存储或者说kv数据库,就是说里面数据都是一对一对的存储的,其中key是唯一的一个索引。这种结构一般是基于哈希表,效率极高,查找复杂度为O(1)。
不过hashmap也不是万能的,随着数据量的增多,造成的hash冲突也就越严重,复杂度增加,redis需要进行rehash,对hashmap进行扩容。如图所示,redis还支持丰富的数据结构类型,那么这些是怎么做到的呢?它们在内存中是怎么存储的呢?
1. redis中的对象
在redis中每一个对象都对应一个redisObject对象,如下,这里重点讲type、encoding和ptr
127.0.0.1:6379> set key "hello world"
OK
127.0.0.1:6379> get key
"hello world"
127.0.0.1:6379> type key
string
127.0.0.1:6379> RPUSH scores 87 89 97
(integer) 3
127.0.0.1:6379> type scores
list
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;
1.1 类型
对象的类型有以下五种,分别代表字符串、列表、集合、有序集合和哈希对象。
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
一个数据库键为什么类型,可以通过TYPE命令查看
127.0.0.1:6379> set key "hello world"
OK
127.0.0.1:6379> get key
"hello world"
127.0.0.1:6379> type key
string
127.0.0.1:6379> RPUSH scores 87 89 97
(integer) 3
127.0.0.1:6379> type scores
list
1.2 编码
对象的ptr指针指向对象的底层数据结构,对应encoding属性。如下
/* 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 OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
相应的查看命令 object encoding key , 这些编码并非已成不边,比如int编码和embstr编码的字符串对象,有可能会变为raw编码的字符串对象
127.0.0.1:6379> set hi "hello"
OK
127.0.0.1:6379> object encoding hi
"embstr"
127.0.0.1:6379> sadd numbers 87 69 90
(integer) 3
127.0.0.1:6379> object encoding numbes
(nil)
127.0.0.1:6379> object encoding numbers
"intset"
1.3 对象指针
如下图所示,为RAW编码的字符串对象
下图为ziplist编码的列表对象,同理其他编码也都是用这种结构
1.4 引用计数实现内存回收
redis的对象系统中使用引用计数来实现内存回收。当引用计数为0时,释放对象回收内存。
1.5 lru
lru这个属性表示对象最后一次被访问的时间到当前时间的间隔。相应命令如下
127.0.0.1:6379> OBJECT idletime key
(integer) 1942
如果设置了maxmemory选项,回收算法为volatile-lru或者allkeys-lru的话,当key数量超出上限,lru时长高的那部分将会被回收。
2. 字典
Redis中采用了字典这一结构来管理键值对,用它作为数据库的底层实现,同样也是哈希键的底层实现。下面从哈希表、哈希表节点和字典的三个重要的数据结构分来分析下。
2.1 哈希表
/* 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;
table: 哈希表数组,size:哈希表数组大小,sizemask:哈希表大小掩码,用来确定新键的位置,used:哈希表已有节点的数量。
2.2 哈希表节点
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
key保存键,v保存值,next指向另一个节点,用来解决键冲突。可以看到v是一个联合体,对应的值可以是uint64_t\int64_t\double这些基础类型,也可以是指针,间接指向对象。
2.3 字典
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);// 计算hash值的函数
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;
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;
type是一个dictType的指针。dictType中保存了一组用操作键值对的函数,privdata则是这些函数的可选参数。这类似于C++中的虚函数表指针(更确切的讲是虚表指针类似于它),以此来实现多态。
ht是一个包含两个dictht的数组,字典正常情况下只使用ht[0], ht[1]只会在rehash时使用, rehashidx则记录了目前的进度,没有rehash,则默认为-1。下图为普通状态下的字典、hash表和哈希表节点。
参考:
[0] https://hazelcast.com/glossary/key-value-store/
[1] redis设计与实现
[2] http://www.redis.cn/documentation.html