首页 > 数据库 >Redis底层数据结构

Redis底层数据结构

时间:2023-07-13 23:45:10浏览次数:39  
标签:元素 Redis 索引 查找 哈希 数据结构 底层

Redis是什么?

Redis是一个键值数据库,以“快”著称

Redis是为什么这么快?

我们都知道Redis很快,它在接收到一个键值对数据后,能以微妙级别的速度找到数据并快速完成操作。数据库这么多,为啥 Redis 能有这么突出的表现呢?

一方面,这是因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快。

另一方面,这要归功于它的数据结构。这是因为,键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础,也是Redis为什么这么"快"的原因所在了。

Redis的值类型和数据结构

说到Redis的数据结构我们先来说说Redis值的类型,也就是数据的保存形式,Redis值的类型分为5种,分别是String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)。但Redis的底层数据结构简单来说大致有6种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。它们和数据类型的对应关系如下图所示:


从上面的图中我们可以知道,只有String类型的底层只有一种数据结构,也就是简单动态字符串,其他四种集合数据类型底层都是2种数据结构。

Redis键和值的关系

Redis为了保存所有的键值对关系,默认使用了2个全局哈希表(哈希表1和哈希表2循环使用),哈希表的好处自然不用多说了,查询复杂度是O(1),哈希表其实就是数组,数组的每个元素称为一个哈希桶,哈希桶中元素保存的并不是值本身,而是指向具体值的指针,这也就是为什么集合数据类型数据也可以很方便保存的原因了,我们只需要根据*key计算一次哈希值(计算哈希值其实可以理解为有一个哈希算法,将key传入,然后返回给你一个计算后的值),然后Redis会将这个根据哈希桶的数量选择一个哈希桶将哈希值和哈希桶映射在一起,以后就能通过哈希值直接找到相应的哈希桶了(ps:前提是没有哈希冲突)。

哈希桶中的 entry 元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到


Redis哈希冲突

现在你是不是有些疑问,那两个key计算后的哈希值冲突了咋办昵?
首先来说什么是哈希冲突:哈希冲突指两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中,当你往Redis中存储了大量的数据之后,哈希冲突是在所难免的,此时Redis会采用链式哈希来"暂时"解决一下这个问题,链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

如下图所示:entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突。此时,entry1 元素会通过一个next指针指向 entry2,同样,entry2 也会通过next指针指向 entry3。这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。

但是,这里依然存在一个问题,哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。对于追求“快”的 Redis 来说,这是不太能接受的。

所以,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。那具体怎么做呢?

这时我在上面提起的Redis默认使用了2个哈希表,其中哈希表2在未使用时是不会分配内存空间的,但是当哈希冲突过多时Redis采用了渐进式rehash方案来处理,那么什么是渐进式rehash呢?

rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。那具体怎么做呢?

这个过程分为三步:

给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
释放哈希表 1 的空间。
到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。
这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,因为Redis的工作线程只有1个,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。

这也就是为什么要用到 “渐进式” rehash了,就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:

这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

Redis各数据类型的操作效率

对于 String 类型来说,找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是它的复杂度了(ps:哈希冲突不考虑哈)。

和 String 类型不同,一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。

那么下面我们看下集合类型的操作复杂度,集合类型的底层数据结构上面我已经简单介绍过了,大家可以回头看下,其中,哈希表的操作特点我们刚刚已经学过了。整数数组和双向链表也很常见,它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低;压缩列表和跳表我们平时接触得可能不多,但它们也是 Redis 重要的数据结构,所以我来重点解释一下。

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

我们再来看下跳表。

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:

如果我们要在链表中查找 33 这个元素,只能从头开始遍历链表,查找 6 次,直到找到 33 为止。此时,复杂度是 O(N),查找效率很低。

为了提高查找速度,我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。例如,从前两个元素中抽取元素 1 作为一级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时,我们只需要 4 次查找就能定位到元素 33 了。

如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。

可以看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。

