八股文之redis

文章目录

非关系型数据库之redis

1、谈谈你对redis理解

Redis(Remote Dictionary Server) 是开源的高性能非关系型键值对数据库,可以存储键和五种不同类型的值之间的映射,键类型只能是字符串,值支持五种数据类型:字符串,列表,集合,散列表,有序集合;和传统的管系统数据库不同redis基于内存处理的,读写速度非常快,故而redis常常用来作为缓存,分布式锁,事务,持久化机制,多种集群方案。

2、redis底层数据结构

注意,Redis 数据结构并不是指 string(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Zset(有序集合),因为这些是 Redis 键值对中的数据类型,并不是数据结构。这些数据类型的底层实现的方式,才是数据结构。

Redis 底层的数据结构一共有 6 种,如下图右边部分,它和数据类型对应关系也如下图

图片

  • List 数据类型底层数据结构由「双向链表」或「压缩表列表」实现;
  • Hash 数据类型底层数据结构由「压缩列表」或「哈希表」实现;
  • Set 数据类型底层数据结构由「哈希表」或「整数集合」实现;
  • Zset 数据类型底层数据结构由「压缩列表」或「跳表」实现

sds

背景

Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS。

c语言字符串缺陷

C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。

1 在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束

举个例子,C 语言获取字符串长度的函数 strlen,就是通过字符数组中的每一个字符,并进行计数,等遇到字符为“\0”后,就会停止遍历,然后返回已经统计到的字符个数,即为字符串长度

C 语言的字符串用 “\0” 字符作为结尾标记有个缺陷。假设有个字符串中有个 “\0” 字符,这时在操作这个字符串时就会提早结束,比如 “xiao\0lin” 字符串,计算字符串长度的时候则会是 4

2 用 char* 字符串中的字符必须符合某种编码(比如ASCII)。这些限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据

3 C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。

举个例子,strcat 函数是可以将两个字符串拼接在一起。

c //将 src 字符串拼接到 dest 字符串后面 char *strcat(char *dest, const char* src);

C 语言的字符串是不会记录自身的缓冲区大小的,所以 strcat 函数假定程序员在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立,就会发生缓冲区溢出将可能会造成程序运行终止,(*这是一个可以改进的地方*)。

而且,strcat 函数和 strlen 函数类似,时间复杂度也很高,也都需要先通过遍历字符串才能得到目标字符串的末尾。然后对于 strcat 函数来说,还要再遍历源字符串才能完成追加,对字符串的操作效率不高。

sds结构设计

图片

结构中的每个成员变量分别介绍下:

  • len,SDS 所保存的字符串长度。这样获取字符串长度的时候,只需要返回这个变量值就行,时间复杂度只需要 O(1)。
  • alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算 出剩余的空间大小,然后用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区益处的问题。
  • flags,SDS 类型,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
  • buf[],字节数组,用来保存实际数据。不需要用 “\0” 字符来标识字符串结尾了,而是直接将其作为二进制数据处理,可以用来保存图片等二进制数据。它即可以保存文本数据,也可以保存二进制数据,所以叫字节数组会更好点。

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

优势
O(1)复杂度获取字符串长度

Redis 的 SDS 结构因为加入了 len 成员变量,那么获取字符串长度的时候,直接返回这个变量的值就行,所以复杂度只有 O(1)

二进制安全

因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而且 SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的

不会发生缓冲区溢出

C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。

所以,Redis 的 SDS 结构里引入了 alloc 和 leb 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。

而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小,以满足修改所需的大小。

在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。

这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。

节省内存空间

SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

Redis 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同

之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。

使用了专门的编译优化来节省内存空间

链表

图片

特点
  • listNode 链表节点带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表
  • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)
  • list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1)
  • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;

链表的缺陷也是有的,链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。

能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。

因此,Redis 的 list 数据类型在数据量比较少的情况下,会采用「压缩列表」作为底层数据结构的实现,压缩列表就是由数组实现的,下面我们会细说压缩列表。

压缩列表

图片

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组

插入时候

当我们往压缩列表中插入数据时,压缩列表 就会根据数据是字符串还是整数,以及它们的大小会在 prevlen 和 encoding 这两个元素里保存不同的信息,这种根据数据大小进行对应信息保存的设计思想,正是 Redis 为了节省内存而采用的。

查询时候

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

压缩列表节点包含三部分内容:

  • *prevlen*,记录了前一个节点的长度;
  • *encoding*,记录了当前节点实际数据的类型以及长度;
  • *data*,记录了当前节点的实际数据;

图片

连锁更新

