redisObject
Redis存储的数据都使用redisObject来封装,包括string,hash,list,set,zset在内的所有数据类型。
简单来说,就是将key-value封装成对象,key是一个对象,value也是一个对象
typedef struct redisObject{ // 对象的类型 unsigned type 4:; // 对象的编码格式 unsigned encoding:4; // 指向底层实现数据结构的指针 void * ptr; //LRU计时时钟 lru:REDIS_LRU_BITS //引用计数器 int refcount; }robj; String
Redis 的字符串是动态字符串,是可以修改的字符串
SDS
struct sdshdr{ //记录buf数组中已使用字节的数量 //等于 SDS 保存字符串的长度 int len; //记录 buf 数组中未使用字节的数量 int free; //字节数组,用于保存字符串 char buf[]; } 常数复杂度获取字符串长度
对于C语言求字符串长度,需要遍历字符串,对于sds已经记录了当前字符串长度,时间复杂度为O(1)
杜绝缓冲区溢出
C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。
减少修改字符串的内存重新分配次数
空间预分配:
对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
惰性空间释放:
对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用,相当于打个标记
编码
- int 编码:保存的是可以用 long 类型表示的整数值。
- raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
- embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读
Redis中对于浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型
字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间
zipList
压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构
- previous_entry_ength 记录压缩列表前一个字节的长度
- encoding 节点的content的内容类型以及长度
- content:保存节点的内容
压缩列表从表尾节点倒序遍历,首先指针通过zltail偏移量指向表尾节点,然后通过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表,有效地减少了内存的浪费
skipList
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。具有如下性质:
1、由很多层结构组成;
2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
3、最底层的链表包含了所有的元素;
4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
①、搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
②、插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k层。
③、删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
List
双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值
编码:
- 在列表元素较少的情况下会使用一块连续的内存存储,形成的结构是
ziplist,也即是压缩列表 - 列表保存元素个数小于512个,每个元素长度小于64字节
- 列表元素较多的时候,会将各个zipList串起来,形成quickList
- 上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置
Hash
字典,用于保存键值对的抽象数据结构
编码
哈希对象的编码可以是 ziplist 或者 hashtable
ziplist:
hashtable:
- 列表保存元素个数小于512个
- 每个元素长度小于64字节
不能满足这两个条件的时候使用 hashtable 编码。第一个条件可以通过配置文件中的 set-max-intset-entries 进行修改
Redis 的字典使用哈希表作为底层实现,有两个哈希表:
- ht[0]:用于存放真实的key-vlaue数据
- ht[1]:用于扩容(rehash)
解决哈希冲突:方法是链地址法,通过字典里面的 *next 指针指向下一个具有相同索引值的哈希表节点
触发扩容的条件
- 服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。
- 负载因子 = 哈希表已保存节点数量 / 哈希表大小
渐近式 rehash
扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的
intset
Redis用于保存整数值的集合抽象数据类型,不会重复且有序(从小到大)
保存类型为int16_t、int32_t ,int64_t 的整数值
升级操作:将int16_t升级到32,或者64,节省内存
不支持降级操作
Set
String 类型的无序键值对,特殊的字典,值为null,值不能重复
集合对象编码是intset,hashtable
当集合同时满足以下两个条件时,使用 intset 编码:
1、集合对象中所有元素都是整数
2、集合对象所有元素数量不超过512
不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置
zset
有序集合
编码:
当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
- 保存的元素数量小于128字节;
- 保存的所有元素长度都小于64字节。
不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改
来源:https://blog.csdn.net/LJJZJ/article/details/98766576