对象类型和编码
在redis中,并没有使用之前学到的数据结构来直接构建数据库,而是基于这些数据结构构建了一个对象系统,这个系统包含了字符串对象,列表对象,哈希对象,集合对象和有序集合对象五种类型。
在redis中,对象的数据结构如下:
1 | /* The actual Redis Object */ |
对象宏定义如下:
对象的ptr指针指向了这个对象的底层数据结构,这些数据结构由对象的encoding属性决定:
不同类型的编码的对象可能有多种形式:
字符串对象
字符串的编码可以是int,raw,embstr。
- 若为整数并且可用long表示,那么编码设置为int,(void)转为(int)类型。
- 若为字符串类型,且长度大于32字节,那么用动态字符串存储,编码设置为raw,调用两次内存分配。
- 如果为字符串类型且长度小于32字节,那么用embstr编码表示,与上面不同的是embstr直接调用一次内存分配,将redisObject和embstr存储在一起。
编码转换
- int编码的字符串执行append命令追加一个非整数值或者长度超过限制,那么int将变为raw类型。
- embstr为可读类型,也就是不能执行任何更改操作,因此要更改必须转为raw类型。一旦对embstr更改将立即编程raw类型。
字符串命令的实现
列表对象
列表对象通过ziplist或者linkedlist编码实现。
如果是压缩链表,在内存中的布局如下图所示:
如果是双向队列,如下图所示:
链表中存储的是动态字符串对象,字符串对象是Redis五种对象中唯一一种会被其他四种类型对象嵌套的对象。
编码转换
满足下面两个条件,使用ziplist编码:
- 列表对象保存的所有字符串元素长度小于64字节
- 列表对象保存的元素数量小于512个
除此之外使用linkedlist编码。
列表命令实现
哈希对象
哈希对象通过ziplist或者hashtable编码实现。
- ziplist编码的哈希对象使用压缩链表作为底层实现
新键值对加入哈希对象是,会先将保存了键的压缩链表节点压到表尾,在将保存了值的节点压到表尾。如下图所示: - hashtable编码使用字典作为底层实现
哈希对象每个键值对使用一个字典键值对来保存。每个键和值都是一个字符串对象。如下图所示:
编码转换
满足下面两个条件使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
- 键值对数量小于512个。
除此之外使用字典存储。
命令的实现
集合对象
集合对象使用intset或者hashtable编码。
- intset使用整数集合作为底层实现。
- hashtable使用字典作为底层实现。
编码转换
集合对象满足下面两个条件使用intset编码:
- 集合对象保存的所有元素都是整数。
- 集合对象保存的元素数量不超过512个。
除此之外使用hashtable实现。
集合命令的实现
有序集合
有序结合使用ziplist或者skiplist编码。
- ziplist使用压缩链表作为底层实现。每个集合元素使用两个紧挨在一起的压缩链表节点表示,第一个是元素成员,第二个是元素的分值。
- skiplist使用zset作为底层实现。
zset
zset有一个字典和一个跳表组成:
1 | typedef struct zset { |
其中zsl跳表按照分值从小到大保存了所有集合元素,每个跳表节点都保存了一个集合元素,具体的:跳表的obj指针指向元素,score属性保存元素分值。通过跳表可以对有序集合进行范围型操作,比如ZRANK.ZRANGE等命令。dict字典结构为有序结合创建了一个从成员到分值的映射,键保存成员,值保存分值,因此通过字典可以O(1)地查找元素的分值。
为什么要用两个结构来实现zset?
理论上可以使用其中任何一个来实现,但是只使用字典的话,执行ZRANK,ZRANGE命令就需要O(NlogN)的复杂度,单使用跳表的话,查找复杂度会是O(logN)。并且两种数据结构都使用指针来避免了额外的内存开销。
上图所示为了简化,stingObject和score分开表示,但是在实际的实现中,指针都是指向同一块内存区域。
编码转换
同时满足下面两个条件使用压缩链表,否则使用zset:
- 集合保存的元素数量小于128个
- 保存的元素成员的长度都小于64字节
命令的实现
内存回收、对象共享和空转时长
- 内存回收
在对象结构体中
1 | typedef struct redisObject { |
有一个refcount字段用来记录对象的引用计数信息。对象的整个生命周期可以分为创建对象,操作对象,释放对象三个阶段。
- 对象共享
对象的引用计数还能带来对象共享的作用:如果键A创建”100”作为值对象,此时键B如果需要创建一个同样的值对象,就可以直接在A的值对象的引用计数上加1。
redis不共享包含字符串的对象,原因在于每次共享需要比较对象是否完全相同,如果是整数,复杂度为O(1),如果是字符串,复杂度是O(N),如果是包含了多个值的对象,复杂度会上升为O(N^2)。
- 空转时长
另外结构体中的lru记录了当前时间减去值对象最后一次被访问的时间,当程序内存超过限制,会优先清理长时间没有访问的元素。