redis的底层数据结构
redis支持的数据类型有五种,字符串、列表、集合、哈希、有序集合。其中用于实现的底层数据结构有六种,简单动态字符串、整数数组、哈希表、双向链表、跳表、压缩列表。
简单动态字符串
简单动态字符串,底层使用char数组保存数据,但是会进行预分配,提前分配较大的空间,避免字符增加时每次都需要扩容,但是会占用额外空间。同时使用变量记录字符串已使用的长度,可以在O(1)时间复杂度内获取字符串的长度。
整数数组
整数数组作为集合的一种实现形式,它主要以有序、无重复的方式存储数字。底层存储的数字长度有2字节、4字节和8字节。集合中所有的数字长度必须都是一致的,以长度最长的数字的长度为准。假如一开始所有数字都是2字节,添加一个必须要4字节存储的数字之后,所有元素都需要升级成4字节进行存储。同时由于从高位向低位转变可能是不安全的,可能会造成数据截取,所以一旦升级之后并不支持再次降级。
升级的好处在于,在支持不同长度的整数的同时,降低了内存占用。当数字位数都比较短时,占用空间很小。
哈希表
哈希表用于存放KV数据。本质上是一个数组,使用哈希函数计算出key的索引值,如果对应的key不存在,就把kv插入到索引位置。当多个key索引值一样时,就会出现哈希碰撞,redis使用拉链法,将后续的节点依次插入索引位置,同时使用头插法,将后来的节点插入链表的头部。
当哈希表存储的元素数量太多时,哈希碰撞会越来越严重,导致每次查询都需要遍历链表,查询效率退化至O(n)。因此当数据量过大时,需要对哈希表进行扩容。简单来说就是新建一个长度是原来两倍的数组,将旧数组中的数据全部迁移至新数组中,这个过程被称为rehash。
假如哈希表中元素数量很多,一次性进行rehash耗时很长,会导致查询线程阻塞,因此redis采用渐进式rehash的方式进行迁移。每次查询都将从旧表上搬运一个节点的数据至新的哈希表中,同时更新也将更新在新表中。这样就导致rehash的时间平摊在之后的查询中,避免单次查询阻塞。
压缩列表
压缩列表也是一串连续的区域,因此比较节省空间。压缩列表的每一个节点包含三部分内容
- previous_entry_size 前一个节点的空间占用大小,可以通过这个字段推断前一个节点的起始位置,从而实现逆向遍历
- encoding 编码格式
- content 具体存放内容
当压缩列表中的节点数据被修改时,会引发后续所有节点的移动以及下一个节点previous_entry_size字段的改动。倘若下一个节点previous_entry_size的长度发生变化,又会导致下下个节点的变动,从而有可能导致整个列表雪崩式变动,复杂度为O(N^2),不过发生概率非常小。
redis Object
redis对外提供的数据类型(字符串、列表、哈希、集合和有序集合)都被封装成redis object。redis object记录了当前对象的类型、编码方式(使用哪种数据结构),以及指向具体实现的指针。
同时redis object 使用引用计数法对对象的引用进行计数,每次创建新的对象或者对象被新的指针引用时计数都会增加,同时指针断开时引用也会减少,当引用计数为0时,该对象就会被回收。
redis object还记录了对象最近的访问时间,从而为lru算法淘汰冷数据提供基础。