压缩列表除了查找复杂度高的问题,压缩列表在插入元素时,如果内存空间不够了,压缩列表还需要重新分配一块连续的内存空间,而这可能会引发连锁更新的问题。

压缩列表里的每个节点中的 prevlen 属性都记录了「前一个节点的长度」,而且 prevlen 属性的空间大小跟前一个节点长度值有关,比如:

  • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
  • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:

图片

因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值。

这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为 e1 的前置节点,如下图:

图片

因为 e1 节点的 prevlen 属性只有 1 个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作,并将 e1 节点的 prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。

多米诺牌的效应就此开始。

图片

e1 原本的长度在 250~253 之间,因为刚才的扩展空间,此时 e1 的长度就大于等于 254 了,因此原本 e2 保存 e1 的 prevlen 属性也必须从 1 字节扩展至 5 字节大小。

正如扩展 e1 引发了对 e2 扩展一样,扩展 e2 也会引发对 e3 的扩展,而扩展 e3 又会引发对 e4 的扩展…. 一直持续到结尾。

这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」,就像多米诺牌的效应一样,第一张牌倒下了,推动了第二张牌倒下;第二张牌倒下,又推动了第三张牌倒下….

连锁更新一旦发生,就会导致压缩列表 占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。

所以说,虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大了,压缩列表就会面临「连锁更新」的风险。

因此,压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。

哈希表

当一个哈希键包含的 key-value 比较多,或者 key-value 中元素都是比较长多字符串时,Redis 就会使用哈希表作为哈希键的底层实现。

Hash 表优点在于,它能以 O(1) 的复杂度快速查询数据。主要是通过 Hash 函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快。

但是存在的风险也是有,在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高。

3、redis网络io模型

4、redis布隆过滤器(BloomFilter)

5、redis神奇的HyperLoglog解决统计问题

6、redis之bitmap情况

7、Redis五种数据类型及应用场景(重点)

STRING字符串、整数或者浮点数对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 :一个键最大能存储512MB1、分布式锁:SETNX(Key, Value),释放锁:DEL(Key),2、复杂计数功能缓存(用户量,视频播放量)
LIST列表从两端压入或者弹出元素 对单个或者多个元素进行修剪, 只保留一个范围内的元素存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据,简单的消息队列的功能
SET无序集合添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集,做全局去重的功能,点赞,转发,收藏;
HASH包含键值对的无序散列表添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在结构化的数据,比如一个对象,单点登录
ZSET有序集合添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名去重但可以排序,如获取排名前几名的用户,做排行榜应用,取TOPN操作;延时任务;做范围查找。周榜,月榜,年榜

基本操作

# 查询所有的键值对信息
keys *

字符串 String

SET			--存入一个字符串键
SETNX		--存入一个字符串键,若Key存在则操作失败
GET			--获取指定Key的字符串
MSET		--批量存入字符串键
MGET		--批量获取指定Key的字符串
DEL			--删除指定Key(所有类型都可以使用此命令)

127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> set k3 12
OK
127.0.0.1:6379> get k3
"12"
127.0.0.1:6379> set k4 12.24
OK
127.0.0.1:6379> get k4
"12.24"
127.0.0.1:6379> set k5 23.12f
OK
127.0.0.1:6379> get k5
"23.12f"
127.0.0.1:6379> set k6 true
OK
127.0.0.1:6379> get k6
"true"
127.0.0.1:6379> get k11
"v1"
127.0.0.1:6379> get k22
"v3"
127.0.0.1:6379> del key11
(integer) 1
127.0.0.1:6379> mget k4 k6
1) "12.24"
2) "true"
127.0.0.1:6379> set k1 k2
OK
127.0.0.1:6379> setnx k1 v11
(integer) 0

列表 List

LPUSH Key value [value...]			--往key的列表键中左边放入一个元素,key不存在则新建
RPUSH Key value [value...]			--往key的列表键中右边放入一个元素,key不存在则新建
LPOP Key							--从key的列表键最左端弹出一个元素
RPOP Key							--从key的列表键最右端弹出一个元素
LRANGE Key start stop				--获取列表键从start下标到stop下标的元素

eg:案例说明
127.0.0.1:6379> lpush list1 99
(integer) 10
127.0.0.1:6379> lpop list1
"99"
127.0.0.1:6379> rpop list1
"l44"
127.0.0.1:6379> lpush list1 87
(integer) 9
127.0.0.1:6379> lrange list1 0 10
1) "87"
2) "23"
3) "l4"
4) "l3"
5) "l2"
6) "l1"
7) "l11"
8) "l22"
9) "l33"

</