数据结构
整数集合
当一个集合只包含整数且数量不多时,redis就会使用整数集合作为底层实现。
typedef struct inset{ //编码方式 uint32_t encoding; //元素数量 uint32_t length; //数组 int8_t contents[]; }
contents是从小到大的set集合,contents的类型由encoding字段来确定,并不是int8_t。
升级和降级
升级有3步骤
- 1.根据新元素类行扩展整数集合底层数组空间大小,并分配空间
- 2.将底层数组现有的所有元素都转换为新元素类型,保持增序
- 3.将新元素插入
如整数集合encoding从16位转32位,则首先将所有元素扩展为32位,然后移动元素进行插入。升级可以提升整数集合的灵活性同时节约内存。
整数集合不支持降级操作,一旦升级之后就会一直保持升级状态。
压缩列表
压缩列表是列表键和哈希键的底层实现之一。是为了节约内存开发的一种数据结构,组成如下:
[zlbytes,zltail,zllen,entry1,entry2,...,entryN,zlend]
zlbytes记录整个压缩列表占用的内存字节数;zltail记录尾节点距离起始地址有多少字节;zllen记录包含的节点个数;entry是节点;zlend标记节点末端;
压缩列表的每个节点,可以保存一个字节数组或整数值,由三部分构成
[previous_entry_length,encoding,content]
previous_entry_length记录前一个节点的长度,通过这个可以计算前一节点的起始地址。压缩列表的便利就是用这种方式实现的,可以基于这个字段不停的回溯到前面。
encoding记录节点content的数据类型和长度。
content则保存实际的entry值,根据encoding决定content的类型。
连锁更新问题:由于节点的previous_entry_length记录前一个节点的长度,一旦在小于254个字节的节点中插入一个大于254字节的新节点到表头,会诱发后续所有previous_entry_length进行更新,会产生O(N^2)级别的操作,但由于实际情形下,压缩列表长度并非特别长,更新节点不多,并不会影响性能。
对象
redis的所有数据结构,都是用于构造出可用的对象进行使用。内置的对象为字符串、列表、哈希、集合、有序集合五种。
typedef struct redisObject{ unsigned type:4;//类型 unsigned encoding:4;//编码 }
type属性记录对象的类型。encoding标识底层实现的数据结构,使用OBJECT ENCODING可以查看。
字符串对象
字符串的encoding可以是int,raw和embstr。embstr是一种短字符串保存的优化编码方式。浮点数在redis中也是以字符串值来保存的。
列表对象
列表的底层是ziplist或者linkedlist。list所有元素小于64字节使用ziplist,如果元素数量大于512个则转为linkedlist。具体的规则是可以修改的。
哈希对象
哈希对象底层是ziplist或hashtable。在ziplist中键值为紧挨的两个node,也是固定规则转hashtable。
集合对象
集合对象底层是intset或者hashtable。如果是字符串集合使用hashtable,如果是整型则为intset。只要集合包含字符串都换转hashtable。
有序集合对象
有序集合底层是ziplist或skiplist。这两个数据结构可能在zset中同时使用,同时使用可以提升性能。一般来说,字典和跳表共享元素值,并不会造成内存浪费。当有序集合元素小于128且所有长度小于64字节使用ziplist,否则使用skiplist。
命令
redis的命令分为两种类型,一种是任何对象(键)都可以执行(del、expire),另一种是特定的对象(键)可以执行(set,get对字符串;hdel,hset对哈希)。
这种实现依赖对redisObject结构的type属性判断,同时还会根据值对象的编码方式(encoding)来选择对应的命令。
内存回收
由于c语言本身不具备内存回收功能,redis在自己的对象系统中构建了一种引用计数实现内存回收。他由redisObject的refcount属性记录
typedef struct redisObject{ int refcount;//计数 }
对象共享
类似于浅拷贝,多个指针指向一个对象的机制,可以有效的节约内存。但只有包含整数的字符串对象实现共享(带字符串的字符串对象不行)。
空转时长
redisObject有lru属性来判断对象最后一次被访问的时间。用于服务清除内存的算法。