Redis之底层数据结构

Redis 数据结构包括哈希表、SDS、压缩列表、quickList、哈希表和跳表。哈希表解决冲突采用链式哈希,通过渐进式rehash避免阻塞。SDS是可变字符串,适用于小字符串,避免了C字符串的不可变性。压缩列表适用于小数据,但修改可能导致连锁更新。quickList结合了ziplist和linkedlist,减少内存消耗和更新损耗。跳表用于sorted set,提供快速查找。
摘要由CSDN通过智能技术生成

一 Redis数据结构

Redis底层数据结构有三层意思:

  • 从Redis本身数据存储的结构层面来看,Redis数据结构是一个HashMap。
  • 从使用者角度来看,Redis的数据结构是String,List,Hash,Set,Sorted Set。
  • 从内部实现角度来看,Redis的数据结构是ict,sds,ziplist,quicklist,skiplist,intset。

这五种数据类型分别对应以下几种数据结构:
在这里插入图片描述
如图所示:

  • String的底层是:简单动态字符串
  • List的底层是:双向链表和压缩列表。
  • Hash的底层是:压缩链表和哈希表
  • Set底层是:整数数组和哈希表。
  • Sort Set底层是:压缩链表和跳表。

1.Redis哈希表如何实现

  Redis是一个k-v的数据库。为了实现从key到value的快速访问,Redis使用了一个哈希表来保存所有的键值对。

  • 一个哈希表,本身就是一个数组,数组的每个元素称为一个哈希桶。
  • 不管是键类型还是值类型,哈希桶内的元素保存的都不是值本身,而是指向具体值的指针。

  如下图中可以看到,哈希桶中的entry元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到:
在这里插入图片描述
因为这个哈希表保存了所有的键值对,所以,它也叫做全局哈希表。

  • 哈希表的最大好处就是让我们可以用O(1)的时间复杂度来快速查找到键值对----我们只需要计算键的哈希值,就可以知道它对应的哈希桶位置,然后就可以访问相应的entry元素。
  • 这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。也就是说,不管哈希表里有10万个键还是 100 万个键,我们只需要一次计算就能找到相应的键。
  • 也就是说,整个数据库就是一个全局hash表,而hash表的时间复杂度就是O(1),只需要计算每个键的hash值,就知道对应的hash桶的位置,定位桶里面的entry找到对应数据,这个也是redis块的原因之一。

2.Redis哈希冲突

1,什么是hash冲突?

  哈希冲突,也就是指,两个key的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。毕竟,哈希桶的个数通常要少于key的数量,hash冲突是不可避免。

2,hash冲突如何解决?

  Redis解决哈希冲突的方式,就是链式哈希:所谓的链式哈希,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间一次用指针连接。如下图:
在这里插入图片描述
  图中的entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突。此时,entry1 元素会通过一个next指针指向 entry2,同样,entry2 也会通过next指针指向entry3。这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。

3,渐进式rehash

  上面说过,哈希冲突的解决办法是hash链表,但是hash链表中如果hash冲突链越来越长,肯定会导致redis的性能下降,解决办法是什么尼?也就是渐进式rehash。
  为了使得rehash操作更加高效,redis默认使用了两个全局哈希表:哈希表1和哈希表2.一开始,当你刚插入数据时,默认使用哈希表1,此时的哈希表2并没有被分配空间。随着数据逐步增多,redis开始执行rehash,这个过程分为三步:

  1. 把哈希表2分配更大的空间,比如是当前哈希表1大小的两倍
  2. 把哈希表1中的数据重新映射并拷贝到哈希表2中
  3. 释放哈希表1的空间

  至此,我们就可以从哈希表1切换到哈希表2,用增大的哈希表2保存更多的数据,而原来的哈希表1操作留作下一次rehash扩容备用。

  上面步骤有一个问题,那就是第二步涉及大量的数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成redis线程阻塞,无法服务其他请求。此时,redis就无法快速访问数据了。为了避免这个问题,redis采用了渐进式rehash
  简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的entries。如下图所示:
在这里插入图片描述
  这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。另外,渐进式rehash执行时,除了根据键值对的操作来进行数据迁移,redis本身还会有一群定时任务在执行rehash,如果没有键值对操作,这个定时任务会周期性的搬移一些数据到新的哈希表中。

二 Redis中value的内部实现数据结构

  redis是通过对象来表示存储的数据的,redis 也是键值对存储的方式,那么每存储一条数据,redis至少会生成2个对象,一个是redisObject,用来描述具体数据的类型的,比如用的是那种数据类型,底层用了哪种数据结构,还有一个对象就是具体存储的数据。 这个存储对象数据就是通过redisObject这个对象的指针来指引的。
  由于不同的数据类型,是有不同的内部实现且互相交叉的,具体如图所示:
在这里插入图片描述
  下面我们将分开介绍简单动态字符串,双向列表,压缩列表,哈希表,跳表和整数数组。

1.RedisObject对象解析

  上面说过redis本身在存储数据的时候会产生一个redisObject对象用来存储当前key对应的value的数据类型。其结构如下:

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;

其中具体的值含义如下:
1,type: 字段表示value的对象类型,占4byte。目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。在执行 type key的时候能查看到对应的类型:
在这里插入图片描述
2,encoding 表示对象的内部编码,占4个比特。对于redis支持的每种类型都至少有两种编码,对于字符串有int、embsre、row三种
通过encoding属性,redis可以根据不同的使用场景来对对象使用不同的编码,大大提高的redis的灵活性和效率。命令:object encoding key;
在这里插入图片描述
3,lru: 记录对象最后一次被访问的时间,当配置了maxmemory和maxmemory-policy=valatile-lru | allkeys-lru时,用于辅助LRU算法删除键数据。可以使用 object idletime {key} 命令在不更新lru字段情况下查看当前键的空闲时间。开发提示:可以使用scan + object idletime 命令批量查询哪些键长时间未被访问,找出长时间不访问的键进行清理降低内存占用。
4,refcount字段: 记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount {key} 获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。具体细节见之后共享对象池部分。
5,ptr字段: 与对象的数据内容相关,如果是整数直接存储数据,否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作。开发提示:高并发写入场景中,在条件允许的情况下建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数从而提高性能。eg:一个代表string的robj,它的ptr可能指向一个sds结构;一个代表list的robj,它的ptr可能指向一个quicklist。

2.SDS:简单动态字符串

1,实现原理

  在c语音中,定义的字符串是不可被修改的,因此redis设计了可变的字符串长度对象,接SDS(simple dynamic string),实现原理:

struct sdshdr{
   
    //记录buf数组中已存的字节数量,也就是sds保存的字符串长度
    int len;
 
    // buf数组中剩余可用的字节数量
    int free;
 
    //字节数组,用来存储字符串的
    char buf[];
}

参数解析:

  1. len : 保存的字符串长度。获取字符串的长度就是O(1)
  2. free:剩余可用存储字符串的长度
  3. buf:保存字符串

这样设计的优点:
1):当用户修改字符串时sds api会先检查空间是否满足需求,如果满足,直接执行修改操作,如果不满足,将空间修改至满足需求的大小,然后再执行修改操作
2):空间预分配

  1. 如果修改后的sds的字符串小于1MB时(也就是len的长度小于1MB),那么程序会分配与len属性相同大小的未使用空间(就是再给未使用空间free也分配与len相同的空间) 例:字符串大小为600k,那么会分配600k给这个字符串使用,再分配600k的free空间在那。
  2. 惰性空间释放,当缩短sds的存储内容时,并不会立即使用内存重分配来回收字符串缩短后的空间,而是通过free将空闲的空间记录起来,等待将来使用。真正需要释放内存的时候,通过调用api来释放内存
  3. 通过空间预分配操作,redis有效的减少了执行字符串增长所需要的内存分配次数
  4. 如果修改后sds大于1MB时(也就是len的长度大于等于1MB),那么程序会分配1MB的未使用空间 例:字符串大小为3MB,那么会分配3MB给这个字符串使用,再分配1MB的free空间在那。
2,不同的编码实现

查看key的编码:object encoding key
在这里插入图片描述
1)int (REDIS_ENCODING_INT)整数值实现:
  存储的数据是整数时,redis会将键值设置为int类型来进行存储,对应的编码类型是REDIS_ENCODING_INT。
2)embstr(REDIS_ENCODING_EMBSTR)
  由sds实现 ,字节数 <= 39。存储的数据是字符串时,且字节数小于等于39 ,用的是embstr
优点:
1、创建字符串对象由两次变成了一次
2、连续的内存,更好的利用缓存优势
缺点:
1、由于是连续的空间,所以适合只读,如果修改的话,就会变成raw
2、由于是连续的空间,所以值适合小字符串

3)raw (REDIS_ENCODING_RAW)
  由sds实现。字节数 > 39

3.双向链表(LinkedList)

1,List结构:
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;
其中:
	head:表头节点
	tail:表尾节点
	len:包含的节点数量
	(*dup)函数:节点值复制函数
	(*free)函数:节点值释放函数
	(*match)函数:节点值比较函数,比较值是否相等
2,ListNode链表节点结构:
typedef  struct listNode {
   
    struct listNode *prev;
    struct listNode *next;
    void *value;  
} listNode;
其中
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值