文章目录
- 非关系型数据库之redis
- 1、谈谈你对redis理解
- 2、redis底层数据结构
- 3、redis网络io模型
- 4、redis布隆过滤器(BloomFilter)
- 5、redis神奇的HyperLoglog解决统计问题
- 6、redis之bitmap情况
- 7、Redis五种数据类型及应用场景(重点)
- 8、为什么使用redis,或者说为什么用缓存
- 9、为什么要用 Redis 而不用 map 做缓存?
- 10、Redis为什么这么快
- 11、redis到底是单线程还是多线程
- 12、Redis底层如何使用跳表来存储
- 13、能说一下RDB和AOF的实现原理吗?
- 18、Redis的过期键的删除策略
- 19、redis淘汰策略以及应用场景
- 20、Redis key的过期时间和永久有效分别怎么设置?
- 21、我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?
- 22、Redis如何做内存优化?
- 23、缓存雪崩(必备)
- 24、缓存穿透(必备)
- 25、缓存击穿(必备)
- 26、Redis实现分布式锁(重点)
- 27、如何保证Redis与数据库的数据一致性(重点)
- 28、redis中单点模式,主从模式,哨兵模式,集群模式
- 29、redis主从复制,读写分离
- 30、如何解决redis并发竞争的key问题(重点)
- 31、redis实现异步队列?
- 32、redis实现延时队列
- 33、Redis回收使用的是什么算法?
- 34、redis使用特点,理解
- 35、关系型数据库和非关系型数据库
- 36、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?(必备)
- 37、如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?(必备)
- 38、如果有大量的key需要设置同一时间过期,一般需要注意什么?
- 39、redis key过期了为什么内存没有释放
- 40、Redis中key没有设置过期时间为什么被redis主动删除了
- 41、Redis淘汰key算法LRU和LFU
- 42、Redis如何做持久化的?
- 43、是否使用过Redis集群,集群的原理是什么?
- 44、Redis常用的客户端有哪些
- 45、WATCH命令和基于CAS的乐观锁
- 46、redis事务的了解CAS
- 47、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 | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 注:一个键最大能存储512MB | 1、分布式锁: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"