整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且元素数量不多时,redis就会使用整数集合作为集合键的底层实现。
整数集合定义在intset.h/intset中:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
contents数组是整数集合的底层实现,集合中的每个元素都是数组的一个数组项,各个项在数组中按照值得大小从小到大排列,且不包含任何重复项;length即为数组的长度;contents数组的真正类型取决于encoding属性的值:
一个可能的整数集合结构如下:
encoding为INTSET_ENC_INT16,元素数目为3,则contents数组的大小等于sizeof(int16_t)*length=16*3=48
当我们在整数集合中添加的元素类型比现有元素类型都要长时,需要先对整数集合进行升级,升级分为以下三个步骤:
新元素要么大于所有元素,要么小于所有元素:
引入升级机制,有以下两点好处:
但是整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
压缩列表是列表键和哈希键的底层实现之一,是redis为了节约内存而开发的,一个压缩列表包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
一个压缩列表的各个组成部分如下图所示:
其中,各个组成部分详细说明如下:
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用的内存字节数 |
zltail | uint32_t | 4节 | 记录压缩列表表尾节点距离列表的起始地址有多少字节 |
zllen | uint16_t | 2字节 | 记录压缩列表节点数量 |
entryX | 列表节点 | 不定 | 压缩列表中的节点 |
zlend | uint8_t | 1字节 | 特殊值,用于标记压缩列表末端 |
压缩列表节点定义如下:
typedef struct zlentry {
// prevrawlen :前置节点的长度
// prevrawlensize :编码 prevrawlen 所需的字节大小
unsigned int prevrawlensize, prevrawlen;
// len :当前节点值的长度
// lensize :编码 len 所需的字节大小
unsigned int lensize, len;
// 当前节点 header 的大小
// 等于 prevrawlensize + lensize
unsigned int headersize;
// 当前节点值所使用的编码类型
unsigned char encoding;
// 指向当前节点的指针
unsigned char *p;
} zlentry;
prevrawlen用来记录前置节点的长度,如果前置节点长度小于254字节,那么prevrawlen属性的长度就是1字节,前置节点的长度就保存在这一字节中;如果前置节点长度不小于254字节,那么prevrawlen属性的长度就是5字节,其中,prevrawlen的第一字节会被设置为0xFE,而之后的4个字节则会保存前置节点的长度。
因为节点记录了前置节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前直节点的起始地址,从而可以实现列表的反向遍历。
对于zlentry结构体,可以将压缩列表节点抽象为以下结构:
其中,previous_entry_length记录了前置节点的长度,encoding记录了数据类型以及长度:
content负责保存当前节点的值。
该节点编码的最高两位是00,表示保存的是字节数组,编码后续1011表示字节数组的长度是11,content记录了值“hello world”。
该节点编码的最高两位是11,表示保存的是整数值,编码11000000表示整数类型为int16_t,content记录了值4396。
假如在压缩列表中,有多个连续的、长度介于250-253字节的节点,如下图所示:
此时,如果将一个长度大于等于254的新节点设置为压缩列表头节点,那么新节点将成为entry1的前置节点:
此时,entry1已经无法记录前置节点的长度,所以需要对entry1进行空间重新配,将previous_entry_length属性从原来的1字节扩展到5字节。但是,此时entry1节点总长度由于该更新也扩充到了254字节以上,以此类推,还需要对entry2、entey3…entryN进行更新,这种情况称为“连锁更新”。
除了添加节点可能出现连锁更新,删除节点也可能出现此情况,考虑以下情况:
entry1-entryN都是大小介于250-253字节的节点,big节点长度大于254字节,small节点长度小于254字节,将small节点删除后,仍会出现连锁更新。
连锁更新在最坏的情况下需要对列表执行N次空间重分配,每次空间重分配的最坏复杂度是O(N),所以连锁更新最坏复杂度是O(N**2)。
尽管复杂度很高,但是出现连锁更新的概率很低:
在redis中,并没有直接使用简单动态字符串、字典、压缩列表、整数集合等数据结构,而是基于这些数据结构构建了一个对象系统,使用对象系统有以下几点好处:(1)可以针对不同的使用场景为对象设置不同的实现,从而优化对象在不同场景下的使用效率;(2)redis对象系统还实现了基于引用计数的内存回收机制以及对象共享机制,可以让多个键共享同一个对象来节省内存;(3)redis对象带有访问时间记录功能,在服务器启用了maxmemory功能时,空转时长较大的键会优先被服务器删除。
redis中每个对象都由一个redisObject结构表示:
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;
type属性记录了对象的类型,这个属性值是以下常量中的一个:
常量类型 | 对象的名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
在redis中,总是键值对共存的情况,键总是一个字符串对象,值可以是任意对象。当我们对一个键执行TYPE命令时,命令的返回结果就是键对应的值对象类型,而不是键对象的类型。
ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定,encoding属性记录了对象所使用的编码,其值可以是以下列表中的一个:
编码常量 | 对应的数据结构 |
---|---|
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数结合 |
REDIS_ENCODING_SKIPLIST | 跳跃表 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
每种类型都至少使用了两种不同的编码类型:
类型 | 编码 | 对象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用SDS实现的字符串 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端列表实现的列表 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表实现的有序集合 |
使用object encoding命令可以查看一个数据库值对象的编码。
字符串对象的编码可以是int、raw或者embstr。
如果一个字符串对象保存的是整数值,且该值可以用long类型表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里,并将字符串对象的编码设置为int。
如果字符串保存的是一个字符串,且字符串的长度小于39字节,那么字符串使用embstr编码方式来保存这个字符串值。
embstr是专门用于保存短字符串的一种优化编码方式,但是embstr和raw编码一样,同样使用SDS来保存字符串,但不同的是,raw会调用两次内存分配函数分别创建redisObject和sdshdr结构,而embstr只会调用一次内存分配函数来分配一块连续的内存。
int编码和embstr编码的字符串对象在满足条件下,会被转换为raw编码的字符串对象。
对于int编码的字符串来说,如果我们执行一些命令,使得这个对象保存的不再是整数值,而是一个字符串,那编码将从int变为raw。
对于embstr编码的字符串来说,对字符串执行任何修改命令时,程序都会先将编码转换为raw格式,在执行修改。
列表对象的编码可以是ziplist或者linkedlist,当
哈希对象的编码可以是ziplist或者hashtable,当
其中,采用ziplist时,键值对总是紧挨在一起的,先插入键,再紧接着插入值。
可以是intset或者hashtable实现,intset编码的集合对象使用整数集合作为底层实现,当
有序集合可以采用ziiplist或者skiplist实现,当
同时,为了兼顾有序和查找的复杂度,在有序对象中,还使用了一个dict字典保存了键到值的映射关系。
redis通过引用计数来实现了内存回收这一功能,创建新对象时,它的引用计数初始化为1,并通过以下几个函数来对引用计数进行更新:
函数 | 作用 |
---|---|
incrRefCount | 引用计数加1 |
decrRefCount | 引用计数减1,当引用计数为0时,释放内存 |
resetRefCount | 重置计数为0,但是不释放对象内存 |
引用计数除了用作内存回收以外,还带有对象共享的作用。
如果键A创建了一个包含整数值100的字符串对象作为值对象,同时键B也要创建一个包含整数值100的字符串对象作为值对象,那此时,为了节约内存,键A和键B可以共用一个值对象,只需要将键B的值指针指向键A的值对象,并将值对象的引用计数加1即可。