目录
动态字符串
sds
redis用c语言编写,自己构建了一个名为简单字符串(simple dynamic string,SDS)的抽象类型,作为redis的默认字符串表示.
如:
1 | redis> SET msg "hello world" |
redis在数据库中创建的过程.
- 键值对的
键
是一个字符串对象.对象底层实现保存则字符串msg
的SDS. - 键值对的
值
也是一个字符串对象,也是SDS.
除了用作保存数据库中的字符串,SDS还被用作缓冲区(BUFFER),AOF中的AOF缓冲区.客户端状态的输入缓冲区,都是SDS实现
sds定义
1 | struct sdshdr { |
sds与c字符串的比较
c字符串使用n+1的字符数组表示长为n的字符串,字符数组的最后一个元素总是空字符\0.并不能满足redis对字符串在安全性,效率性及功能方面的需求
- c字符串不记录自身长度.获取长度的复杂度为O(N).
- 杜绝了缓冲区溢出,stract函数可以拼接字符串,由于c字符串不保存长度,stract在拼接的时候默认用户已经为dest分配了足够的内存.sds的拼接回自动将sds的空间扩展至执行所需的大小.
char *stract(char *dest,const char *src);
- c字符串不记录自身长度,每次增长或缩短,总要对这个c字符串的数据进行内存重新分配.
- sds的空间预分配
- 如果对sds进行修改,sds长度将小于1mb,那么程序分配和len属性同样大小的未使用空间.sds的len和free相等.
- 如果分配的长度大于等于1mb,那么会分配1mb未使用的空间
- 惰性空间释放
当sds的api需要缩短sds保存的字符串,程序不会立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性
- sds的空间预分配
- 二进制安全.c字符串除了字符串的末尾之外,不能包含空字符,不然会被程序读入的空字符误认为字符串结尾.redis会以处理二进制的方式来处理sds存放在buf里的数据.不会对其中的数据进行任何限制,过滤.
redis不是用buf来保存字符,而是用来保存一系列二进制数据
- 兼容部分c字符串函数.
链表
链表提供了高效的节点重排能力,顺序性节点访问方式(连续内存?),可以通过增删节点来灵活地调整链表长度.
链表在redis中的应用非常广泛.列表键的底层实现之一就是链表.当一个列表键包含了数量比较多的元素或者表中包含的元素都是比较长的字符串,redis就会用链表作为底层实现.
除了列表键,发布订阅,慢查询,监视器也用到了链表.
链表和链表节点的实现.
1 | typedef struct listNode{ |
redis链表特点:
- 双端
- 无环
- 带头指针尾指针
- 带长度
- 多态,链表节点使用void*指针保存节点值,通过list的dup,free,match属性为节点值设置类型特定函数.
字典
字典,符号表(symbol table),关联数组(associative array),映射(map).
字典在redis数据库中应用相当广泛:
- redis数据库就是用字典作为底层实现. 在数据库中创建一个key为”msg”,值为”hello world”的键值对,这个键值对就是保存在代表数据库的字典里面的.
1
2redis> SET msg "hello world"
OK - 字典也是hash键的底层实现之一,当一个hash键包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,redis就会用字典作为hash键的底层实现.
字典的实现
哈希表
1 | typedef struct dictht { |
哈希表节点
1 | typeof struct dictEntry{ |
字典
1 | typedef struct dict { |
哈希算法
MurmurHash算法
冲突解决
拉链法解决得冲突
rehash
负载因子计算: load_factor = ht[0].used/ht[0].size
扩展条件
- 没有执行bgsave或bgrewriteaof,当负载椅子大于等于1进行扩展
- 执行bugszve活bgrewriteaof,
跳表
通过在每个节点维持多个指向其他节点得指针,从而快速访问.
支持平均O(logN),最坏O(N)复杂度得节点查找.有序集合键得底层实现.另一个地方在集群节点中用作内部数据结构.
整数集合
集合键的底层实现之一.
压缩列表
ziplist,列表键和hash键底层实现之一
为了节约内存开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构.一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值.
当一个列表只包含少量列表项, 并且每个列表项要么就是小整数值,要么就是长度短的字符串,redis就会使用ziplist作为列表底层实现.
主要结构
节点的构成
previous_entry_length | encoding | length |
---|
连锁更新
每个previous_entry_length属性记录了前一个节点的长度,如果前一节点长度小于254,那么,用1字节保存即可,如果大于等于254,则用5字节.
如果将一个大于254的节点设置为压缩列表的表头节点.而e1(原先第一个节点)的previous_entry_length为1,则必然不够放,需要扩充成5字节长度.那么e1的长度从原来的长度(如250-253)则变为254-257.那么e2也需要扩展…这种特殊情况造成的多次扩展空间操作被称为连锁更新
对象
redis基于以上几种数据结构创建了对象系统,这个系统包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种使用到的结构.
redis的对象系统实现了基于引用计数的内存回收机制.不再使用某个对象的时候,这个对所占用的内存就会自动释放.redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存.
对象类型与编码
redis使用对象来表似数据库的键和值,每次在redis的库中创建一个新的键值对时,至少会创建两个对象,一个用作键值对的键(对象),另一个对象用作值(对象).
1 | typedef struct redisObject { |
类型常量 | 对象名 | type输出 |
---|---|---|
redis_string | 字符串 | string |
redis_list | 列表 | list |
redis_hash | 哈希对象 | hash |
redis_set | 集合对象 | set |
redis_zset | 有序集合对象 | zset |
1 | > set msg "helloworld" |
编码与底层实现
对象的ptr指针指向对象的底层实现数据结构,这些数据结构由对象的encoding属性决定.encoding属性记录了对象所用的编码,也即说这个对象使用了什么数据结构作为对象的底层实现
编码常量 | 编码所对应的底层数据结构 |
---|---|
redis_encoding_int | long类型的整数 |
redis_encoding_embstr | embstr编码的简单动态字符串 |
redis_encoding_raw | 简单动态字符串 |
redis_encoding_ht | 字典 |
redis_encoding_linkedlist | 双端链表 |
redis_encoding_ziplist | 压缩列表 |
redis_ecndoing_intset | 整数集合 |
redis_encoding_skiplist | 跳跃表和字典 |
每种类型的对象都至少使用了两种不同编码,部分对象的编码:
1 | > object encoding msg |
object encoding 命令输出
|对象使用的底层数据结构|编码常量|object encoding命令输出|
|:——————|:——|:——————–|
|整数|redis_encoding_int|int|
|embstr编码的简单动态字符串(sds)|redis_encoding_embstr|embstr|
|简单动态字符串|redis_encoding_raw|raw|
|字典|redis_encoding_ht|hashtable|
|双端链表|redis_encoding_linkedList|linkedlist|
字符串对象
字符串对象的编码可以是 int,raw或embstr
int
1 | redis> set numer 10086 |
raw
如果字符串对象保存的一个字符串值长度大于32字节,那么将用一个简单的动态字符串(SDS)保存这个字符串值.将这个对象编码设置为raw.
1 | redis> set numer 10086 |
embstr
如果字符串的值长度小于等于32字节,那么字符串对象使用embstr编码的方式保存这个字符串值.
embstr时专门用来保存短字符串的一种优化编码方式.和raw编码一样,都是用redisObject结构和sdshdr结构表似字符串对象.
raw会调用两次内存分配函数创建redisObject和sdshdr结构,而embstr调用一次内存分配函数,分配一块连续的空间
embstr保存短字符串值得好处:
- embstr编码创建字符串对象得内存分配从raw编码得两次降低为1次
- 释放embstr编码得字符串对象也只需要释放一次
- 连续内存比raw编码字符串对象更好利用缓存优势
编码的转换
int编码和embstr编码的对象在条件满足情况下,会被转为raw编码的字符串对象.
1 | > set number 10086 |
列表对象
快速列表(3.2之后)
在 Redis3.2 之前,linkedlist 和 ziplist 两种编码可以进选择切换,如果需要列表使用 ziplist 编码进行存储,则必须满足以下两个条件:
- 列表对象保存的所有字符串元素的长度都小于 64 字节。(list-max-ziplist-value)
- 列表对象保存的元素数量小于 512 个。(list-max-ziplist-entries)
redis 3.2版本之后列表只用quicklist了,时linked list 与 ziplist的结合.
1 | typedef struct quicklistNode { |
quicklist 就是综合平衡考虑了 linkedlist 容易产生空间碎片的问题和 ziplist 的读写性能两个维度而设计出来的一种数据结构。使用 quicklist 需要注意以下 2 点:
- 如果 ziplist 中的 entry 个数过少,最极端情况就是只有 1 个 entry 的压缩列表,那么此时 quicklist 就相当于退化成了一个普通的 linkedlist
- 如果 ziplist 中的 entry 过多,那么也会导致一次性需要申请的内存空间过大(ziplist 空间是连续的),而且因为 ziplist 本身的就是以时间换空间,所以会过多 entry 也会影响到列表对象的读写性能。
ziplist 中的 entry 个数可以通过参数 list-max-ziplist-size 来控制:list-max-ziplist-size 1
,可以设置为负数(-1~-5)
- -1:每个 ziplist 最多只能为 4KB
- -2:每个 ziplist 最多只能为 8KB
- -3:每个 ziplist 最多只能为 16KB
- -4:每个 ziplist 最多只能为 32KB
- -5:每个 ziplist 最多只能为 64KB
hash对象
hash对象的编码可以是ziplist或者hashtable
编码转换
- hash对象保存的所有键值对的键和值的字符串长度小于64字节
- 哈希对象保存的键值对个数小于512.
不能满足这两个条件的hash对象需要使用hashtable编码.
hash-max-ziplist-value
hash-max-ziplist-engries
1 | > hset book name "Mastering c++ in 21 days" |
集合对象
集合对象的编码可以是intset或者hashtable,有点类似java里得hashmap也是用map做底层…
字典的每个key是一个字符串对象,值为null.
编码转换
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512
不能满足的集合对象需要使用hashtable编码.
有序结合对象
编码可以是ziplist或者skiplist
- ziplist:每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个保存元素的分值(score).压缩列表内的结合元素按分值从小到大进行排序.
1 |
|
zlbytes | zltail | zllen | “banana” | 5.0 | “cherry” | 6.0 | “apple” | 8.5 | zlend |
---|
跳表编码的有序集合对象使用zset结构作为底层实现.一个zset结构同时包含一个字典和一个跳跃表:
1 | typedef struct zset { |
理论上有序集合可以单独使用字典或者跳表
如果单独使用字典实现有序集合,那么虽然查找成员的复杂度为O(1),但是字典是无序的,所以每次执行范围操作就会对所有元素进行排序,至少需要O(NlogN),以及额外的O(N)的内存空间.
如果使用跳表来实现有序集合,那么查找分值的操作将从O(1)上升为O(LogN).
实际中,字典和跳表会共享元素的成员和分值,不会造成数据重复
类型转换
- 有序集合保存的数量小于128
- 有序集合保存的所有元素成员长度都小于64字节
不能满足以上两个条件的有序集合对象将使用skiplist编码1
2zset-max-ziplist-entries
zset-max-ziplist-value