和memcached相比,redis提供更丰富的数据结构,支持基于列表、集合、哈希表等多种数据结构。这些命令是通过6种底层数据结构来实现的,这6种数据结构是:
- 简单动态字符串(SDS)
- 链表
- 字典
- 跳跃表
- 整数集合
- 压缩列表
SDS
用来保存redis数据库中的字符串,它是一个C结构体,代码如下:
struct{
int len;
int free;
char buf[];
}
结构体中的三个属性分别用来表示已使用的字节数量、未使用的字节数量、字节数组,redis之所以设计这个结构而不使用C语言中的字符串,是因为SDS结构相比C语言中的字符串有如下优点:
记录了字符串的长度,执行strlen操作时,复杂度为O(1),而普通字符串为O(n)。
SDS采用预先分配和惰性释放的优化策略,减少重新分配空间的次数,其中有free字段记录未使用空间字节数。
二进制安全,除了可以存储字符还可以存储任意二进制。
SDS操作函数会自动检查空间是否足够并且空间不足时自动扩展空间,从而杜绝缓冲区溢出。
链表
用来存储链表结构数据,它被用来实现SADD等命令,结构体代码如下:
typedef struct listNode{
struct listNode* prev;
struct listNode* next;
void* value;
}listNode
字典
字典在Redis中应用非常广发,Redis数据库本身就是使用字典来作为底层实现的,比如我们执行命令:SET msg "hello world",redis就会在数据库中创建一个键为msg值为"hello world"的字典。
字典在实现上包含两个hash表,常规状态下,只有第一个hash表存储数据,当发生rehash时,会给第二个hash表分配空间,并且把第一个hash表中的数据拷贝到第二个hash表中。如果hash表中的数据量比较大时,会采用渐进式rehash,分多次完成,字典中有个rehashindex字段记录rehash进度(如果当前未进行rehash值为-1),这样的话在字典中查询数据时,可能会查找两个hash表,先查第一个表,如果查不到再查第二个,但是新增的数据都会添加到第二个hash表中。当服务器正在执行BGSAVE、BGREWRITEAOF等耗CPU的命令时,会尽量避免rehash操作。
跳跃表
redis实现了一个跳跃表结构,它被用来实现有序集合即ZADD等命令。
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj* *obj;
}zskiplistNode;
typedef struct zskiplist{
//头、尾节点指针
struct zskiplistNode *header, *tail;
//节点数量
unsigned long length;
// 表中层最大的结点的层数
int level
}
整数集合
当集合中只包含整数时,并且集合中的元素数量不多时,redis会采用整数集合当做底层实现。
typedef struct intset{
uint32_t encoding;
uint32_t length;
int8_t contents[];
}intset;
其中contents中的元素有可能是16、32、64字节,redis只会分配合适的内存,比如当前添加的时16位的整数,那么只会给contents元素分配16字节,当后续往数组中再次添加16字节以上的整数时,会给该数组升级重新分配32/64字节内存,只能给数组升级不能降级。
压缩列表
当列表、集合、hash表、有序集合等结构中的元素数量小于某个阀值,其中的所有元素都小于某个阀值时,redis会采用压缩列表当做这些结构的底层实现,旨在节约内存,压缩列表包含了下列各个组成部分:- zlbytes 4字节,纪录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或计算zlend时使用。
- zltail 4字节,纪录压缩列表表尾节点距离压缩列表的起始地址有多少个字节,通过这个偏移量程序可以无须遍历整个压缩列表就可以确定表尾节点的地址。
- zllen 2字节,纪录压缩列表包含的结点数量,当属性值等于UINT16_MAX时,结点的真实数量需要遍历整个压缩列表才能计算得出。
- entryX,列表节点,长度由结点中的内存结点,结点中有个字段previous_entry_length记录了前一个几点的长度,当结点的长度在254左右时,对某结点的修改可能会引发所有结点的连锁更新。
压缩列表会节约内存,但是对结构元素的访问修改时会损失一定的性能,所以当元素数量或者元素大小超过了某个阀值之后会对值对象进行编码转换重新分配内存。
对象
Redis并没有直接使用上面那些数据结构来实现键值对数据库,而是在这些数据接口的基础上创建了一个对象系统,每次当我们在Redis数据库中新创建一个键值对时,至少会创建两个对象,一个键对象,一个值对象。每个对象都包含指定的类型和编码。
对象类型
对象的type属性设置,可以通过TYPE命令来输出对象的类型,对象有下面几种类型:
- 字符串对象,类型常量为REDIS_STRING,输出值string。
- 列表对象,类型常量为REDIS_LIST,输出值list。
- 哈希对象,类型常量为REDIS_HASH,输出值hash。
- 集合对象,类型常量为REDIS_SET,输出值hash。
- 有序集合表对象,类型常量为REDIS_ZSET,输出值zset。
对象编码
对象的encoding属性设置,可以通过OBJECT ENCODING命令查看对象的编码,对象有下面几种编码:
- long类型整数,REDIS_ENCODING_INT,输出int。
- embstr编码的SDS,REDIS_ENCODING_EMBSTR,输出embstr。embstr编码是专门用于保存短字符串的一种优化编码方式,跟正常的字符编码相比,字符编码会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中一次包含redisObject和sdshdr两个结构。
- SDS,REDIS_ENCODING_RAW,输出raw。
- 字典,REDIS_ENCODING_HT,输出hashtable。
- 双端链表,REDIS_ENCODING_LINKEDLIST,输出linkedlist。
- 压缩列表,REDIS_ENCODING_ZIPLIST,输出ziplist。
- 整数集合REDIS_ENCODING_INISET,输出intset。
- 跳跃表和字典,REDIS_ENCODING_SKIPLIST,输出skiplist。
每种类型的对象至少使用了两种不同的编码,在不同的条件下redis会对对象进行编码转换,比如如果对象保存的全部都是整数,那么他的编码是int,但是当执行了一些命令之后对象中存储的已经不全部是整数了,那么会把该对象的编码从int变为raw,其它类型的对象同理。