大白话八股文-Redis

一.Redis的数据类型有哪些

  • 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
  • 3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。

redis的5种基础数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。

String

虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。

List

Redis 中的 List 其实就是链表数据结构的实现。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

Hash

Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。

Set

Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet 。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。

你可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。

Sorted Set(ZSet)

Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。

二.Redis为什么这么快

1.基于内存实现

Redis 将数据存储在内存中,读写操作不会受到磁盘的 IO 速度限制,所以Redis的读写速度会非常的快。

2.使用I/O多路复用模型

传统阻塞 IO ,在执行accept 、recv 等网络操作时,如遇到异常情况会一直处于阻塞状态。多路指的是多个 socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。

Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以 Redis 一直在处理事件,提升了 Redis 的响应性能。

Redis 线程不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis可以同时和多个客户端连接并处理请求,从而提升了并发性。

3.采用单线程模型

Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是多线程执行。

单线程的优势

  • 不会因为线程创建导致的性能消耗
  • 避免上下文切换引起的 CPU 消耗,没有多线程切换的开销
  • 避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题
  • 代码更清晰,处理逻辑简单

Redis6.0后引入了多线程网络IO来提升连接性能,但是数据读取仍然使用的是单线程。

4.高效的数据结构

为了追求速度,不同数据类型使用不同的数据结构速度才得以提升。每种数据类型都有一种或者多种数据结构来支撑,Redis数据类型和底层数据结构的关系如下图所示。

SDS的特性

SDS是String的底层实现结构。

低时间复杂度,SDS的len保存了已使用空间的长度,获取字符串长度的时间复杂度为O(1)
空间预分配,SDS被修改后,会被分配所需要的必须空间以及额外的未使用空间。
分配规则:如果SDS被修改后,len的长度小于1M,那么SDS将被分配和len相同长度的未使用空间。举个例子,如果 len=10,重新分配后,buf的实际长度会变为10(已使用空间)+10(额外空间)+1(空字符)=21。如果SDS被修改后,len长度大于1M,那么SDS将分配1M的未使用空间。

ZipList的特性

ZipList是List 、hash、sorted Set三种数据类型底层实现之一。

ZipList是由一系列特殊编码的连续内存块组成的顺序型的数据结构,不容易产生内存碎片,内存利用率高。
适用情景:一个列表只有少量数据,并且每个列表项是小整数或短字符串
ZipList的结构。
查找第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,时间复杂度是O(1)。而查找其他元素时,只能逐个查找,此时的时间复杂度是O(N)。
ZipList在表头有三个字段:zlbytes、zltail和zllen,分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

ZipList的缺点

插入和删除操作需要频繁的申请和释放内存,同时会发生内存拷贝,数据量大时内存拷贝开销较大。

LinkedList的特性

LinkedList是List的底层实现结构之一。

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

LinkedList的缺点

除保存数据外还需要保存prev、next两个指针,内存利用率低,LinkedList的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。

ZipList和LinkedList区别

内存使用

  • ziplist‌:通过紧凑存储数据来减少内存使用,适用于元素数量少且元素值小的场景。ziplist不存储指向上一个节点和下一个节点的指针,而是存储上一个节点的长度和当前节点的长度,从而节省内存‌。
  • linkedlist‌:每个节点都会存储指向上一个节点和指向下一个节点的指针,这会导致大量的内存碎片,因为指针本身也占用内存‌。

访问速度

  • ziplist‌:由于数据是连续存储的,ziplist在访问时可以利用CPU缓存,提高读取速度。但是,修改中间元素可能需要重构整个列表,这可能会影响性能‌。
  • linkedlist‌:不支持随机访问,要访问链表中的某个元素,必须从头节点开始遍历到目标节点,这在大型链表中可能会导致较慢的访问速度‌。

