[八股速成]redis篇
前言
我之前整理过redis超详细八股笔记:https://www.nowcoder.com/discuss/583767072316858368?sourceSSR=search,但是说实话因为这份这份八股资料过于详细,内容过于充实,给背记带来了很大的挑战,所以我准备再出一系列帖子,内容就是我根据自己的面试经历和网上的面经,去筛选八股里面哪些是最常被问到的问题把它们整理出来,这样也能省去大家自己整理和筛选的时间,大家可以在面试前一两个小时快速把这一系列最常问八股的帖子拿出来看看,临时抱佛脚的效果应该很好。后面这系列帖子我会放入专栏https://www.nowcoder.com/creation/manager/columnDetail/0ybvLm,欢迎大家订阅。最后我想说,速成虽好,但是还是建议有时间就去看看我详细的八股笔记帖子。
1.redis常用数据类型
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
- 字符串(string):普通字符串,Redis中最简单的数据类型,string的内部结构实现上类似Java的ArrayList
- 哈希(hash):也叫散列,类似于Java中的HashMap结构
- 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList,底层是双向链表
- 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
- 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
2.zset的底层原理
Redis的有序集合(Zset)底层采用两种数据结构,分别是压缩列表(ziplist)和跳跃表(skiplist)。
- 当Zset的元素个数小于128个且每个元素的长度小于64字节时,采用ziplist编码。在ziplist中,所有的key和value都按顺序存储,构成一个有序列表。这种实现方式主要是为了节省内存,因为压缩列表是一块连续的内存区域,通过紧凑的存储可以有效地利用空间。虽然压缩列表可以有效减少内存占用,但在需要修改数据时,可能需要对整个列表进行重写,性能较低。
- 跳表是一种多层次的链表结构,通过多级索引提升查找效率。在不满足使用压缩列表的条件下,Redis会采用跳表作为Zset的底层数据结构。跳表能够提供平均O(logN)的时间复杂度进行元素查找,最坏情况下为O(N)。跳表中的每一层都是一个有序链表,并且层级越高,链表中的节点数就越少,从而允许在高层快速跳过一些元素,达到快速定位的目的。
综上所述,Redis的Zset通过灵活地使用压缩列表和跳跃表作为底层数据结构,在不同的场景下平衡了内存使用效率和数据操作性能。这两种数据结构各有优劣,压缩列表适用于数据量小、内存受限的场景,而跳跃表适合于数据量大、需要高效操作的环境。
什么是跳表?
跳表(Skip List)是一种基于有序链表的数据结构,通过多级索引的方式实现高效的查找、插入和删除操作。
跳表以空间换时间的方式优化了传统单链表的效率。在单链表中,即使数据是有序的,查找一个元素也需要从头到尾遍历整个链表,时间复杂度为O(n)。而在跳表中,通过建立多层索引来实现快速查找。顶层索引链表的节点数量远少于底层原始链表,并且层级越高,节点越少。
跳表中的每一层都是一个有序链表,并且每个节点都包含指向同层级下一个节点的指针以及指向下一层对应节点的down指针。例如,当查找一个元素时,首先在顶层索引进行查找,如果当前节点的值大于要查找的值,则继续在同一层级向右移动;如果小于要查找的值,则通过down指针下沉到下一层继续查找。每下降一层,搜索范围就缩小一半,最终在底层链表中找到目标元素或者确认元素不存在。
跳表的插入和删除操作同样高效,其时间复杂度也是O(logn)。向跳表中插入新元素时,首先要找到合适的插入位置,保持链表的有序性。然后通过随机函数决定新节点应该出现在哪些层级的索引中:随机结果高于某个固定概率p,就在该层级插入新节点。删除操作类似,先找到要删除的节点,然后在所有包含该节点的层级中移除它。
3.hash的底层原理
Redis的Hash数据结构底层原理主要基于两种数据结构:ziplist和hashtable。
具体来说,这两种数据结构的应用如下:
- ziplist:当满足特定条件时(键和值的字符串长度都小于64字节,且键值对数量少于512),Hash数据结构会采用ziplist作为其底层实现。在ziplist中,所有的key和value都按顺序存储,构成一个有序列表。这种实现方式主要是为了节省内存,因为压缩列表是一块连续的内存区域,通过紧凑的存储可以有效地利用空间。
- hashtable:当不满足ziplist的条件时,Hash数据结构会使用hashtable作为底层实现。在hashtable中,每个键值对都以字典的形式保存,其中字典的键为字符串对象,保存了原键值对的键;字典的值为另一个字符串对象,保存了原键值对的值。这样的结构允许快速的查找、插入和删除操作。
此外,在Hash数据结构中,如果ziplist编码所需的两个条件中的任意一个不再满足时,会发生编码转换,即原本保存在ziplist中的所有键值对会被转移到字典中,对象的编码也会从ziplist变为hashtable。这通常发生在键的长度过大、值的长度过大或者键值对的数量过多的情况下。
综上所述,Redis的Hash数据结构根据数据的规模和访问模式灵活地在ziplist和hashtable之间切换,以达到既节省内存又保证访问效率的目的。
4.3种redis特殊数据类型
Bitmap (位图)
Bitmap 存储的是连续的二进制数字(0 和 1),本来int数字占4字节32位,但通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态(比如:01表示1,001表示2) 。,所以 Bitmap 本身会极大的节省储存空间。
# 将名为myBitmap的bitmap的第5位设置为1 SETBIT myBitmap 5 1 //SETBIT key offset value
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
HyperLogLog(基数统计)
HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素。这是真的厉害,这就是数学的魅力么!并且,Redis 对 HyperLogLog 的存储结构做了优化,采用两种方式计数:
- 稀疏矩阵:计数较少的时候,占用空间很小。
- 稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间
Geospatial (地理位置)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
数值范围0-40亿的数如何排序(bitmap)
使用Bitmap进行排序是一种特殊的方法,适用于处理大量数据的排序问题,尤其是在内存有限的情况下。以下是使用Bitmap排序的步骤:
- 初始化Bitmap:根据数值范围创建一个足够大的Bitmap。由于数值范围是0-40亿,Bitmap的大小需要能够覆盖这个范围,即至少需要40亿位。
- 标记数值:遍历待排序的数值列表,将每个数值在Bitmap中对应的位置标记为1。例如,如果数值是5,则在Bitmap的第6位(从0开始计数)标记为1。
- 按位输出:按照Bitmap的顺序,输出所有标记为1的位置对应的数值,即可得到排序后的结果。
5.使用 Redis 实现一个排行榜怎么做?
Redis 中有一个叫做 Sorted Set
的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
相关的一些 Redis 命令: ZRANGE
(从小到大排序)、 ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
6.redis为什么这么快?
1、完全基于内存的,C语言编写,没有磁盘IO上的开销。数据存在内存中,读写速度快。
2、采用单线程,避免不必要的上下文切换以及锁等同步机制的开销
3、使用多路I/O复用模型,基于select/epoll等I/O多路复用技术实现高吞吐量网络I/O
- IO多路复用是一种操作系统中的一种技术,允许一个线程或进程同时处理多个输入输出(IO)操作。Redis通过使用IO多路复用技术,以单线程的方式高效地处理了多个客户端的请求,避免了为每个连接创建新线程的开销,并保持高性能。
能解释一下I/O多路复用模型?
IO多路复用模型的思路就是:系统提供了select、poll、epoll函数可以同时监控多个fd(文件描述符)的操作,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,一旦某个描述符就绪(一般是读就绪或者写就绪),select函数就会返回可读/可写状态,这时询问线程再去通知想请求IO操作的线程,对应线程此时再发起IO请求去读/写数据。
文件描述符是一个非负整数,用于标识被进程打开的文件,是操作系统为了高效管理这些文件所创建的索引。
7.Redis遇到哈希冲突怎么办?
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next
指针, 多个哈希表节点可以用 next
指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
原理跟 Java 的 HashMap 类似,都是数组+链表的结构。当发生 hash 碰撞时将会把元素追加到链表上。
8. Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
写:
- 先更新 db
- 然后直接删除 cache 。
读 :
- 从 cache 中读取数据,读取到就直接返回
- cache 中读取不到的话,就从 db 中读取数据返回
- 再把数据放到 cache 中。
1.在写数据的过程中,可以先删除 cache ,后更新 db 么?
不行,因为这样可能会造成 数据库(db)和缓存(Cache)数据不一致的问题。举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。
过程如下:请求 1 先把 cache 中的 A 数据删除 -> 请求
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏价格永远为19.9元! 不想当架构师的后端开发工程师不是好码农! 此专栏一方面用于存放我的架构设计学习笔记, 另外我会在本专栏加入一系列最常问八股问题帖子,内容就是我根据自己的面试经历和网上的面经,去筛选八股里面哪些是最常被问到的问题把它们整理出来,大家可以在面试前一两个小时快速把这一系列最常问八股的帖子拿出来看看,临时抱佛脚的效果应该很好