Redis数据结构底层原理

简单动态字符串(SDS)

redis是C语言的程序,但是字符串并没有采用C语言提供能的,自己写了一个字符串的结构。

redis中的字符串结构:

struct sdshdr {
 // 记录buf数组中已使用字节的数量
 // 等于SDS所保存字符串的长度
 int len;
 // 记录buf数组中未使用字节的数量
 int free;
 // 字节数组,用于保存字符串
 char buf[];
};

并且redis中字符串同样采用了C语言中的格式,就是最后一个使用空字符''/0''结束,这个过程对程序员是透明的,sds自动追加的。

为什么有现成的结构,还要自定义结构呢?原因有以下几点:

SDS结构的优点

获得字符串长度时,复杂度降低为O(1)

C语言中的字符串获取长度的时候,需要遍历所有字符,知道碰到'/0',过程中的个数是长度。复杂度为O(n),redis为了追求快速。优化了这一点,结构体中保存数据的长度,属性诶len。使得复杂度降低为O(1).

避免缓冲区溢出的风险

c语言中的字符串赋值前,需要先分配内存空间。如果改变字符串的长度,还需要考虑内存空间是否够用,如果没有提前分配空间,就会出现缓冲区溢出。为了避免这种情况,sds会先检查内存是否够用,不够用的情况下,会先分配内存空间,之后改变字符串。这样就不会出现缓冲区溢出。

减少了内存重分配的次数

c语言中的字符串改变,追加字符换时,需要增加空间,截取字符串时,又要释放空间。内存中的重分配是一个耗时的操作。为了有话这一点。sds会在追加的时候,预先分配额外的空间,而在截取的时候,不会立即释放内存。

空间预分配
  • 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性,同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。举个例子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成
    13+13+1=27字节(额外的一字节用于保存空字符)。
  • 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte

用过这种策略,不用每次追加时都要分配空间。
Redis数据结构底层原理_第1张图片

惰性空间释放

截取字符串时,不会立即释放空间,而是改变属性free的值。
Redis数据结构底层原理_第2张图片

二进制数据安全

c语言中的字符串是以‘/0’结束的。所以只要看到‘/0’,就认为该字符串已经结束了,这样就无法保存二进制数据。sds优化了这一点。
Redis数据结构底层原理_第3张图片

总结

比起C字符串,SDS具有以下优点:
1)常数复杂度获取字符串长度。
2)杜绝缓冲区溢出。
3)减少修改字符串长度时所需的内存重分配次数。
4)二进制安全。
5)兼容部分C字符串函数(因为sds会自动追加一个‘/0’)。

链表

链表的作用通常是装一组数据。C语言没有提供这种数据结构,redis自定义了该结构。

链表的结构

使用ListNode节点承装数据

typedef struct listNode {
 // 前置节点
 struct listNode * prev;
 // 后置节点
 struct listNode * next;
 // 节点的值
 void * value;
}listNode;

list作为工具类使用类似于JDK中的LinkedList类。redis中的链表是一个双向链表。

typedef struct list {
 // 表头节点
 listNode * head;
 // 表尾节点
 listNode * tail;
 // 链表所包含的节点数量
 unsigned long len;
 // 节点值复制函数
 void *(*dup)(void *ptr);
 // 节点值释放函数
 void (*free)(void *ptr);
 // 节点值对比函数
 int (*match)(void *ptr,void *key);
} list;

Redis的链表实现的特性

可以总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

总结

  1. 哪里用到了链表这种结构呢?列表键,发布与订阅,慢查询,监视器等
  2. redis中的链表是双端的,无环的。

字典

字典是一种保存键-值的数据结构。有的地方也称作映射表。c语言中没有提供这种数据结构的实现,所以redis自己实现了,java语言中的jdk已经提供了实现HashMap。

结构

哈希表的结构

typedef struct dictht {
 // 哈希表数组
 dictEntry **table;
 // 哈希表大小
 unsigned long size;
 //哈希表大小掩码,用于计算索引值
 // 总是等于size-1
 unsigned long sizemask;
 // 该哈希表已有节点的数量
 unsigned long used;
} dictht;

table组是用来保存数据的;size记录的是数组的大小,并非是数据的个数;sizemash是用来取模的,用于计算索引。used是记录字典中数据个数的。
Redis数据结构底层原理_第4张图片

哈希表节点的结构

看完了哈希的结构,下面看看哈希表节点的结构。哈希表的数据是哈希表节点,也可以看成,哈希表是保存哈希表节点的,哈希表节点才是保存真正的数据的。

typedef struct dictEntry {
 // 键
 void *key;
 // 值
 union{
 void *val;
 uint64_tu64;
 int64_ts64;
 } v;
 // 指向下个哈希表节点,形成链表
 struct dictEntry *next;
} dictEntry;

需要质疑的是值的类型,可以说是一个指针,我对c语言不了解,我的猜测是保存了指针,那么哈希表就可以存任意类型的数据,只要赋值指针即可。除了指针,还可以保存整数,这个应该是一个优化操作吧。另外还有一个指向下一个节点的指针,用来将哈希值相同的节点组成链表。
Redis数据结构底层原理_第5张图片

字典结构

和Java中的字典结构不同,在java中,哈希表其实就已经是结构了,但是在redis的实现了有包了一层:

typedef struct dict {
 // 类型特定函数
 dictType *type;
 // 私有数据
 void *privdata;
 // 哈希表
 dictht ht[2];
 // rehash索引
 //当rehash不在进行时,值为-1
 in trehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

看到了,有一个dictht ht[2],相当于连个哈希表结构,为什么这么做呢?直接向Java中似的,用哈希表不行么?下面详细分析下。typeprivdata属性针对的是不同类型的数据,用不同的函数即参数,例如Java中,基本类型的比较和对象的比较可以用一个吗?不行吧,所以要分开,上面两个就是区分用的;哈希表数组,为啥用两个呢?保存数据的时候是占用一个的,另一个是迁移数据时用的,trehashidx就是保存迁移进度的,如果没有rehash,那么值是-1;
Redis数据结构底层原理_第6张图片

哈希算法

如果添加一个键值对,底层是如何运作的呢? 首先通过Hash算法计算出一个哈希值,再用这个值和数组大小取模,得出数组中的索引位置。将数据出入到该位置。
如果该位置已经有数据了,不是冲突了吗? redis采用的链表解决冲突,如果索引中有多个数据,这里的数据是哈希表节点,这些节点会以链表的结构存储。因为是单向链表,为了提升添加效率,采用的头插法。就是添加在头部。防止插入到尾部,遍历整个链表。

rehash

rehash主要针对的是对数组的的扩容和缩容操作,将数据重新映射到新的数组中。

数据迁移

当数组需要扩容或缩容时,会给ht[1]分配空间,将ht[0]的数据迁移到ht[1]中,迁移完成后,将ht[1]赋值给ht[0],释放ht[1]的空间。也就是说ht[0]是放数据的,ht[1]是用来扩容的工具数组。

新数组的大小分配
  • 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的(2的n次方幂);
  • 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次方幂。

举例:数据个数是5,那么扩容之后的大小是:5*2=10,10之后的第一个2的n次方幂是16,所以扩容之后的大小是16。

触发条件
  • 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
  • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
  • 当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作

为什么条件不同呢?在执行这两个命令的时候,大部分操作系统会采用写时复制技术(copy-on-write)技术提升效率,本质就是,不在原数据修改,复制一份副本。修改副本之后替换原数据。在这个过程中,为了避免频繁的扩展操作。需要提高负载因子。

渐进式rehash

上面的迁移过程并不是一次性完成的,这样会阻塞redis的服务线程。而是渐进的,分多次的。
以下是哈希表渐进式rehash的详细步骤:

  1. ht[1]分配空间,让字典同时持有ht[0]ht[1]两个哈希表。
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表该索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。
因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。例如,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类。
另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。
这里有个问题了,能操作到的可以迁移,如果某个索引的位置的数据一直用不到,岂不是永远也迁移不了了吗

总结

字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希
键。

  • Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
  • 当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
  • 哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表。
  • 在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的

跳跃表

跳跃表示一种有序的数据结构。通过在每个节点中维持多个指向其他节点的指针,实现快速访问。
Redis数据结构底层原理_第7张图片
最左边是zskiplist结构:

  • header:指向跳跃表的表头节点。
  • tail:指向跳跃表的表尾节点。
  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

后面的是zskiplistNode结构:

  • 层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
  • 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
  • 分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
  • 成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。

跳跃表节点结构

typedef struct zskiplistNode {
 // 层
 struct zskiplistLevel {
 // 前进指针
 struct zskiplistNode *forward;
 // 跨度
 unsigned int span;
 } level[];
 // 后退指针
 struct zskiplistNode *backward;
 // 分值
 double score;
 // 成员对象
 robj *obj;
} zskiplistNode;

层:层是一个数组,在创建节点的时候会随机分配一个1~32之间的值作为该节点的层数组大小。
层中的前进指针:用来连接下一个节点的同一层,如果下一个节点没有该层,就跳过。
层中的跨度:这个属性主要是用来计算排位,就是从头结点到目标节点经过了几步。

后退指针:用于从后向前遍历的
分值:跳跃表的节点是按照分值的大小从小到大排序的。
成员对象:和分值成对出现的数据

Redis数据结构底层原理_第8张图片

跳跃表结构

typedef struct zskiplist {
 // 表头节点和表尾节点
 structz skiplistNode *header, *tail;
 // 表中节点的数量
 unsigned long length;
 // 表中层数最大的节点的层数
 int level;
} zskiplist;

跳跃表的结构很简单,记录了节点个数,最大层数,表头,表位指针。

总结

  • 跳跃表是有序集合的底层实现之一。
  • Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
  • 每个跳跃表节点的层高都是1至32之间的随机数。
  • 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
  • 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。

整数集合

整数集合,顾名思义:一个集合中只有整数。是redis集合数据结构的一种优化,条件是,保存的数据是整数,同时,数据的个数不多。

结构

typedef struct intset {
 // 编码方式
 uint32_t encoding;
 // 集合包含的元素数量
 uint32_t length;
 // 保存元素的数组
 int8_t contents[];
} intset;

encoding:数据类型;length:元素个数;contents:数组里面的对象是按照从小打到的顺序存储的,并且没有重复元素。
元素数组标记为int8_t,实际它并不存储int8_t的值,真正类型取决于encoding属性。

  • 如果encoding属性的值为INTSET_ENC_INT16,那么contents就是一个
    int16_t类型的数组,数组里的每个项都是一个int16_t类型的整数值(最小值
    为-32768,最大值为32767)。
  • 如果encoding属性的值为INTSET_ENC_INT32,那么contents就是一个
    int32_t类型的数组,数组里的每个项都是一个int32_t类型的整数值(最小值
    为-2147483648,最大值为2147483647)。
  • 如果encoding属性的值为INTSET_ENC_INT64,那么contents就是一个
    int64_t类型的数组,数组里的每个项都是一个int64_t类型的整数值(最小值
    为-9223372036854775808,最大值为9223372036854775807)。

升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。
升级整数集合并添加新元素共分为三步进行:
1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
2)将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
3)将新元素添加到底层数组里面。

升级的好处:

  1. 灵活,不用束缚于类型;添加类型不同的,也会自定适应新元素。
  2. 节约内存。多大的数据开辟多大的内存,不会一开始就开辟到最大。

注意:不支持降级

总结

  • 整数集合是集合键的底层实现之一。
  • 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
  • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存。
  • 整数集合只支持升级操作,不支持降级操作。

压缩列表

注意,压缩列表并不是一种数据结构,它是一中存储结构。本质是连续的内存块
它是列表和哈希键的底层实现结构之一。条件是数量小,数据要么是整数,要么是短的字符串。

结构

在这里插入图片描述

  • 列表zlbytes属性,表示压缩列表的总长为80字节。
  • 列表zltail属性,表示头结点到尾结点的偏移量。如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量60,就可以计算出表尾节点的地址。
  • 列表zllen属性,表示压缩列表中节点的个数。
  • 列表entryX属性,表示压缩列表的节点对象。
  • 列表zlend属性,表示压缩列表的末端标记。

压缩列表节点的结构

-
(1)previous_entry_length:保存的是前一个节点的占用的字节数。
如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。
如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制值254),而之后的四个字节则用于保存前一节点的长度。
该属性还有一个作用,可以计算出前一个节点的位置。因为已经直达了长度,即即内存的偏移量。

(2)encoding:编码,表示数据的类型;保存的数据可以是字节数组还可以是整数。编码是不一样的。

Redis数据结构底层原理_第9张图片
(3)content:保存的数据,字节数组或者整数

数据保存的是字节数组:
Redis数据结构底层原理_第10张图片

数据是整数:
Redis数据结构底层原理_第11张图片

连锁更新

压缩列表节点中有个属性是previous_entry_length,并不是一个定长度。如果插入一个节点,该节点的长度大于254了,那么后面的节点将更新重新分配内存,占5个字节的长度。如果这个节点大超过了254,后面的节点也会重复该操作。这连串的扩展内存,称为连锁更新。

会造成性能的影响吗?影响不大,原因是:触发的条件,其实很苛刻,在254左右的长度,另外,只有数据量小的时候才会使用压缩列表。所以数据量小,扩展内存消耗不大。

总结

  • 压缩列表是一种为节约内存而开发的顺序型数据结构。
  • 压缩列表被用作列表键和哈希键的底层实现之一。
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
  • 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。

对象

之前介绍了redis中的几种底层数据结构,redis并没有直接使用这些实现数据库,而是又封装了一层为redis对象,就好像哈希表的结构一样,并没有直接只用哈希表dictht,而是再一次封装为了dict

redis对象

结构

 // 类型
 unsigned type:4;
 // 编码
 unsigned encoding:4;
 // 指向底层实现数据结构的指针
 void *ptr;
 // ...
} robj;

type:表示对象的类型,encoding:表示编码,即底层的数据结构类型;*ptr:数据

type 类型

redis对象的类型:
Redis数据结构底层原理_第12张图片
使用Type命令可以查看该对象的类型

# 
键为字符串对象,值为字符串对象
redis> SET msg "hello world"
OK
redis> TYPE msg
string
# 
键为字符串对象,值为列表对象
redis> RPUSH numbers 1 3 5
(integer) 6
redis> TYPE numbers
list
# 
键为字符串对象,值为哈希对象
redis> HMSET profile name Tom age 25 career Programmer
OK
redis> TYPE profile
hash
# 
键为字符串对象,值为集合对象
redis> SADD fruits apple banana cherry
(integer) 3
redis> TYPE fruits
set
# 
键为字符串对象,值为有序集合对象
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
redis> TYPE price
zset

不同的对象对应不同的类型
Redis数据结构底层原理_第13张图片
注意:type key这种命令不是查看的键的类型,而是查看的是值的类型。

encoding 编码

编码的作用是标注底层实现:
Redis数据结构底层原理_第14张图片
之前介绍的都是底层实现,而具体到对象用的是那种数据结构,就是encoding决定的了。

和类型的对应关系:
Redis数据结构底层原理_第15张图片
同样可以使用命令,查看对象的编码

redis> SET msg "hello wrold"
OK
redis> OBJECT ENCODING msg
"embstr"
redis> SET story "long long long long long long ago ..."
OK
redis> OBJECT ENCODING story
"raw"
redis> SADD numbers 1 3 5
(integer) 3
redis> OBJECT ENCODING numbers
"intset"
redis> SADD numbers "seven"
(integer) 1
redis> OBJECT ENCODING numbers
"hashtable"

Redis数据结构底层原理_第16张图片
Redis数据结构底层原理_第17张图片
为什么一种类型有多中底层实现呢?这是redis做的优化。在特定情况下使用特定的结构效率更高,就好像现实生活中,去附近超市买东西,走着就行了,要是到市里办事,就要开车了,要是跨省,就要飞机或者火车了。根据需要选择开销小的,没必要去超市直接选择开车。

字符串对象

在这里插入图片描述
字符串的底层实现有三种,下面详细看下具体的实现。

int

如果保存的值是数值,并且是long范围内的,那么将会使用int编码存储。
Redis数据结构底层原理_第18张图片

raw

如果保存的是字符串,长度是是大于32字节的,那么采用raw编码。
Redis数据结构底层原理_第19张图片

embstr

这种情况是存小长度的字符串,或者是long范围之外的数值型。
在这里插入图片描述

raw和embstr的区别

raw有两次分配内存的操作,分配redis对象,分配sds对象;embstr只有一次内存分配,redis对象和sds对象是连续的内存空间。这样的好处就是快,效率高,分配和释放都只要一次,另外,由于是连续的空间,内存利用率更高了。

注意:只有整数,还需是long范围内的才能用int编码,如果是浮点型的,会使用embstr编码。也就是说浮点型会以字符串的形式保存,之后转换为浮点型进行操作,之后又转换为字符串存储。

类型间的转换

int编码的值追加字符串后悔转换为raw编码
embstr编码的字符串是没有操作方法的,需要转换为raw才可以。可以认为embstr编码是只读的。

总结

Redis数据结构底层原理_第20张图片

列表对象

列表对象的底层是压缩表或者是链表。
在这里插入图片描述

压缩表

使用压缩表的时候,每一个entryNode里保存一个数据。这里我想到了three这个字符串并不是sds结构,只是字节数组,因为压缩表节点中的content只能保存数值或者字节数组
Redis数据结构底层原理_第21张图片

链表

Redis数据结构底层原理_第22张图片

注意:这里的string对象已经介绍过了,是redis对象+sds对象。
Redis数据结构底层原理_第23张图片

转换条件

  • 列表对象保存的所有字符串元素的长度小于64字节;
  • 列表对象保存的元素数量小于512个;
  • 不能满足这两个条件的列表对象需要使用linkedlist编码。

哈希对象

哈希对象的底层是亚索表或者是字典
在这里插入图片描述

压缩表

当底层是压缩表时,键值对的连续存储的。
Redis数据结构底层原理_第24张图片

字典

注意字典的键和值都是字符串对象。并非字节数组。这一点一定要分清。
Redis数据结构底层原理_第25张图片

转换条件

和列表的转换条件一致。

  • 列表对象保存的所有字符串元素的长度小于64字节;
  • 列表对象保存的元素数量小于512个;

集合对象

集合对象底层是哈希表或者数组集合在这里插入图片描述

数组集合

Redis数据结构底层原理_第26张图片

哈希表

使用哈希表,键保存的是数据,键值则是null.
Redis数据结构底层原理_第27张图片

转换条件

当集合对象可以同时满足以下两个条件时,对象使用intset编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过512个。

不能满足这两个条件的集合对象需要使用hashtable编码。

有序集合对象

有序集合对象使用的压缩表或者跳跃表和字典的组合。
在这里插入图片描述

压缩表

压缩表作为底层结构的时候是如何维持分数的从小到大的顺序呢?新加入一个数据,怎么分配的?这个不懂了
Redis数据结构底层原理_第28张图片
在这里插入图片描述

跳跃表和字典的组合

redis在跳跃表和字典的基础上有封装了一层。zset对象。

typedef struct zset {
 zskiplist *zsl;
 dict *dict;
} zset;

跳跃表保证了数据的有序性,字典保证了取分数时的复杂度是O(1)。
Redis数据结构底层原理_第29张图片

Redis数据结构底层原理_第30张图片

转换条件

当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:

  • 有序集合保存的元素数量小于128个;这个条件和上面的不一样,其他的是512个。
  • 有序集合保存的所有元素成员的长度都小于64字节;

不能满足以上两个条件的有序集合对象将使用skiplist编码。

其他方面

关于redis命令的思考

客户端执行一个命令,redis服务端进行对应的操作,底层其实就是调用对象的方法。对于一个对象而言,底层的结构实现是不同的,不同的实现有相同的功能,比如列表的获取长度命令:LLen,列表的底层可能是亚索表或者是链表,每个结构都实现了返回长度的方法。

内存回收

redis实现了自己的内存回收器。还记得对象的结构吗?还有一个属性用来引用计数。

typedef struct redisObject {
 // ...
 // 
引用计数
 int refcount;
 // ...
} robj;

当对象创建的时候,初始化为1,之后每被引用一次,该值就会加一,不再被引用就会被减一,等到值为0,就可以释放该对象。

共享对象

注意只有保存了整数的字符串对象是贡献的,保存了字符串的字符串对象不是共享的。
Redis数据结构底层原理_第31张图片
为什么是这样的呢?因为共享对象,就表示该数据是一样的,验证数据可能是O(n)。虽然内存共享可以节省内存,但是受限于CPU的时间。

总结

  • Redis数据库中的每个键值对的键和值都是一个对象。
  • Redis共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可,以在不同的使用场景上优化对象的使用效率。
  • 服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。
  • Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
  • Redis会共享值为0到9999的字符串对象。
  • 对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。

你可能感兴趣的:(redis)