### 三级标题:跳表的数据结构设计 跳表(Skip List)是一种基于链表结构的高效数据结构,通过在链表中添加多层索引,使得查找、插入和删除操作的时间复杂度可以达到 $ O(\log n) $。LevelDB 中的跳表设计简洁而高效,其核心思想是通过分层的索引来加速查找过程。 在跳表中,每个节点可以有多个指针,这些指针分别指向不同层级的下一个节点。层级越高,节点之间的跨度越大,从而可以快速跳跃到目标节点附近。每个节点的层级是通过概率性算法随机生成的,在 LevelDB 中,节点的层级生成规则是:每次插入新节点时,以一定的概率(通常是 1/2)增加节点的层级,直到达到最大层级限制。 ### 三级标题:跳表的实现原理 在 LevelDB 中,跳表的实现主要依赖于以下几个关键点: 1. **节点结构**:跳表的节点包含键值对以及一个指针数组,该数组存储了该节点在各个层级指向的下一个节点。每个节点的层级是动态生成的,并且层级越高,节点数量越少。 2. **插入操作**:当插入一个新节点时,首先需要确定该节点的层级。然后,从最高层级开始,逐层查找并更新每一层的指针,以确保新节点能够正确地插入到跳表的相应位置。 3. **查找操作**:查找操作从最高层级开始,沿着每一层的指针逐步逼近目标节点。一旦发现当前层无法继续前进,则下降到下一层继续查找,直到找到目标节点或确定目标节点不存在[^2]。 4. **删除操作**:删除操作与插入操作类似,需要从最高层级开始查找目标节点,并更新每一层的指针以绕过被删除的节点。 ### 三级标题:跳表的优势与应用场景 跳表相比于传统的平衡树(如红黑树)具有以下优势: - **简单性**:跳表的实现相对简单,不需要复杂的旋转操作来维持平衡。 - **并发性**:跳表在多线程环境下更容易实现高效的并发操作,因为它的结构允许局部修改而不影响整体结构。 - **性能稳定性**:由于跳表的层级是随机生成的,因此在大多数情况下,它的性能表现非常稳定,能够保持 $ O(\log n) $ 的时间复杂度[^3]。 在 LevelDB 中,跳表主要用于存储内存中的 MemTable 数据。MemTable 是 LevelDB 的内存数据结构,用于缓存最近写入的数据。由于跳表支持高效的插入、删除和查找操作,因此非常适合用于 MemTable 的实现。 ### 三级标题:跳表的时间复杂度分析 跳表的查找操作的时间复杂度可以通过以下公式进行分析: $$ O\left(\frac{1}{p} \cdot \log_{\frac{1}{p}} n\right) $$ 其中,$ p $ 是节点层级增加的概率(通常为 1/2),$ n $ 是数据量。由于 $ \log_{\frac{1}{p}} n $ 是 $ \log n $ 的常数倍,因此跳表的时间复杂度通常简化为 $ O(\log n) $[^4]。 ### 三级标题:跳表的可视化示例 为了更直观地理解跳表的构建过程,可以通过一个简单的代码示例来模拟跳表的基本操作。以下是一个简化的跳表实现示例: ```python import random class SkipNode: def __init__(self, key, value, level): self.key = key self.value = value self.forward = [None] * (level + 1) class SkipList: def __init__(self, max_level, p): self.max_level = max_level self.p = p self.header = self.create_node(self.max_level, None, None) self.level = 0 def create_node(self, level, key, value): return SkipNode(key, value, level) def random_level(self): level = 0 while random.random() < self.p and level < self.max_level: level += 1 return level def insert(self, key, value): update = [None] * (self.max_level + 1) current = self.header for i in range(self.level, -1, -1): while current.forward[i] and current.forward[i].key < key: current = current.forward[i] update[i] = current current = current.forward[0] if current is None or current.key != key: new_level = self.random_level() if new_level > self.level: for i in range(self.level + 1, new_level + 1): update[i] = self.header self.level = new_level node = self.create_node(new_level, key, value) for i in range(new_level + 1): node.forward[i] = update[i].forward[i] update[i].forward[i] = node def search(self, key): current = self.header for i in range(self.level, -1, -1): while current.forward[i] and current.forward[i].key < key: current = current.forward[i] current = current.forward[0] if current and current.key == key: return current.value else: return None # 示例使用 skip_list = SkipList(5, 0.5) skip_list.insert(10, "Value10") skip_list.insert(20, "Value20") skip_list.insert(30, "Value30") print(skip_list.search(20)) # 输出: Value20 ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值