一、简介
Redis数据库里边的每一个键值对(key-value pair)都是由对象构成。其中,数据库键总是一个字符串对象(sting object),而值则可能是字符串对象(string objec)、哈希对象(hash object)、列表对象(list object)、集合对象(set object)、有序集合(sorted set object)的其中一种。
这些键值对象,都是由底层redis内置定义的各种数据结构所支持组成的,本文意在讲解redis对象的底层数据结构原理,以及一些由于存储对象的变化所带来的底层数据结构的变更,从而有目的有方向的帮助我们更加专业的使用redis,性能、效率达到最大。
二、概况
我们知道redis支持5种对象结构供用户使用,它们分别是:
| 数据类型 | 数据类型TYPE |
| 字符串对象(sting object) | REDIS_STRING |
| 列表对象(list object) | REDIS_LIST |
| 哈希对象(hash object) | REDIS_HASH |
| 集合对象(set object) | REDIS_SET |
| 有序集合(sorted set object) | REDIS_ZSET |
redis在底层提供了8种基础数据结构,用来支持在应用层面给用户提供的这5种对象结构,底层数据结构标识如下:
| 底层编码TYPE | 底层数据结构 |
| REDIS_ENCODING_INT | long类型的整数 |
| REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
| REDIS_ENCODING_RAW | 简单动态字符串 |
| REDIS_ENCODING_HT | 字典 |
| REDIS_ENCODING_LINKEDLIST | 双端链表 |
| REDIS_ENCODING_ZIPLIST | 压缩列表 |
| REDIS_ENCODING_INTSET | 整数集合 |
| REDIS_ENCODING_SKIPLIST | 跳跃表 |
redis中提供的5种对象结构,是由上述8中底层数据结构中的一种或者多种构成组成的,下面我们详细的介绍这8中基础数据结构的工作原理及其特性。
三、详情
3.1 简单动态字符串(SDS)
简单动态字符串(simple dynamic string)是redis的字符串的默认表示方式,如果是表示字符串字面量,比如一些无需修改的字符串,则会使用C语言的字符串表示,比如日志打印;如果是动态的字符串表示,则会使用SDS结构来存储表示。
使用场景
SDS是redis最底层最基本的数据结构,在redis的数据库中包含字符串值的键值对都是使用SDS存储的。
结构定义
redis中SDS的结构定义为:
struct sdshdr {
//buf数组已使用字节数量;SDS保存字符串长度
int len;
//buf数组空闲的字节数量
int free;
//存储数据的数组
char buf[];
}
一个保存数据的SDS数据结构的例子如下所示:

上图展示的是一个存储着redis字符串的SDS结构示例:
len:数值为5,表示这个SDS存储的字符串的长度为5;
free:数值为1,表示这个SDS结构还可以存储的字节数,当前还有1字节可用;
buf:是实际存储字符串的char类型的数组,以‘\0’表示结束。
注:'\0'空字符结尾遵循了C语言字符串的规范,这个空字符的1字节不计算在SDS结构的长度内,遵循这个规范可以支持直接使用C语言的一部分函数功能。
与C对比
C语言的字符串存储信息,没有记录当前存储的字符串长度信息,相对于SDS中的len属性,因此SDS将获取字符串长度操作复杂度从O(N)降低到O(1);另外由于C没有记录长度信息,因此一些操作比如拼接,会有缓冲区溢出的风险。SDS使用free和len属性解除数组和字符串长度的关联,会根据free检查新增的字符串是否超长,进行动态扩容操作。
SDS是以二进制的方式来处理存放在buf数组中的数据,不会对数据进行任何额外操作、过滤和假设等操作,也就是存进去是什么样子,拿出来的时候也是一样。
内存策略
1.空间预分配:用于优化SDS字符串增长操作,以修改后的len属性值和阈值1MB比较
如果len < 1MB,扩容至新长度的2倍,即如果新修改的字符串是15字节,则扩容至free=15字节,一共30+1字节;
如果len >= 1MB,则分配1MB的使用空间,过程如下:

2.惰性空间:用于优化字符串缩短操作
当SDS字符串进行缩短操作时,SDS并不会直接释放掉已经分配的内存,而是记录在free中,用于下一次增长字符串操作使用,避免了内存重分配的资源消耗。当然会有造成内存泄漏的风险,SDS提供了释放未使用空间的api,避免这种问题。
总结
简单动态字符串和C语言的字符串相比有几个优点如下:
| C语言字符串 | SDS |
| 获取字符串长度复杂度O(N) | 获取字符串长度复杂度O(1),len属性获取 |
| api操作内存溢出风险 | 规避内存溢出风险,动态扩容 |
| 只能存储文本字符串 | 可以存储文本字符串、二进制数据 |
| 修改字符串N次,必然需要N次内存重分配 | 修改字符串N次,最多需要N次内存重分配(预分配) |
| 可以使用本身所有操作函数 | 可以使用一部分C语言库函数(保留空字符结尾) |
3.2 链表(linked list)
链表具有高效的节点重排能力,并且可以顺序访问,通过节点操作来改变整个链表的状态。在C语言内无内置链表结构,因此redis构建了自己的链表数据结构。
使用场景
列表键元素较多的时候,底层就会使用链表来存储元素,以及在发布订阅、慢查询、监视器等地方也用到了链表结构。
结构定义
链表是由两个数据结构定义而成的,首先是节点组成结构,listNode逻辑如下:
struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点值
void *value;
}
上述结构可以看到,前置后置节点指针组成一个双向的链表,redis还使用了另外一个结构持有此链表,操作更加方便,list结构如下:
struct list {
//头节点
listNode *head;
//尾节点
listNode *tail;
//链表节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void (*free)(void *ptr);
//节点值对比函数
void (*match)(void *ptr, void*key);
}
可以看到list结构定义了listNode的首尾节点以及链表节点数量,可以更加方便的操作管理listNode节点,整个链表结构如下:

特性
| 特性 | 概括 |
| 双端 | 每个listNode节点包含前置和后置节点,获取前后节点复杂度O(1) |
| 无环 | 头尾节点的前驱和后置为null,作为链表访问终点 |
| 头尾指针 | list结构分别指向listNode头尾节点,获取头尾节点复杂度O(1) |
| 长度计数 | 获取链表长度复杂度O(1) |
| 多态 | void*指针指向任意类型数据,提供操作节点函数 |
3.3 字典(dict)
字典又被称作符号表(symbol table)、关联数组(associative array)或者映射(map),是一种用来存储key-value键值对的抽象数据结构。在一个字典里边每个key值都是唯一的,通过key值查找到对应的value值。
使用场景
redis数据库使用字典结构实现,另外字典也是哈希键的实现之一,当哈希键的元素比较多或者元素比较大的时候,就会使用字典结构来作为哈希键底层的存储结构。
结构定义
字典使用哈希表作为底层实现,首先我们看下哈希表的结构定义,dictht结构如下:
struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,计算索引,等于size-1
unsigned long sizeMask;
//哈希表已有节点数量
unsigned long used
}
table是一个dictEntry结构的数组,里边存储着指向dictEntry结构的一个指针,size是哈希表的大小,used是哈希表已经使用的节点数量,sizeMask是用来计算元素节点在table数组的位置。下面我们看下哈希表节点dictEntry结构定义:
struct dictEntry {
//哈希表节点的key值
void *key
//哈希表节点的value值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}
可以看到,每一个哈希表节点dictEntry存有一个key和一个value,以及一个指向下一个哈希表节点的指针,这样就可以形成一个链表,在table相同位置的哈希表节点则会通过这个指针进行关联。在redis中,字典是使用dict结构来表示字典的,结构如下:
struct dict {
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引(rehash不在进行时,值为-1)
int trehashidx;
}
因此可以看出,redis中的字典是以哈希表作为基础实现的,通过dict进行哈希表的关联管理,默认的dict初始化了dictht数组的长度为2,哈希表是由dictht哈希表和dictEntry哈希表节点所定义的,假设当存储部分数据之后,结构如下:

哈希算法
dict默认初始化dictht哈希表数组的长度为2,默认使用ht[0]哈希表,ht[1]是最为rehash时使用的一个空间哈希表。当一个新的元素添加到字典里边时,基本思想是根据hash算法计算出新元素在dictht[x]中table数组的位置,计算过程如下:
//根据设定的hashtype找到设定的hash函数,计算key的hash值
hash = dict->type->hashFunction(key)
//key的hash值和哈希掩码所与运算计算下标志
index = hash & dict->ht[x].sizemask
假设现在有一个字典是空的,我们将(k0,v0)假如到字典中,大致过程如下:
计算k0的哈希值,我们假设hash = dict->type->hashFunction(k0)值为8,在通过index = hash & dict->ht[x].sizemask计算得到index=8 & 3 = 0;

哈希冲突
哈希表解决哈希冲突是使用的链地址法,我们前面介绍到dictEntry中含有下一个哈希表节点的指针next,同一链关联的节点在table中处于同一个下标;并且由于dictht没有尾节点的指针,因此为了速度考虑,新的节点总是处于链表的头节点上。
rehash
随着哈希表的节点的数量的变化,为了哈希表的负载因子维持在一个合理的范围内,我们需要对哈希表进行必要的扩展和收缩,也就是对哈希表进行重哈希的操作。当以下条件任意一个满足的时候,则会主动的进行rehash动作:
1.服务器目前没有执行BGSAVE或者BGREWRITEAOF命令,且哈希表的负载因子大于等于1;
2.服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,且哈希表的负载因子大于等于5;
负载因子 = ht[0].used / ht[0].size,之所以在执行复制命令期间提高负载因子的阈值,是因为执行BGSAVE或者BGREWRITEAOF命令会fork出一个子进程,增加了系统的负荷,且需要内存。提高负载因子,从而提高rehash的阈值,最大限度节约内存。除此之外,如果负载因子小于0.1,程序会自动对哈希表执行收缩动作。
重哈希的操作过程如下:
1.为ht[1]分配空间,这个空间的大小取决于ht[0]包含的元素数量也就是ht[0]的used属性值和要执行的操作;
2.如果是扩展操作,则ht[1]的大小是第一个大于等于ht[0].used*2的2的n次方幂;
3.如果是收缩操作,则ht[1]的大小是第一个大于等于ht[0].used的2的n次方幂;
4.ht[1]内存空间分配好后,将ht[0]上的节点rehash到ht[1]上面,也就是将原来哈希节点通过key的新hash计算出新的index,并放入到ht[1]中指定位置;
5.ht[0]上所有的键值对全部迁移至ht[1]后,则会把ht[0]释放,并将ht[1]更改成ht[0],且新创建一个ht[1]的哈希表作为下一次的rehash使用;
问题?如果哈希表很大的话,大量的迁移操作会影响服务性能,以及在转移期间发生的新的键的变更该如何处理呢?在redis中通过渐进式哈希方式解决这些问题,渐进式哈希过程如下:
1.为ht[1]分配空间,这个空间的大小取决于ht[0]包含的元素数量也就是ht[0]的used属性值和要执行的操作;
2.在字典中维持了一个rehashidx的属性,初始化为0,表示rehash正式开始;
3.在rehash进行期间,每次对字典执行ADUS操作时,除了执行指定操作外,还要将ht[0]表上索引为rehashidx上的键值对rehash到ht[1]上,全部哈希完毕后,将rehashidx值加一;
4.随着操作不断执行,最终在某个时间点,ht[0]全部的元素节点rehash到了ht[1]上,此时将rehashidx设置为-1,完成。
渐进式哈希避免了短时间大量的迁移工作和计算量影响服务性能。在渐进式rehash期间,ht[0]和ht[1]是共同工作的,ADUS操作都是在二者一起进行的,其中新增工作只会在ht[1]上进行,查询则会先去ht[0]查询,如果不存在则会到ht[1]再次查询,随着时间和操作的执行,最终ht[0]会变成一个空表。
3.4 跳跃表(skip list)
跳跃表是一种有序数据结构,它通过每个节点中维持了多个指向其他节点的指针,从而达到快速访问节点的目的。大多数情况下跳跃表查找平均复杂度是O(logN),最坏情况是O(N)。跳跃表的效率可以和平衡树媲美,并且因为跳跃表的实现简单,很多情况下可以采用跳跃表来代替平衡树。
使用场景
跳跃表是有序集合的底层数据结构之一,如果一个有序集合的元素数量比较多或者有序集合中元素的长度比较大,则会使用跳跃表作为底层的数据存储结构。
结构定义
跳跃表由两个结构定义的,首先是zskiplist结构用于保存跳跃表相关信息,zskiplistNode是跳跃表的节点表示,首先我们看下跳跳跃表节点,zskiplistNode结构定义:
struct zskiplistNode {
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
}
层:zskiplistNode的层数组可以包含多个前进指针,指向不同的跨度的节点,一般来说前进指针越多,访问其他节点越快;每次新创建一个zskiplistNode节点的时候都会根据幂次定律(越大的树出现几率越小)随机生成一个介于1和32之间的数值作为level数组的大小。
前进指针: 每个节点有多个层,每一个层都有一个前进指针,用于从表头访问表尾方向的节点;
后退指针:用于表尾向表头方向访问节点,和前进指针不同的是,每一个节点只有一个表尾指针;
分值:每一个节点都存储着一个分值,double类型的浮点数,跳跃表中数据都按照分值进行排序,相同分值的按照对象字典序排序;
对象:对象是节点存储的一个对象指针,他指向了一个字符串对象,字符串对象则保存着一个SDS值;
zskiplist维护管理着这些节点,可以更方便的对跳跃表节点进行处理,例如快速访问头、尾节点,快速获取跳跃表长度以及最大深度,下面我们看下zskiplist结构定义:
struct zskiplist {
//跳跃表头尾节点指针
struct zskiplistNode *header, *tail;
//跳跃表节点数量
unsigned long length;
//跳跃表最高层数
unsigned long level;
}
首先定义了头尾指针指向了跳跃表的头和尾,另外记录了跳跃表的节点数量还有最大层数节点的层数,下面我们看下具有节点的跳跃表的基本结构样式:

跳跃表的查找过程是自上而下的一个跨度查找,先从上层确认区间,然后逐级下查,平均复杂度为O(logN)。
3.5 整数集合(int set)
整数集合是redis 用来保存整数值的数据结构,可以保存类型为int16_t、int32_t或者int_64的整数值,并且保证集合中不会出现重复数字。
使用场景
整数集合是集合键的底层实现之一,当一个集合键只包含整数值元素,并且这个集合元素不多的时候,就会使用整数集合作为底层实现。
结构定义
在redis中使用intset结构表示整数集合,intset结构如下:
struct intset {
//编码方式
unit32_t encoding;
//集合元素数量
unit32_t length;
//底层数组
int8_t contents[];
}
encoding表示contents数组的编码方式,包括int_16、int_32以及int_64;length属性表示数组中元素的数量;contents数组是实际存储元素的地方,虽然声明为int8类型,但是他是按照encoding编码类型存储数据,数组中的元素按照从小到大的顺序排序,并且数组中没有重复整数。
当encoding=INTSET_ENC_INT16时,则数组中的值都是int16_t的整数值(-32768~32767),同理当encoding=INTSET_ENC_INT32或者encoding=INTSET_ENC_INT64时候分别定义了数组中整数的类型,下图展示了一个存储元素的整数集合的示例:

升级
当已有集合存储的是int16_t类型的数据,如果此时有一个int32_t类型的整数存储进来的时候,就涉及到整数集合的升级逻辑,因为原来的存储方式无法满足新的整数的存储。整数集合的升级操作分为3个步骤:
1.首先我们根据新整数类型进行数组的扩容空间操作,为新元素分配空间;
2.将底层元素按照新的编码方式进行转换,并且安放在新的位置上,且保证有序性不变;
3.将新元素添加到数组中,完毕。
下图展示了添加新元素需要升级的例子:
首先看一下初始状态,inset里边有是那个元素,1,2,3,按照元素11以int16_t编码格式存储,如下图:

当新元素65535以int32_t编码格式添加至此整数集合中时,则当前intset需要进行升级操作,先按照新编码格式进行扩容操作:

接下来就是将原来的元素按照新的编码方式转化并放入对应位置,比如原来的元素3,在四个元素中排行第三,则应放入新的数组的索引2处:

如此类推,按照排序:3->index2;2->index1;1->index0,移动完毕后,情况如下:

最后我们把新增的元素放到数组中,位置是索引3,且把length和encoding属性进行更新:

到此,intset整数集合的升级操作完成,升级操作区分了编码方式,这样可以使得同一个数组存储相同类型数据,避免类型错误;另外,比全部直接使用int64_t类型存储方式更加节省内存。
注:一旦升级,就不会在降级,就会一只保持升级之后的编码方式,哪怕是后来只有低级的编码类型。
3.6 压缩列表(ziplist)
压缩列表是redis为了节约内存而开发的,是由一系列特殊编码连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
使用场景
压缩列表是列表键和哈希键实现的底层结构之一,当一个列表键包含比较少的列表项,并且每个列表项是比较小的字符串或者小整数值,这个列表键底层就是使用压缩列表作为实现结构。
结构定义
压缩列表的结构如下:

各个参数的含义如下:
| 属性 | 类型 | 长度 | 用途 |
| zlbytes | uint32_t | 4字节 | 记录了整个压缩列表所占的内存字节数:在对压缩列表进行内存重分配或者计算zlend位置使用 |
| zltail | uint32_t | 4字节 | 记录了列表的表尾节点距离列表起始地址的字节数:通过此偏移量可以不用遍历这个那个列表就知道列表尾节点的位置 |
| zllen | uint16_t | 2字节 | 记录了压缩列表的节点的数量:当整个属性值小于uint16_max(65535)时,这个属性就是列表的节点数量;如果超过这个值,则需要遍历整个列表才知道节点数量 |
| entry | 列表节点 | 不定 | 压缩列表的各个节点,节点长度根据节点内容而定 |
| zlend | uint8_t | 1字节 | 特殊值0xFF(十进制255),用于标记压缩列表的结尾 |
下图展示了一个包含三个节点的压缩列表示例,压缩列表占用字节0x50(十进制80),尾节点距离起始地址0x3c(十进制60),因此p表示头,则p+0x3c表示尾部节点,情况如下:

entry压缩列表节点可以保存一个字节数组或者一个整数值,其中字节数组可以是以下三种长度之一:
1.长度小于等于63(2的6次幂减1)字节的字节数组;
2.长度小于等于16383(2的14次幂减1)字节的字节数组;
3.长度小于等于4294967295(2的32次幂减1)字节的字节数组;
整数值可以是以下六种长度之一:
1.4位长,介于0至12之间的无符号整数;
2.1字节长的有符号整数;
3.3字节长的有符号整数;
4.uint16_t类型整数;
5.uint32_t类型整数;
6.uint64_t类型整数;
每一个entry结构都是由三个属性组成,entry结构如下图:

previous_wntry_length表示前一个节点所占的字节数,也就是前一个entry的长度,因此可以通过一个节点的起始地址,便可以通过与previous_wntry_length属性向前遍历整个列表节点。本属性的长度为1字节或者5字节,如果前一个节点的长度小于254字节,则此属性是1字节,如果前节点长度超过254字节,则此属性是5字节长,其中第一字节设置为0xFE(十进制254),后面的四个字节用来保存前节点的长度。0x05表示前节点长度是5字节,0xFE00002726表示前节点长度为0x00002726(十进制10086)
encoding记录了content保存的数据的类型以及长度,也就是刚才上述提到的节点可取类型3种数组+6种整数类型;
1.一字节、两字节和五字节长,值得最高位是00、01和10是字节数组编码,表示content存储的是字节数组,数组的长度在encoding去除最高两位的其他位表示;
2.一字节长,值得最高位是11开头的整数编码,表示content存储的是整数值,整数的长度在encoding去除最高两位的其他位表示;
content属性保存了节点存储的值,可以是整数或者数组,由encoding属性决定。
下图分别展示了存储字节数字和整数值的压缩列表的示例:

连锁更新问题
前面介绍压缩列表使用previous_entry_length属性记录前一个节点所占的字节数,前节点长度是否超出254字节,决定了本节点的previous_entry_length属性所占的长度是1还是5字节。现在有这样一个场景,我们假设有e1到eN的压缩列表节点,他们的长度都介于250~253字节之间,这样每个节点的previous_entry_length属性占用1字节,这时候如果新来了一个节点插入到列表头部,他的长度大于等于254字节,此时后面的节点就需要重新分配previous_entry_length属性至5个字节来保存新节点的长度,那么问题就来了,后节点发现前面超出254字节了,他也要更新previous_entry_length属性长度为5字节,循环到尾部,导致了连锁更新的问题,当然删除节点也会产生这样问题。
连锁更新最坏需要N次内存重分配,复杂度还是比较高的,但是连锁更新发生的情况也是比较极端的,需要好多连续介于250~253字节长度的节点,这种不是很常见。
四、应用
前面介绍了redis底层定义的几种数据结构,包括简单字符串(SDS)、链表(linked list)、字典(dict)、压缩列表(ziplist)、跳跃表(skiplist)、整数集合(intset)。
redis数据库没有直接使用这些底层的数据结构,而是基于这些底层的数据结构构建了对象系统,每种对象至少包含了我们在前面介绍的一种数据结构。在上层使用对象结构可以针对不同的使用场景使用不同的底层数据结构,从而优化对象在不同场景下的使用效率。
redis使用对象表示数据库中的键值对,每当我们创建一个键值对的时候,我们至少创建俩个对象,一个是键对象,另一个是值对象。对象结构是使用redisObject结构定义的,结构如下:
struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
}
type属性记录了这个redisObject的类型,也就是文章开头提到的,提供给客户端使用的五种redis对象类型:
| 数据类型 | 数据类型TYPE | type命令对应输出 |
| 字符串对象(sting object) | REDIS_STRING | string |
| 列表对象(list object) | REDIS_LIST | list |
| 哈希对象(hash object) | REDIS_HASH | hash |
| 集合对象(set object) | REDIS_SET | set |
| 有序集合(sorted set object) | REDIS_ZSET | zset |
当我们对一个键执行type命令时,将会返回这五种redisObject类型之一。
ptr指针指向了底层存储的数据结构,而这些数据结构由encoding决定,也就是我们上面提到的8中底层数据结构,每一种redisObject都至少使用了两种编码,下面是对象和编码的组合关系:
| 类型type | 编码encoding | redisObject-redis对象 |
| REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
| 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 ENCIDING”输出redis对象的编码方式,下面我们看下redis五种对象机构的编码切换方式。
1.string
上面介绍了string对象有三种编码方式,分别是int、embstr和raw,他们的分别出现场景是:
当字符串保存的是一个整数值,并且可以用long表示时,该字符串对象使用int编码;
当字符串编码是一个字符串时,如果字符串的长度大于39,则使用raw编码表示,ptr指针指向底层SDS底层结构;
当字符串编码是一个字符串时,如果字符串的长度小于等于39,则使用embstr编码表示,ptr指针指向底层embstr编码的SDS底层结构;
下图是三者的一个示例:
字符串保存的是一个整数值,可以用long表示时,该字符串对象使用int编码

字符串编码是一个字符串,且字符串的长度大于39,则使用raw编码表示,ptr指针指向底层SDS底层结构

字符串编码是一个字符串,且字符串的长度小于等于39,则使用embstr编码表示,ptr指针指向底层embstr编码的SDS底层结构 ,可以看到结构是比较紧凑的,他和raw编码方式在数据结构上是相同的,都是使用SDS简单动态字符串此层数据结构,不同之处在与embstr只需要一个内存分配即可完成整个的数据存储,而raw需要两次内存分配分别构造redisObject和SDS对象。

int和embstr在一定情况下,会改编成raw编码方式,比如原来是int编码,修改成了一个字符串或者拼接了字符串导致不可以用long表示,则会升级为raw;embstr编码方式没有提供修改的程序,也就是说embstr是一个只读的编码方式,如果对embstr编码的字符串修改,则会改变编码方式为raw,然后在进行修改。
2.list
列表对象的底层实现是ziplist或者linkedlist,出现的场景是:
当列表保存的字符串长度都小于64字节且节点数量小于512个的时候使用ziplist编码,否则使用linkedlist结构;这两个限制阈值是可以通过配置文件修改的,以下是两种编码方式的示例:
使用ziplist数据结构编码的列表:

假设此列表是使用linkedlist编码结构,则如下所示,简化了stringObject的结构展示:

3.hash
哈希对象的结构可以是ziplist或者ht,区分场景如下:
当哈希对象保存的所有键值对的键和值的长度都小于64字节,且保存的键值对数量小于512个的时候使用ziplist,否则使用ht。
分别使用两种编码的hash结构如下:
下图展示的是使用压缩列表的哈希对象,可以看到键值对是紧紧贴在一起的,添加的时候,首先把键添加至压缩列表的结尾,然后再把值添加到列表结尾,后添加的键值对在压缩列表的结尾处:

下图展示了使用ht结构的哈希对象的结构:

4.set
集合对象的编码方式有intset和ht,区分场景如下:
当集合对象保存的都是整数值,且集合元素数量不超过512个,就会使用intset编码,否则使用ht结构存储,两种编码方式的集合对象机构如下图:
下图展示了使用intset编码的集合对象的结构图示:

下图展示了使用ht编码方式的集合对象的结构:

5.zset
有序集合使用了ziplist和skiplist两种编码结构,区分场景如下:
当有序结合数量小于128个,并且所有元素成员的长度都小于64字节,就会使用ziplist编码方式,否则使用skiplist编码,两种编码方式的有序集合的结构如下:
下图展示了使用ziplist编码方式的有序结合机构,可以看到元素是按照分值从小到大进行排列的,对象和分值是紧紧挨在一起的

下图展示了使用skiplist编码方式的有序集合对象,可以看到这个编码下同时使用了ht字典和skiplist两种底层结构,通过字典可以用O(1)的复杂度获取到成员的分值,跳跃表本身通过分值排序的,所以有序查找更快,二者同时存在,是为了性能得到提升,且图中展示的成员和分值是分开展示的,实际上是公用一份的,这里是为了图的区分度:

五、资源地址
文档:《Redis 设计与实现》


被折叠的 条评论
为什么被折叠?



