Redis数据结构的天然优势:
Redis有五大数据类型:string,list,set,hashmap,sorted set,而memcache只有key-value,也就是说,要想使用memcache 存储复杂数据,那么对于数据的解构需要放在客户端来实现,memcache只提供数据的存储.而redis由于自带数据类型,对于数据的解构在redis server处已经为你做好了.
Redis五大数据类型的底层实现
简单动态字符串(SDS)
任何数据类型的key都是这种SDS,而value如果是string类型,那么也是SDS实现的,value如果是list类型,里面push进去的字符串也是SDS
SDS结构体定义
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
和C语言的字符串设计一样,SDS字符串也是以’\0’结尾的,这是为了更好的直接调用C函数库,但是这个额外的字节不被计入len
那么,SDS和C字符串有什么区别呢?
1.C字符串获得自身长度的时间复杂度为O(n),而SDS可以通过len属性直接获得,当然,len的更新和维护需要在具体的API操作中进行
2.SDS不会发生缓冲区的溢出.当进行字符串拼接时,传统的C函数
会假定dest已经有足够的空间,从而直接把src拼接到dest的后面.一旦dest分配的内存不足,就会把dest后面内存保留的内容覆盖掉,而SDS提供的API sdscat则会在拼接之前执行检查,如果空间不足会进行扩容操作
3.SDS减少了修改字符串带来的内存重分配次数.在C语言字符串中,每次对字符串进行拼接或截取操作,都需要重新开辟内存否则会分别造成内存覆盖或内存泄露.而这种内存重分配是十分耗时的系统调用,因此redis的SDS的buf的实际长度是比有效长度大的,这样可以减少扩容的次数.buf的扩容策略为:
4.SDS是二进制安全的.C语言的字符串使用固定编码ASCII,并且除了字符串末尾外,其余部分不能包含空字符,否则会被认为是结尾,这些限制使得C字符串只能保存文本数据而不能保存图像音频等数据.而所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它
被读取时就是什么样。SDS的len属性不会因为遇到空字符就判断字符串结束
链表:
由于C语言并没有提供内置的链表,因此Redis实现了链表数据结构
typedef struct listNode {
//前置节点
struct listNode * prev;
//后置节点
struct listNode * next;
//节点的值
void * value;
}listNode;
typedef struct list {
//表头节点
listNode * head;
//表尾节点
listNode * tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void (*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
} list;
Redis的链表是多态的:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
字典
字典数据结构被用于redis的许多场景,比如在数据库中创建一个键为"msg",值为"hello world"的键值对时,这个键值对就是保存在代表数据库的字典里面的。除了用来表示数据库之外,字典也是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对
typedef struct dictEntry {
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
Redis 的哈希表整体数据结构如下
typedef struct dict {
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为-1
in trehashidx;
} dict;
之所以会有两个dictht哈希表,是因为其中一个被用作rehash,redis的哈希表采用渐进式rehash。以下是哈希表渐进式rehash的详细步骤:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置
为0,表示rehash工作正式开始。 - 在rehash进行期间,每次对字典执行添加、删除、查找或者更新
操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在
rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程
序将rehashidx属性的值增一。 - 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有
键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表
示rehash操作已完成。渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对
所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,
从而避免了集中式rehash而带来的庞大计算量。
跳表
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序
集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
关于跳表的实现请参考 跳表论文解读
redis的跳表node中,以有序集合的score作为遍历依据的key,并且额外存储了一个obj指针,指向一个SDS,作为有序集合的值
整数集合
整数集合(intset)是set的底层实现之一,当集合只包含整数数值的元素时,并且集合中元素数量不多,redis就使用整数集合作为它的底层实现
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
encoding属性指定了集合中整数的最大长度,比如32位或64位,一旦一个更高长度的整数被加入到集合中,集合就会进行升级操作,将底层数组现有的所有元素都转换成与新元素相同的类型。contents是一个有序数组,这个数组以有序、无重复的方式保存集合元素。里面整数的类型取决于encoding属性。因为整数集合可以通过自动升级底层数组来适应新元素,所以我们可以随意地将int16_t、int32_t或者int64_t类型的整数添加到集合中,而不必担心出现类型错误,这种做法非常灵活。
压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,它通过列表节点的额外信息最大程度的利用了内存。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
关于列表节点的具体内容如下所示
previous_entry_length记录了上一个节点的长度,previous_entry_length属性的长度可以是1字节或者5
字节:如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制值254),而之后的四个字节则用于保存前一节点的长度可以用它进行表尾到表头的遍历
encoding属性记录了content属性所保存数据的类型以及长度
content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
Redis的对象系统
Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。举个例子,以下SET命令在数据库中创建了一个新的键值对,其中键值对的键是一个包含了字符串值"msg"的对象,而键值对的值则是一个包含了字符串值"hello world"的对象。Redis中的每个对象都由一个redisObject结构表示
typedef struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
类型:
编码: