面经攻略:详谈Redis常见数据类型

👉本篇速览

早在最开始学Redis的时候,我们就学到了这九种数据类型:String、Hash、List、Set、Zset、BitMap、HyperLogLog、GEO、Stream,但其实在学的时候并不了解它的底层是怎么存储这些数据,而不同的数据类型又有哪些应用的场景,本文将分别讲述这九种数据类型的底层实现和应用场景,但是不会去具体讲解这九种数据类型的使用或者说是指令

Tips:文章很长,建议大家静心攻读,相信会有不错的收获!也可以收藏起来,重复去看和理解(骗收藏hhh)


1️⃣ String

我们不难发现Redis的数据类型中没有char,int,double……这种数据类型,因为Redis的String不仅可以是字符串,还能是数字(整数或者浮点数),就让我们来一起看看是如何做到兼容的:

🍕 SDS-简单动态字符串

String 类型的底层的数据结构实现主要是靠 int 和 SDS,SDS:简单动态字符串

🍔 C语言字符数组

在讲解SDS之前我们先谈谈为什么不使用char型数组(Redis使用C语言写的,此处的char型数组是指char*):

  1. char型数组判断字符串长度有局限性且获取字符串长度的时间复杂度高 C语言使用strlen判断字符串长度的函数是对字符数组进行遍历,找到\0,那么对于字符串中有\0的,就会遍历提前结束,得到的长度并不准确 刚刚也提到,是对字符数组进行遍历,那么得到长度的时间复杂度就是O(N)

  2. char型数组容易缓冲区溢出:C语言的库提供的字符串操作函数并不安全,因为这些函数的底层并不会判断缓冲区大小是否够用,因此会出现缓冲区溢出进而程序崩溃


🍟 SDS数据结构

其实SDS的实现就是把字符数组封装成了一个能够成熟使用的对象,用C语言的话来说,就是一个结构体,我们来看看结构的中的成员变量

该结构体中的字段有:

  1. len:记录字符串的长度,这样就不需要全表遍历去获得字符串的长度,获取字符串长度的时间复杂度就:O(N) -> O(1)

  2. alloc:分配给字符数组的空间长度,通过alloc - len可以计算出剩余的空间大小,方便后续的扩容,也就是说在做操作的时候会判断剩余的空间是否够用,这样也就防止了缓冲区溢出的问题

  3. flags:表示SDS的类型,可以理解为type

  4. buf[]:字节数组,也就是真正存储数据的数组,除了可以保存字符串还能保存二进制数据


🌭 存储数据的流程

对于存入的key-value的键值对,会把value的信息封装成一个RedisObject,然后编码为SDS存入内存:

​如果字符串是一个数值,数值可以使用long类型存储,例如666.66,那么它的RedisObject就为:

首先是字符串形式,因此Type:String,然后encoding有三种类型:

  • int:用于编码数值类型的数据

  • raw:用于编码数据量大于指定字节的字符串数据

  • embstr:用于编码数据量小于指定字节的字符串数据

  • 这个指定的字节,与redis的版本有关系: redis 2.+ 是 32 字节 redis 3.0 - 4.0 是 39 字节 redis 5.0 是 44 字节

那我们再来讨论字符串长度小于指定字节,也就是说使用embstr编码的字符串时,它的RedisObject就为:

编码得到SDS,并把这个数据结构存到内存中:

那么,既然分出了不同字节不同编码,两种编码方式有什么区别呢?

我们先看看内存图,我们可以发现:embstr的RedisObject和SDS空间是连在一起的,而raw编码的二者是两块不连续的内存

### 跳表的数据结构和实现原理 跳表(Skip List)是一种基于链表的数据结构,它通过增加多层索引来提升查找效率,从而解决了传统链表查找效率低的问题。跳表通过多层索引结构,将查找、插入和删除的时间复杂度降低到平均 O(log n) 的水平。 #### 数据结构定义 Redis 中跳表的实现定义如下: 跳表节点的结构体定义如下: ```c typedef struct zskiplistNode { // 存储的元素 sds ele; // 存储的分数 double score; // 指向上一个节点的前向指针,用于反向遍历 struct zskiplistNode *backward; // 后向指针,是一个包含0-32个指针的数组 struct zskiplistLevel { // 后向指针 struct zskiplistNode *forward; // 跨度 unsigned long span; } level[]; } zskiplistNode; typedef struct zskiplist { // 跳表头节点和尾节点 struct zskiplistNode *header, *tail; // 跳表节点个数 unsigned long length; // 当前跳表的最大层级 int level; } zskiplist; ``` 上述定义中,每个节点的 `level` 是一个动态数组,用于存储不同层级的后向指针和跨度信息。这种结构使得跳表能够通过跳跃式查找快速定位目标节点。 #### 实现原理 跳表的核心思想是通过多层索引结构来加速查找操作。在跳表中,每个节点可以存在于多个层级中,层级越高,索引的步长越大。查找时,从最高层开始,逐步向下查找,直到找到目标节点或确定目标不存在。 跳表的插入和删除操作需要维护多层索引的平衡性。为了保证跳表的性能,Redis 使用了一个随机函数来决定新插入节点的层级: ```java private int randLevel() { int level = 1; // 随机生成层级,概率为 0.5 while (Math.random() < 0.5f && level < MAX_LEVEL) { level++; } return level; } ``` 该函数通过随机化的方式决定新节点的层级,从而避免了跳表退化为普通链表的情况。随机化策略使得跳表的层级分布接近理想状态,从而保证了平均 O(log n) 的时间复杂度 [^3]。 #### 跳表的优势 跳表相比其他数据结构具有以下优势: 1. **查找效率高**:跳表的查找、插入和删除操作的时间复杂度为 O(log n),比传统链表的 O(n) 更高效。 2. **实现简单**:跳表的实现比平衡树(如红黑树)更简单,不需要复杂的旋转操作。 3. **空间开销可控**:跳表的额外空间主要用于存储多层指针,平均每个节点增加的指针数为 O(log n)。相比红黑树,跳表的空间开销更可控 [^4]。 4. **支持范围查询**:跳表支持高效的范围查询操作,这在某些应用场景中非常有用。 #### 应用场景 跳表广泛应用于需要高效查找、插入和删除操作的场景。例如,Redis 使用跳表实现了有序集合(Sorted Set),通过跳表的特性支持高效的范围查询和排名操作。此外,跳表还可以用于数据库索引、缓存系统等场景。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值