Redis 数据结构
2015-08-02 14:57
573 查看
redis适用场景:
1.取最新N个数据的操作(利用list最新放redis,其他数据从数据库直接读取)
2.排行榜(利用sorted set,将你要排序的值设置成 sorted set的score,将具体的数据设置成相应的value,只需增加数据,redis自动排序)
3.需要精准设定过期时间的应用(sorted set的score值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据)
4.计数器(命令原子性,你可以轻松地利用 incr,decr等命令来构建计数器系统。)
5.缓存
6.实时消息系统(发布与订阅)
7.队列系统(使用 list 可以构建队列系统,使用 sorted set 甚至可以构建有优先级的队列系统)
8.数据排重(Set)
9.实时系统
...
sds字符串的内存结构:
示例:
优点:
1. C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符‘\0’。因为C字符串并不记录自身的长度信息,所以获取长度需要遍历整个字符串数组,时间复杂度为O(N),而Redis的动态字符串则记录了其长度,时间复杂度降为O(1),因为字符串键底层使用SDS来实现,所以对一个字符串反复STRLEN,也不会造成性能影响。
2. C字符串容易造成缓冲区溢出,比如s1 = "reids"; s2 = "mongodb";在内存中s1和s2字符串紧挨着分配,而此时执行了strcat(s1, "good");good字符串数据将溢出到s2所在空间,导致s2保存的内容被意外更改了。而SDS字符串空间分配策略会杜绝发生缓冲区溢出可能性:当SDS的API要对SDS串进行修改时,API会先检查SDS空间是否满足修改需求,如果不满足的话API会自动将SDS空间扩展至执行修改所需的大小,然后才执行修改操作。
1)如果程序执行的是字符串增长操作,在执行操作之前,需要通过内存重分配扩展底层数组大小,如果忘记操作则会缓冲区溢出。
2)如果程序执行的是字符串缩短操作,在执行操作之前,需要通过内存重分配释放不再使用的内存空间,如果忘记操作则会内存泄露。
而SDS则使用free属性记录未使用空间解除C字符串长度和底层数组的长度之间的关联性。
空间预分配
用于优化字符串的增长操作,当SDS的API对一个SDS进行修改,并且需要对SDS的空间进行扩展的时候,程序不仅会为SDS分配必须的空间,还会为SDS分配额外的空间。
分配策略
+ 如果对SDS进行修改后,SDS的长度将小于1MB,程序分配和len属性同样大小的未使用空间;
+ 如果对SDS进行修改后,SDS的长度将大于等于1MB,程序分配1MB的未使用空间。
惰性空间释放
用于优化字符串的缩短操作,当SDS的API对一个SDS进行缩短时,程序并不立即使用内存重分配来回收多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待下次使用。
4. C字符串的字符必须符合某种编码,出字符串末尾外,字符串里面不能包含空字符,否则最先被读取的空字符被认为是字符串的结尾标识,这些限制使C字符串仅限于文本数据,而不能处理图片,音频,视频,压缩文件等数据。SDS的buf数组是字节数组,redis用它保存二进制安全数据,但该数组末尾也遵循C字符串空字符结尾惯例,从而方便使用C语言的库函数。
被用于列表键的底层实现,发布订阅功能,慢查询功能,监视器等功能。
链表的内存结构:
示例:
被用于Redis数据库的底层实现,哈希表的底层实现。
字典的内存结构:
示例:
哈希算法
当要将一个新的键值添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到指定索引上。redis目前使用Murmurhash2算法来计算哈希值(参考链接Murmurhash算法参考)。
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;
解决键冲突
1)当有两个以上的键被分配到哈希数组同一个索引上时,我们称发生了哈希冲突,redis的哈希表使用链地址法解决键冲突。通过dictEntry结构中的*next属性指针实现。
2)随着操作的不断执行,哈希表保存的键值对会逐渐的增多或减少,为了让哈希表的负载因子维持在一个合理的范围内,当保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩展和收缩,这个工作通过rehash来实现。
重哈希步骤:
+ 为字典的ht[1]哈希表分配空间,这个哈希表空间的大小取决于要执行的操作,以及ht[0]当前所包含的键值对数量。
- 如果执行的是扩展操作,那么ht[1] = 第一个大于等于 ht[0].used * 2 的 2^n;
- 如果执行的是收缩操作,那么ht[1] = 第一个大于等于 ht[0].used 的 2^n;
+ 将保存在ht[0] 上的所有键值对rehash到ht[1]上。
即重新执行计算哈希值和索引值,将键值对放到ht[1]指定索引位置上。
+ 当ht[0]上的值全部迁移完毕,释放ht[0],将ht[1]设置为ht[0],并在ht[1]上创建空白哈希表,为下一次rehash做准备。
重哈希举例:
字典当前存储如上图,此时进行扩展操作,ht[0].used的当前值为4, 4 * 2 = 8,而8恰好是第一个大于等于4*2的2的n次幂,所以程序会将ht[1]哈希表的大小设置为8。分配后如下图:
将ht[0]包含的键值对重哈希到ht[1]哈希表上后,如下图所示:
释放ht[0],将ht[1]设置为ht[0],然后为ht[1]创建一个空白哈希表,如下图所示。执行完毕后,哈希表的大小也由原来的4变成8了。
哈希表的扩展与收缩:
扩展操作执行条件:
1)当服务器目前没有执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1.
2)当服务器正在执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5.
负载因子load_factor = ht[0].used / ht[0].size;执行BGSAVE或BGREWRITEAOF命令时,服务器存在子进程,需要提高扩展操作所需的负载因子,从而尽量避免在rehash操作,减少不必要的内存写入。
收缩操作执行条件:
哈希表的负载因子小于0.1.
redis的rehash采用渐进式重哈希,避免一次性,集中式的重哈希操作导致的服务器阻塞。通过字典结构中的rehashidx字段实现。
渐进式重哈希步骤:
1) 当ht[1]哈希表分配空间后,将字典中的rehashidx字段设置为0.重哈希操作正式开始,
2) 在重哈希操作执行时,每次对字典进行CRUD操作时,除了执行相应操作外,会顺带将ht[0]上在reashidx索引上的所有键值对迁移到ht[1]上,当本次rehash完成时,程序将字典中的rehashidx属性的值加1,
3) 随着字典操作的不断执行,最终在某个时间点,ht[0]的所有键值对被迁移到ht[1]上,这是程序将字典中的rehashidx属性设为-1.表示rehash操作完成。
被用于有序集合的底层实现。
跳跃表的内存结构
示例:
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是各个节点保存的分值却可以是相同的,分值相同的节点将按照成员对象在字典中的大小来进行排序。
整数集合的内存结构
- 如果encoding属性的值是INTSET_ENC_INT16,那么contents属性就是一个int16_t类型的数组。[-32768 ~ 32767]
- 如果encoding属性的值是INTSET_ENC_INT32,那么contents属性就是一个int32_t类型的数组。[-2^32 ~ 2^32 - 1]
- 如果encoding属性的值是INTSET_ENC_INT64,那么contents属性就是一个int64_t类型的数组。
示例
整数集合升级
当新元素类型比整数集合现有元素的类型都要长时,整数集合先进行升级,然后才将新元素添加 到整数集合里。
1)根据新元素的类型,扩展整数集合底层contents空间大小,并为新元素分配空间;
2)将底层数组现有所有元素都转换成新元素类型,并将转换后的元素放入正确的位置,保证有序性;
3)将新元素添加到contents里。
每次新增元素都有可能引起升级,每次升级都要对底层所有元素进行类型转换,所以向整数集合添加新元素时间复杂度是O(N)。
优点:提升灵活性(避免C类型错误),节约内存
整数集合不支持降级
压缩列表是redis为节约内存开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。
可使用object encoding命令查看底层实现类型。
压缩列表内存结构
示例
entry结构:
prev_entry_bytes_length:
表示上个节点所占的字节数,即上个节点的长度,如果需要跳到上个节点,而已知道当前节点的首地址p,上个节点的首地址prev = p-prev_entry_bytes_length
根据编码方式的不同,prev_entry_bytes_length可能占1 bytes或5 bytes:
1 bytes:如果上个节点的长度小于254,那么就只需要1个字节;
5 bytes:如果上个节点的长度大于等于254,那么就将第一个字节设为254(1111 1110),然后接下来的4个字节保存实际的长度值;
encoding与length:
ziplist的编码类型分为字符串、整数
encoding的前两个比特位用来判断编码类型是字符串或整数:
00, 01, 10表示contents中保存着字符串
11表示contents中保存着整数
连锁更新
添加和删除节点都会造成连锁更新,造成性能影响,但是真正造成性能问题的几率非常低,首先需要恰好有多个连续、长度介于250~253byte之间的结点,才有可能引发,其次即使出现,更新的数量不多时也不会造成性能影响,压缩列表存储空间连续便于载入内存,能减少保存指针对内存的消耗,因此它被用于list,set等的基础实现,例如:
当满足其中任一条件时,redis才会将本身用ziplist存储的键转换成list结构。即节约内存,也提升性能。
可使用type命令查看对象类型。
对象的内存结构
raw字符串对象示例
embstr字符串对象示例
embstr编码是专门用于保存短字符串的优化解决方案,用于创建长度小于等于32字节的字符串值,raw编码创建字符串对象时,会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而embstr仅调用一次,分配连续空间来分别创建redisObject和sdshdr结构。内存释放亦是如此,而且embstr更容易载入内存。
ziplist存储的list对象示例
链表存储的list对象示例
ziplist存储的哈希对象示例
字典存储的哈希对象示例
inset编码的集合对象示例
hashtable编码的集合对象示例
ziplist存储的有序集合对象示例
[b]skiplist存储的有序集合对象示例[/b]
redis会共享0~9999的字符串对象,命令object idletime可以查看对象的空转时长。
Murmurhash算法参考
常见的hash算法
Redis数据结构
推荐阅读 >> 《redis设计与实现》
1.取最新N个数据的操作(利用list最新放redis,其他数据从数据库直接读取)
2.排行榜(利用sorted set,将你要排序的值设置成 sorted set的score,将具体的数据设置成相应的value,只需增加数据,redis自动排序)
3.需要精准设定过期时间的应用(sorted set的score值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据)
4.计数器(命令原子性,你可以轻松地利用 incr,decr等命令来构建计数器系统。)
5.缓存
6.实时消息系统(发布与订阅)
7.队列系统(使用 list 可以构建队列系统,使用 sorted set 甚至可以构建有优先级的队列系统)
8.数据排重(Set)
9.实时系统
...
动态字符串:
Redis没有使用传统的C字符串,构建简单的动态字符串(simple dynamic string,sds),并将其作为redis的默认字符串表示。sds字符串的内存结构:
/* * 保存字符串对象的结构 */ struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[]; };
示例:
优点:
1. C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符‘\0’。因为C字符串并不记录自身的长度信息,所以获取长度需要遍历整个字符串数组,时间复杂度为O(N),而Redis的动态字符串则记录了其长度,时间复杂度降为O(1),因为字符串键底层使用SDS来实现,所以对一个字符串反复STRLEN,也不会造成性能影响。
2. C字符串容易造成缓冲区溢出,比如s1 = "reids"; s2 = "mongodb";在内存中s1和s2字符串紧挨着分配,而此时执行了strcat(s1, "good");good字符串数据将溢出到s2所在空间,导致s2保存的内容被意外更改了。而SDS字符串空间分配策略会杜绝发生缓冲区溢出可能性:当SDS的API要对SDS串进行修改时,API会先检查SDS空间是否满足修改需求,如果不满足的话API会自动将SDS空间扩展至执行修改所需的大小,然后才执行修改操作。
/* * 将给定字符串 t 追加到 sds 的末尾 * * 返回值 * sds :追加成功返回新 sds ,失败返回 NULL * * 复杂度 * T = O(N) */ sds sdscat(sds s, const char *t) { return sdscatlen(s, t, strlen(t)); } sds sdscatlen(sds s, const void *t, size_t len) { struct sdshdr *sh; // 原有字符串长度 size_t curlen = sdslen(s); // 扩展 sds 空间 // T = O(N) s = sdsMakeRoomFor(s,len); // 内存不足?直接返回 if (s == NULL) return NULL; // 复制 t 中的内容到字符串后部 // T = O(N) sh = (void*) (s-(sizeof(struct sdshdr))); memcpy(s+curlen, t, len); // 更新属性 sh->len = curlen+len; sh->free = sh->free-len; // 添加新结尾符号 s[curlen+len] = '\0'; // 返回新 sds return s; }3. C字符串长度和底层数组的长度之间存在关联性,每次增长或缩短一个C字符串,程序都要对内存进行重分配操作:
1)如果程序执行的是字符串增长操作,在执行操作之前,需要通过内存重分配扩展底层数组大小,如果忘记操作则会缓冲区溢出。
2)如果程序执行的是字符串缩短操作,在执行操作之前,需要通过内存重分配释放不再使用的内存空间,如果忘记操作则会内存泄露。
而SDS则使用free属性记录未使用空间解除C字符串长度和底层数组的长度之间的关联性。
空间预分配
用于优化字符串的增长操作,当SDS的API对一个SDS进行修改,并且需要对SDS的空间进行扩展的时候,程序不仅会为SDS分配必须的空间,还会为SDS分配额外的空间。
分配策略
+ 如果对SDS进行修改后,SDS的长度将小于1MB,程序分配和len属性同样大小的未使用空间;
+ 如果对SDS进行修改后,SDS的长度将大于等于1MB,程序分配1MB的未使用空间。
惰性空间释放
用于优化字符串的缩短操作,当SDS的API对一个SDS进行缩短时,程序并不立即使用内存重分配来回收多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待下次使用。
4. C字符串的字符必须符合某种编码,出字符串末尾外,字符串里面不能包含空字符,否则最先被读取的空字符被认为是字符串的结尾标识,这些限制使C字符串仅限于文本数据,而不能处理图片,音频,视频,压缩文件等数据。SDS的buf数组是字节数组,redis用它保存二进制安全数据,但该数组末尾也遵循C字符串空字符结尾惯例,从而方便使用C语言的库函数。
链表:
链表提供高效的节点重排,顺序性访问,灵活增删节点特点。被用于列表键的底层实现,发布订阅功能,慢查询功能,监视器等功能。
链表的内存结构:
/* * 双端链表节点 */ typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value; } listNode;
/* * 双端链表结构 */ 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;
示例:
字典:
字典是一种用于保存键值对的抽象数据结构。被用于Redis数据库的底层实现,哈希表的底层实现。
字典的内存结构:
/* * 哈希表 * * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。 */ typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht;
/* * 哈希表节点 */ typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next; } dictEntry;
/* * 字典 */ typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */ } dict;
示例:
哈希算法
当要将一个新的键值添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到指定索引上。redis目前使用Murmurhash2算法来计算哈希值(参考链接Murmurhash算法参考)。
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;
解决键冲突
1)当有两个以上的键被分配到哈希数组同一个索引上时,我们称发生了哈希冲突,redis的哈希表使用链地址法解决键冲突。通过dictEntry结构中的*next属性指针实现。
2)随着操作的不断执行,哈希表保存的键值对会逐渐的增多或减少,为了让哈希表的负载因子维持在一个合理的范围内,当保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩展和收缩,这个工作通过rehash来实现。
重哈希步骤:
+ 为字典的ht[1]哈希表分配空间,这个哈希表空间的大小取决于要执行的操作,以及ht[0]当前所包含的键值对数量。
- 如果执行的是扩展操作,那么ht[1] = 第一个大于等于 ht[0].used * 2 的 2^n;
- 如果执行的是收缩操作,那么ht[1] = 第一个大于等于 ht[0].used 的 2^n;
+ 将保存在ht[0] 上的所有键值对rehash到ht[1]上。
即重新执行计算哈希值和索引值,将键值对放到ht[1]指定索引位置上。
+ 当ht[0]上的值全部迁移完毕,释放ht[0],将ht[1]设置为ht[0],并在ht[1]上创建空白哈希表,为下一次rehash做准备。
重哈希举例:
字典当前存储如上图,此时进行扩展操作,ht[0].used的当前值为4, 4 * 2 = 8,而8恰好是第一个大于等于4*2的2的n次幂,所以程序会将ht[1]哈希表的大小设置为8。分配后如下图:
将ht[0]包含的键值对重哈希到ht[1]哈希表上后,如下图所示:
释放ht[0],将ht[1]设置为ht[0],然后为ht[1]创建一个空白哈希表,如下图所示。执行完毕后,哈希表的大小也由原来的4变成8了。
哈希表的扩展与收缩:
扩展操作执行条件:
1)当服务器目前没有执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1.
2)当服务器正在执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5.
负载因子load_factor = ht[0].used / ht[0].size;执行BGSAVE或BGREWRITEAOF命令时,服务器存在子进程,需要提高扩展操作所需的负载因子,从而尽量避免在rehash操作,减少不必要的内存写入。
收缩操作执行条件:
哈希表的负载因子小于0.1.
redis的rehash采用渐进式重哈希,避免一次性,集中式的重哈希操作导致的服务器阻塞。通过字典结构中的rehashidx字段实现。
渐进式重哈希步骤:
1) 当ht[1]哈希表分配空间后,将字典中的rehashidx字段设置为0.重哈希操作正式开始,
2) 在重哈希操作执行时,每次对字典进行CRUD操作时,除了执行相应操作外,会顺带将ht[0]上在reashidx索引上的所有键值对迁移到ht[1]上,当本次rehash完成时,程序将字典中的rehashidx属性的值加1,
3) 随着字典操作的不断执行,最终在某个时间点,ht[0]的所有键值对被迁移到ht[1]上,这是程序将字典中的rehashidx属性设为-1.表示rehash操作完成。
跳跃表
跳跃表是一种有序的数据结构,通过在每个节点中维护多个指向其他节点的指针,从而实现快速访问。被用于有序集合的底层实现。
跳跃表的内存结构
/* * 跳跃表节点 */ typedef struct zskiplistNode { // 成员对象 robj *obj; // 分值 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;
示例:
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是各个节点保存的分值却可以是相同的,分值相同的节点将按照成员对象在字典中的大小来进行排序。
整数集合
当一个集合中只包含整数值的元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合键的底层实现。整数集合的内存结构
typedef struct intset { // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组(有序不重复) int8_t contents[]; } intset;虽然将contents属性声明为int8_t类型的数组,但是实际上并不保存int8_t类型的值,真正类型取决于encoding的值。
- 如果encoding属性的值是INTSET_ENC_INT16,那么contents属性就是一个int16_t类型的数组。[-32768 ~ 32767]
- 如果encoding属性的值是INTSET_ENC_INT32,那么contents属性就是一个int32_t类型的数组。[-2^32 ~ 2^32 - 1]
- 如果encoding属性的值是INTSET_ENC_INT64,那么contents属性就是一个int64_t类型的数组。
示例
整数集合升级
当新元素类型比整数集合现有元素的类型都要长时,整数集合先进行升级,然后才将新元素添加 到整数集合里。
1)根据新元素的类型,扩展整数集合底层contents空间大小,并为新元素分配空间;
2)将底层数组现有所有元素都转换成新元素类型,并将转换后的元素放入正确的位置,保证有序性;
3)将新元素添加到contents里。
每次新增元素都有可能引起升级,每次升级都要对底层所有元素进行类型转换,所以向整数集合添加新元素时间复杂度是O(N)。
优点:提升灵活性(避免C类型错误),节约内存
整数集合不支持降级
压缩列表
当一个列表只包含少量的列表项,并且每个列表项要么是小整数值,要么是长度比较短的字符串,那么redis就使用压缩列表作列表的底层实现,哈希键亦是如此。压缩列表是redis为节约内存开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。
可使用object encoding命令查看底层实现类型。
压缩列表内存结构
/* 空白 ziplist 示例图 area |<---- ziplist header ---->|<-- end -->| size 4 bytes 4 bytes 2 bytes 1 byte +---------+--------+-------+-----------+ component | zlbytes | zltail | zllen | zlend | | | | | | value | 1011 | 1010 | 0 | 1111 1111 | +---------+--------+-------+-----------+ ^ | ZIPLIST_ENTRY_HEAD & address ZIPLIST_ENTRY_TAIL & ZIPLIST_ENTRY_END 非空 ziplist 示例图 area |<---- ziplist header ---->|<----------- entries ------------->|<-end->| size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte +---------+--------+-------+--------+--------+--------+--------+-------+ component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend | +---------+--------+-------+--------+--------+--------+--------+-------+ ^ ^ ^ address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END | ZIPLIST_ENTRY_TAIL */
示例
entry结构:
prev_entry_bytes_length:
表示上个节点所占的字节数,即上个节点的长度,如果需要跳到上个节点,而已知道当前节点的首地址p,上个节点的首地址prev = p-prev_entry_bytes_length
根据编码方式的不同,prev_entry_bytes_length可能占1 bytes或5 bytes:
1 bytes:如果上个节点的长度小于254,那么就只需要1个字节;
5 bytes:如果上个节点的长度大于等于254,那么就将第一个字节设为254(1111 1110),然后接下来的4个字节保存实际的长度值;
encoding与length:
ziplist的编码类型分为字符串、整数
encoding的前两个比特位用来判断编码类型是字符串或整数:
00, 01, 10表示contents中保存着字符串
11表示contents中保存着整数
连锁更新
添加和删除节点都会造成连锁更新,造成性能影响,但是真正造成性能问题的几率非常低,首先需要恰好有多个连续、长度介于250~253byte之间的结点,才有可能引发,其次即使出现,更新的数量不多时也不会造成性能影响,压缩列表存储空间连续便于载入内存,能减少保存指针对内存的消耗,因此它被用于list,set等的基础实现,例如:
当满足其中任一条件时,redis才会将本身用ziplist存储的键转换成list结构。即节约内存,也提升性能。
对象
redis使用对象来表示数据库中的键和值,每次我们在数据库中创建一个新的键值对的时候,我们至少要创建两个对象。一个是键对象,一个是值对象。可使用type命令查看对象类型。
对象的内存结构
typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 对象最后一次被访问的时间 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ // 引用计数(引用计数实现内存回收机制) int refcount; // 指向实际值的指针 void *ptr; } robj;
raw字符串对象示例
embstr字符串对象示例
embstr编码是专门用于保存短字符串的优化解决方案,用于创建长度小于等于32字节的字符串值,raw编码创建字符串对象时,会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而embstr仅调用一次,分配连续空间来分别创建redisObject和sdshdr结构。内存释放亦是如此,而且embstr更容易载入内存。
ziplist存储的list对象示例
链表存储的list对象示例
ziplist存储的哈希对象示例
字典存储的哈希对象示例
inset编码的集合对象示例
hashtable编码的集合对象示例
#编码转换条件 intset-> dict intset:集合元素保存的所有元素都是整数,保存的元素数量不超过512个。 set-max-ziplist-entries 512 #最大的元素个数
ziplist存储的有序集合对象示例
[b]skiplist存储的有序集合对象示例[/b]
/* * 有序集合 */ typedef struct zset { // 字典,键为成员,值为分值 // 用于支持 O(1) 复杂度的按成员取分值操作 dict *dict; // 跳跃表,按分值排序成员 // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作 // 以及范围操作 zskiplist *zsl; } zset;
redis会共享0~9999的字符串对象,命令object idletime可以查看对象的空转时长。
Murmurhash算法参考
常见的hash算法
Redis数据结构
推荐阅读 >> 《redis设计与实现》