key-value实现
Redis并不是直接通过基本的数据结构来实现键值对数据库的,而是基于这些数据结构实现了一个对象系统,包括了字符串对象、集合对象、哈希对象、有序集合、列表对象。
每当我们在数据库中添加一个键值对时,会至少新建两个对象:
- 键值对的键是一个对象,该对象总是为字符串对象
- 键值对的值是一个对象,可以任意的基本数据结构。
对象的结构如下:
内存回收
Redis使用了引用计数法来对对象进行回收。
- 创建一个对象时,计数置为1
- 当对象被使用时,计数会加一
- 当对象不被使用时,计数会减一
- 当对象的计数为0,内存会被释放
对象共享
当键A和键B的值对象相同时,那么他们的值指针会指向同一个值对象。被共享的值对象的引用计数会加一。共享对象节约了内存,同时,共享对象不止可以被字符串键使用,在数据结构中嵌套了字符串对象的数据结构也能使用共享对象,如LinkedList编码的列表对象、HashTable编码的哈希对象与集合对象、zset编码的有序集合对象。
注意:只有当共享对象和键想创建的值对象完全相同时,该共享对象才会作为该键的值对象。而且共享对象只能是保存整数的字符串对象。因为整数字符串对象验证的时间复杂度是O(1)
,其他数据结构验证起来时间复杂度过高,如字符串可达O(N),列表或哈希对象可能达O(N^2)。
对象的空转时长
对象中的 lru 属性记录了对象最后一次被访问的时间。空转时长就是 当前的时间减去lru的时间 得出的。当开启了 maxmemory 选项且选择了 LRU 的内存回收算法,当超过 maxmemory 阈值时,那么空转时长较大的键会被优先释放,以回收内存。
基本数据结构
字符串
压缩列表
他是列表键和哈希键的底层实现之一。列表键使用压缩列表作为存储的条件如下:
- 只包含少量列表项
- 每个列表项是较小的整数值或长度较短的字符串
使用压缩列表可以节约内存,但是只能通过遍历去获取对象,当对象太多时效率会变低,所以在符合一定条件的情况下,可以使用压缩列表,此时对效率影响不大且能节约内存。当键不符合上述条件时,即对象太多或列表项太大,会使用其他编码方式来保存列表项,以提高读取、写入效率。
基本结构
每个压缩列表节点可以保存一个整数值或一个字节数组。其结构如下:
previous_entry_length
:保存了前一个节点的长度。用当前节点的指针p减去该长度就可定位到上一个节点的起始位置。压缩列表就是使用这种方法来从表尾向表头遍历。encoding
:记录节点所保存的数据类型和长度。content
:保存的节点的值,即整数值或字符数组。
连锁更新
简而言之,就是当插入一个新节点时,该节点的长度超过了后一个节点的 previous_entry_length
所能表示的范围(超过了1字节),那么此时后一个节点的previous_entry_length
属性就需要扩展空间,而后一个节点的接着下一个节点previous_entry_length
也会由此而需要扩展空间,继而导致了连锁效应,使得新节点后面的所有节点都需要进行空间扩展。最坏时间复杂度是O(N^2)。
当然,实际情况中由于连锁更新导致的性能低下的概率是很小的,主要是因为:
- 需要压缩列表里有多个连续的长度与250~253字节的节点,实际情况不多见。
- 即使出现了连锁更新,当只要更新的节点数量不多,那么对性能不会造成影响。
跳跃表
其实就是一种可以进行二分查找的有序链表,是一种有序的数据结构,每个节点都有多个指向其他节点的指针,以达到快速访问的目的。平均时间复杂度是O(logN),最差是O(N)。它是有序集合的底层实现之一。
实现
Redis中由zskiplistNode
表示跳跃表节点,zskiplist
保存跳跃表节点的相关信息。
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找,直到找到该节点。
zskiplist
包含以下信息:
跳跃表节点:
- 层:
level
数组可以包含多个元素,元素中的指针指向其他节点。程序通过这些层来提高访问其他节点的速度,一般来说,层数越多,访问的速度越快。
每次创建新的节点时,会利用幂次定律为该节点随机生成一个层数作为数组的大小。 - 跨度:即
level[i].span
,用来记录两个节点间的距离。跨度是用来计算排位(rank)的:将沿途访问过的所有层的跨度累加起来,就是目标节点在跳跃表中的排位。 - 分值和成员:节点的score属性即分值,跳跃表中的节点是按照分值从小到大排序的。成员对象obj是一个指针,指向字符串对象,其中保存着SDS值。
与红黑树等平衡树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 范围查询更具优势,跳表可直接往后遍历,而红黑树最差还要回溯到根节点
- 更容易实现;
有序集合使用哈希表和跳跃表实现。使用哈希表是为了能在O(1)
时间复杂度内查找到成员对象对应的分值score。跳跃表可以提高范围查询的效率,如ZRANK、ZRANGE。
参考资料
- 《Redis设计与实现》
- 《Redis深度历险》