下面给大家一个数据结构时间复杂度的总结:

再给大家一些使用上的建议,Redis中值为集合类型的单元素操作无所谓,但是范围操作尽量避免,实在不能避免可以采取渐进式遍历,否则可能会阻塞Redis,再有还要尽量避免bigkey等。
————————————————
版权声明:本文为CSDN博主「天天蹦跶蹦跶的」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/java_cjx/article/details/115005966

标签:元素,Redis,索引,查找,哈希,数据结构,底层
From: https://www.cnblogs.com/huigui-mint/p/17552499.html

相关文章

  • Redis学习指南
    基础资料官方下载、安装、运行redis中文站thelittleredisbook(中文版)Redis几个认识误区 redis作者宣言建模Fast,easy,realtimemetricsusingRedisbitmaps监控、配置RedisCommander (在线运行命令。强烈不推荐,会把浏览器搞死!)RedisLive redmon(★推荐支持监控......
  • 31. Redis分布式锁
    我是javapub,一名Markdown程序员从......
  • Redis压测工具(redis-benchmark)
    redis性能测试工具可选参数如下所示:redis性能测试工具可选参数如下所示:序号选项描述默认值1-h指定服务器主机名127.0.0.12-p指定服务器端口63793-s指定服务器socket4-c指定并发连接数505-n指定请求数100006-d以字节的形式指定SE......
  • RedisTemplate 的简单使用
    redisTemplate.opsForValue() 方法可以获得一个RedisString的操作类,通过该类可以执行一系列字符串类型数据的操作,例如获取、设置、删除数据等。//示例1:设置字符串类型的数据redisTemplate.opsForValue().set("key","value");//示例2:获取字符串类型的数据String......
  • kubernetes 实现 list-watch 的底层原理
    我们都知道,controller-manager,scheduler,kubelet会向apiserver监听感兴趣的对象,当监听对象的内容或状态发生变化后,对应的事件会立即推送到监听者。借由这套事件通知机制,kubernetes才能良好地运转。那么这套事件通知机制是如何实现并驱动的呢?1.etcd在k8s中,apiserver是......
  • 数据结构练习笔记——单链表的创建
    单链表的创建【问题描述】从键盘终端输入若干整数,为其创建带头节点的单链表存储结构【样例输入】51223323345【样例输出】1223323345【样例说明】第一行的数为单链表中元素的个数,后面为各元素的值#include<iostream>usingnamespacestd;structLNode{......
  • Redis面试题
    1.什么是Redis,Redis的优缺点?Redis本质上是一个key-value类型的内存的数据库,很像memcached,整个数据库统统加载在内存当中就行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。优点:因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能快......
  • 【Redis】字符串sds
    sds,即SimpleDynamicStrings,是Redis中存储绝大部分字符串所采用的数据结构。typedefchar*sds;一、类型sds的类型包括SDS_TYPE_5,SDS_TYPE_8,SDS_TYPE_16,SDS_TYPE_32,SDS_TYPE_64五种:#defineSDS_TYPE_50#defineSDS_TYPE_81#defineSDS_TYPE_162#defineSD......
  • 数据安全没保证?GaussDB(for Redis)为你保驾护航
    摘要:GaussDB(forRedis)通过账号管理、权限隔离、高危命令禁删/重命名、安全IP免密登录、实例回收站等企业级特性,保障用户数据库数据和信息安全。本文分享自华为云社区《数据安全没保证?GaussDB(forRedis)为你保驾护航》,作者:GaussDB数据库。近日,一些用户反馈使用的开源Redis......
  • 解决redis 集群命令的具体操作步骤
    实现Redis集群命令简介在使用Redis时,有时可能需要使用Redis集群来处理大规模的数据。Redis集群是一个由多个Redis节点组成的分布式系统,可以提供高可用性和可扩展性。本文将指导你如何实现Redis集群命令。流程概述下面是实现Redis集群命令的流程概述:步骤描述......