03 一文吃透 Redis 核心考点,面试真题深度梳理

写在前面的话

已写 Java后端完整版学习路线(仔细讲解每个技术栈怎么学,学到什么程度可以投实习/面试)+ 各大厂真实面试问题和参考答案 + MySQL八股深度梳理 3篇长文,如有需要欢迎大佬们自取,后续还会更新MySQL、RabbitMQ、SSM等其他部分的面试高频八股梳理,欢迎关注。 01 非科班转码拿下大厂Offer,花费一天整理的Java后端完整版学习路线 02 盘点2025遇到的各大厂面试真题总结 04 MySQL面试八股看这一篇就够了——深度梳理MySQL面试问题

下面的面试问题答案参考来源:

  1. 面试鸭
  2. **
  3. 豆包
  4. DeepSeek

Redis面试考核重点梳理:

1、什么是Redis,其有什么特点?

Redis是一个基于C语言开发的开源内存数据库,因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis存储的是Key-Value键值对数据。Redis还内置了多种数据类型实现(比如String、List、Hash、Set、Sorted Set、Bitmap、HyperLogLog、GEO、Stream等)。并且,Redis还支持事务、持久化以及集群。

2、为什么要用Redis?

(1)访问速度快

简单答就是:传统数据库数据保存在磁盘,而Redis基于内存,内存的访问速度比磁盘快很多。所以我们可以把一些高频访问的数据放到Redis中,这样下次就可以直接从内存中读取。

详细问可以答:

redis为什么这么快?

Redis基于内存,内存的访问速度比磁盘快很多;

Redis使用单线程事件驱动模型结合I/O多路复用,避免了多线程上下文切换以及竞争,提高了并发处理效率;

Redis内置了多种优化过后的数据类型/结构(比如像字符串、哈希、Zset之类),性能非常高;

(2)高并发

一般像MySQL这类的数据库的QPS大概在4k左右(4核8G),但是使用Redis缓存之后很容易达到5w+,甚至能达到10w+(如果采用Redis集群的话会更高)。

QPS(Query Per Second):服务器每秒可以执行的查询次数

由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的热点数据转移到缓存中去,这样用户的一部分请求会直接到缓存而不用经过数据库。

(3)功能全面

Redis除了作为缓存之外,还可以用于分布式锁、限流、消息队列等场景

3、为什么使用Redis而不用本地缓存?

在数据一致性上:本地缓存在多服务器部署时,存在数据不一致的问题,Redis能够保证数据一致性(主要通过主从复制哨兵系统集群模式等方式)

在数据丟失风险上:本地缓存在服务器宕机之后数据会丟失;而Redis能够对数据进行持久化,数据不容易丟失

在功能上:Redis除了键值对存储以外,还支持多种数据结构和功能

4、Redis的常见数据结构、使用场景以及部分结构的实现原理?

最常见的5种:String、List、Hash、Set、Zset结构。

高级数据结构4种:BitMap、HyperLogLog、GEO、Stream。

常见的五种数据结构:

第一个是字符串(String),是Redis最简单也是最常用的数据结构,可以用来存储任何类型的数据,比如字符串、整数、浮点数、图片等(图片的base64编码或者解码或者图片的路径)。

图片的base64编解码:Base64图片是一种将图像数据编码为文本字符串的方法。

String的特点是一个键对应一个值,且主要用于缓存简单的键值对数据,比如Session、Token、序列化的对象、图片路径或者是用于计数,比如用户单位时间的请求数(限流时候可以用到)。

Redis的String结构的底层实现:

Redis底层实现主要是基于SDS也就是简单动态字符串结构的,之所以没有用C语言的字符串是出于以下几点原因:首先C语言的字符串本质就是char数组,在字符数组的中用“\0”表示字符串结束,所以首先它不是二进制安全的,因为遇到\0就认为是结束标志了,没办法存储包含\0的数据;其次我们需要获取字符串长度的时候需要遍历数组,复杂度是O(n),但是SDS会独立记录字符串长度,所以复杂度就是O(1)。另外SDS会自动扩展内存,减少内存分配次数,而字符数组没有动态扩展的能力,需要手动管理。并且字符数组使用不当还可能会发生越界

SDS有四个属性:

len:记录了字符串的长度,也就是已经使用的字节数。

alloc:表示总共可用的字符空间大小,SDS剩余的空间大小就是alloc - len。

buf[]:实际存储数据的字符数组。

flags:表示SDS的类型,sdshdr5\8\16\32\64。

image.png 第二个结构是List,底层采用双向链表实现,是一个有序的字符串集合,支持从两端插入和删除元素,因为是链表结构,所以插入和删除操作时间复杂度都是O(1)。另外,也可以使用List结构去实现先进先出的队列和先进后出的栈的数据结构。可以用于消息队列。

压缩列表和紧凑列表:

压缩列表是一种紧凑的数据结构,它将所有元素紧密排列存储在单个连续内存块中,十分节约空间,但是压缩列表每个节点都会记录前一个元素的大小,所以如果前一个元素长度发生变化例如从253字节增加到254字节,当前元素的prevlen字段可能需要从1字节扩展到5字节(prevlen字段的编码规则就是:如果前一个元素长度小于254字节,仅用1字节存储,如果前一个元素长度大于等于254字节,prevlen就使用5个字节存储)。这种扩展会导致当前元素自身长度变化,进而可能引发后续元素的连锁更新。紧凑列表是通过把长度字段独立存储,不再依赖前一个元素的长度,从而避免了连锁更新。

压缩列表的特性是:紧凑性,所有元素紧密排列在一起,没有额外的内存开销,由于元素是顺序存储的,所以顺序访问性能较好。

紧凑列表通过把长度字段单独保存,避免了连锁更新的问题,然后数据的存储也是保留了压缩列表的核心优势,内存连续分配,紧凑存储。

image.png

image.png

第三个结构是Hash,它是一种键值对结构,适合存储对象。可以在一个键下存储多个字段和值,类似于Java中的Map。其底层实现为哈希表,适合存储结构化数据,比如用户信息、商品信息等。

Redis的Hash结构的底层实现?

在Redis 6及之前,Hash的底层是通过压缩列表加上哈希表的结构,在7之后改成了紧凑列表和哈希表的数据结构。压缩列表和紧凑列表查找key的效率都是O(n)。

修改成紧凑列表的原因:压缩列表的每个元素存储了前一个元素的长度。如果前一个元素长度发生变化例如从253字节增加到254字节,当前元素的prevlen字段可能需要从1字节扩展到5字节(prevlen字段的编码规则就是:如果前一个元素长度小于254字节,仅用1字节存储,如果前一个元素长度大于等于254字节,prevlen就使用5个字节存储)。这种扩展会导致当前元素自身长度变化,进而可能引发后续元素的连锁更新。紧凑列表是通过把长度字段独立存储,不再依赖前一个元素的长度,从而避免了连锁更新。

字段数量小于512个并且每个字段和值的总长度小于64个字节的时候,就使用压缩列表或者紧凑列表,能够节约内存空间。否则就使用哈希表存储。

哈希表一共有四个字段:分别是table(存储数据的结构)、size(哈希表的大小)、sizemask(哈希表大小的掩码,等于size-1,是为了确定哈希节点在哈希表中的位置),以及used(已经使用的节点数量)。

image.png

Redis Hash的渐进式哈希(扩容):

在redis中,随着数据量增大,hash表需要扩容,一般是扩容成原来容量的2倍,扩容之后原来存储的元素需要重新计算位置。

传统的、简单的rehash过程是:

  1. 分配一个新的2倍大的哈希表;
  2. 遍历旧哈希表的每一个桶;
  3. 将旧表每个桶中的所有键值对重新计算哈希值,迁移到新表的对应位置;
  4. 迁移完成后,将旧表释放,将新表设为当前表。

问题在于:如果哈希表非常大(例如存储了上亿个键值对),这个完整的rehash过程会是一个耗时极长的操作。由于Redis是单线程模型,这个漫长的rehash过程会阻塞住整个服务器,导致在此期间所有请求都无法响应。

解决方案:渐进式Rehash。核心思想是:将庞大的迁移工作分摊到多次、小批量的操作中,从而避免一次性的集中计算带来的长时间阻塞。

Redis中包含两个哈希表:表1和表2,表1是正常使用的哈希表,表2仅在rehash过程中使用的临时哈希表。有一个很重要的变量叫rehashidx,当它被设置为-1时,表示当前没有在进行rehash。当rehash开始时,它会被设置为0,表示rehash的进度,下一次就会从表1的这个索引位置开始迁移。

在rehash进行期间(rehashidx >= 0),字典会同时使用两个哈希表。所有的增、删、改、查操作都会按以下逻辑执行:

删、改、查操作:程序会首先在表1上查找键,如果没找到,才会再去表2上查找。操作的实际执行会在键所在的那个哈希表上进行。

增:

新添加的键值对会一律被保存到表2中,而表1不再进行任何添加操作。这一措施保证了表1的键数量只减不增,随着rehash进行最终会变成空表。

分批迁移(核心):

Redis不会有一个独立的线程专门做迁移,而是巧妙地将迁移工作渗透到每一次对字典的请求中。具体来说,每当服务器处理完一个客户端请求后,会检查rehashidx是否不为-1。如果是,则执行一小步rehash,迁移完一个桶后,将rehashidx的值+1,指向下一个要迁移的桶。此外,如果服务器空闲,Redis还会在定时任务中执行更主动的rehash,每次迁移更多桶,以加快整体进度。

当表1的所有桶都迁移完毕时,rehash过程宣告结束。释放表1的空间。将表2设置为新的表1。创建一个新的、空的表2,为下一次rehash做准备。将rehashidx重新设置为-1。

image.png

image.png

购物车实现应该用String还是Hash结构?

由于购物车中的商品频繁修改和变动,所以适合使用Hash存储:

用户id作为存储的key

商品id为field,商品数量为value

用户添加商品就是往Hash里面增加新的field与value;

查询购物车信息就是遍历对应的Hash;

更改商品数量直接修改对应的value值;

删除商品就是删除Hash中对应的field;

清空购物车直接删除对应的key即可。

String和Hash对比

对象存储方式:String存储的是序列化(序列化指把对象转换成可传输的字节流的过程)后的对象数据,存放的是整个对象,操作简单直接。Hash是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash就非常适合。

内存消耗:Hash通常比String更节省内存,特别是在字段较多且字段长度较短时。Redis对小型Hash进行优化(如使用ziplist存储),进一步降低内存占用。

复杂对象存储:String在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。

性能:String的操作通常具有O(1)的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。

第四个结构是Set,它是一个无序且不允许重复元素的字符串集合。查找和插入操作的时间复杂度为O(1)。且支持集合运算,比如交集(SINTER)、并集(SUNION)和差集(SDIFF)。使用场景的话,比如共同好友、共同关注这些就是交集,好友推荐就是差集;以及某些需要随机获取元素的场景,比如抽奖系统。

用Set实现抽奖系统怎么做?

如果想要使用Set实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了:

首先使用SADD指令向指定集合添加一个或多个元素;

然后如果是无重复的抽奖的话,可以通过SPOP随机获取并移除指定集合中一个或多个元素;如果是允许重复的,可以通过srandmember指令随机获取指定集合中的元素,适合允许重复中奖的场景。

image.png

image.png

第五个是Zset,它是一种有序集合,在每个元素上都关联了一个分数,元素按照分数有序排序。非常适合各种需要排序的场景,比如排行榜。

Zset底层实现?

底层实现结合了跳表和哈希表,支持高效的范围查询。

哈希表是用来存储Zset中元素和分数之间的映射关系,通过哈希表可以实现O(1)的时间复杂度对单个元素进行查询,但是如果希望进行范围查询的时候,就需要用到跳表结构。

跳表是一个多层索引的链表,最底下一层存储了完整的元素,上面层存储的是下层的子集,比如我们底层存储了1-10,如果有三层,第二层可能存储的就是13579这样,最上面一层存储的就是1 5 7,所以我们查询元素的时候,可以通过顶层快速定位到要查询的位置范围,然后往下层走,继续做进一步的位置定位,直到找到要查找的元素,平均时间复杂度是O(logn)。如果要插入元素的时候,同样也是先快速定位插入的位置,然后插入到最底层,之后会根据概率随机决定要不要提取一份到上面一层,每一个节点是否被加入上一层的概率是25%(最高层数5.0中是64,7.0中是32)

image.png

为什么Zset底层实现用跳表不用红黑树和B+树?

(1)相比于红黑树而言,跳表实现更简单,跳表基于多层链表实现,通过概率算法动态生成索引层级,逻辑理解上更简单;而红黑树需要复杂的平衡操作来维护结构,代码实现复杂度较高,理解门槛更高;

(2)范围查询更高效,红黑树从结构上不支持范围查询,而跳表可以通过O(logn)的时间复杂度定位起点,然后在原始的链表中先后遍历即可。

(3)结构更灵活,跳表的层数是动态的,可以基于概率分布调整层数,灵活的适应不同的数据量,红黑树无法调整。

相较于B+树,一方面B+树更新更加复杂,涉及页的合并和分裂,会导致额外的计算。B+树节点理论上占的内存也比跳表节点大。因为跳表只需要维护自身的数据和一个指针(可能还有一个回退指针(redis)),而B+树是多叉树,一个节点需要多指针,并且节点内部还有若干指针,每个元素在叶子节点有一份完整的数据内容,在非叶子节点还需要存储键的数据,所以内存开销相比跳表大。

使用Redis实现一个排行榜?

可以使用ZSet结构,它是一个有序集合,每个元素都有一个对应的分数,其底层是基于哈希表和跳表存储的,因此使用ZSet就可以天然实现一个排行榜功能。我们可以使用zadd命令添加用户和分数,使用zrank命令获取某个用户的排名(zrank是升序排名,zrevrank是降序排名)如果要获取前多少名可以使用zrevrange命令,使用zincrby可以对用户分数进行修改

四种高级的数据结构:

Bitmap:存储的是连续的二进制数字,我们可以使用一个bit位来表示一个布尔值,一个经典的使用场景就是统计每天用户的在线状态,我们可以把当天的日期作为key,用户ID作为数组元素的下标offset,然后用1表示活跃过,0表示没有活跃。

HyperLogLog:用于海量数据基数统计的场景,可能会有一定的误差,但是能够以极小的内存估算海量的数据,常用于网站的独立访客数的统计。

GEO:用于存储地理位置信息的数据结构,可以存储经纬度信息并且支持空间查询,比如百度、高德地图,附近的人这些功能。

补充:具体怎么使用GEO,比如在附近的100W商户中快速找到离你最近的5家? 首先创建一个地理集合,把商家的经纬度存到集合里,然后创建地理索引,然后根据自己的坐标,利用$near就可以查询附近离自己最近的商家

5、Redis如何实现数据不丢失/redis持久化机制?

Redis提供了多种持久化机制,主要包括RDB(RedisDatabase)、AOF(Append-OnlyFile)和混合持久化。

首先是RDB持久化,RDB是通过创建快照进行持久化的方式,它会定期将内存中的数据保存到磁盘上的一个二进制文件中。RDB快照可以通过手动命令(如SAVE或BGSAVE)或配置文件中的时间策略(如每隔一定时间或一定写操作次数后自动触发)来生成。

RDB创建快照时会阻塞主线程吗?

Redis提供了两个命令来生成RDB快照文件:

Save命令:是同步保存操作,所以会阻塞Redis主线程;

Bgsave命令:是fork出一个子进程,由子进程执行,不会阻塞Redis主线程,这也是默认选项。

进程和线程?

进程是资源分配的基本单位,每个进程都有自己独立的内存空间,可以看成是一个正在运行的程序实例,不同进程之间相互独立;

线程是CPU调度的基本单位,属于进程,一个进程的不同线程共享内存空间。进程的创建和切换开销要更大。

Linux创建进程命令:fork()复制当前进程生成一个子进程

Pthread_create()创建线程

RDB持久化会有数据丟失风险吗?

使用RDB持久化数据可能会有一定的丢失风险,因为它是间隔性的快照,如果redis崩溃,两次快照之间的数据可能无法保存。

其次是AOF持久化,是通过记录每个写的操作命令来保证数据的完整性。每当Redis执行一个写操作时,该操作会被追加到AOF文件的末尾;随着写操作的增加,AOF文件可能会变得很大,Redis提供了AOF重写机制,通过创建一个新的AOF文件来压缩历史记录,只保留每个键的最新状态;当Redis启动时,会读取AOF文件并重新执行其中的命令,从而恢复数据。

AOF的数据安全性更高,几乎不会丢失数据,因为它记录了每一次写的操作,而且它是可读的文本文件,便于调试和分析,但是它的文件体积通常比RDB大,恢复速度较慢。

因此,在Redis4.0及之后版本中,引入了混合持久化的方式,这种方式首先以RDB格式保存当前数据的快照,然后把数据写入新的AOF,后续写的操作会追加到新的AOF文件中,写入完成后会把含有RDB格式和AOF格式的AOF文件替代原先的AOF文件。

6、Redis线程模型(6.0之前单线程,之后多线程)

在6.0之前通过单线程+I/O多路复用同时处理多个客户端请求,在6.0使用多线程处理网络I/O,但是核心逻辑仍然由单线程完成。

之前设计成单线程原因:

一方面是多线程模型虽然可以利用多核CPU,但是也存在线程竞争,并且要进行频繁的线程上下文切换,会额外增加CPU的开销。另外,Redis的核心性能是依赖于内存操作,而不是CPU计算,所以也不需要多线程;最后,单线程可以简化实现,更容易去维护。

后面又引入多线程原因:

是因为发现网络IO读写成为瓶颈,所以通过多线程来加快IO操作,但是执行命令仍然是单线程顺序执行的,所以也不用担心线程安全问题。

7、Redis内存管理

内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致OOM问题。通过设置合理的过期时间,Redis会自动删除暂时不需要的数据,为新的缓存数据腾出空间。

Redis怎么判断数据是否过期?

Redis会通过一个过期字典(可以看成是一个hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key,过期字典的值是一个long类型的整数,这个整数保存了Redis键的过期时间。我们在查询一个key的时候redis会首先检查该key是否在过期字典里,如果在的话判断过期就直接删除,并且返回null。

Redis数据的过期删除策略:

Redis的过期删除策略是定期删除+惰性删除结合的策略。

惰性删除指的是:在取出或者查询key的时候对数据进行过期检查,发现过期就直接删掉,并且返回结果null。

定期删除是指:每间隔一定的时间(默认100ms)随机从设置了过期时间的key中抽查一批,然后逐个检查这些key是否过期,过期就删除key。并且定期删除还会受到执行时间抽查的key的过期比例的影响,如果删除操作的执行时间已经超过了阈值(默认25ms)就中断这一次删除操作。如果说没有达到阈值,并且这一批抽查的key过期比例超过了一个数值(默认25%),就会再抽查一批,执行相同操作,如果没有超过就中断这次删除操作。

Redis的内存淘汰策略:

首先是不淘汰数据,当运行内存超过了设置的最大内存,不会淘汰数据,而是直接禁止写入;如果淘汰数据的话,对设置了过期时间的key来说,有几种策略:首先是随机淘汰key,然后可以淘汰快要过期的key,还可以淘汰最久未使用的key、淘汰最少使用的key。

(相关问题:MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据? 内存淘汰策略,淘汰最少使用或者最久未使用的key)

8、Redis的事务

Redis支持事务,但是Redis的事务和MySQL的事务不太一样,MySQL中的事务主要支持原子性、一致性、隔离性和持久性,而redis中的事务指的是所有命令都在一个原子操作中执行,要么全部执行,要么全部不执行,另外,mysql的事务支持回滚操作,而redis的事务不支持回滚

MySQL的ACID(原子性、一致性、隔离性、持久性)

MySQL的原子性表示事务要么都成功执行,要么失败就回滚所有操作,主要通过undolog日志实现,undolog会记录事务操作前的数据状态,如果事务失败,就会根据日志回到操作前的状态;一致性可以通过约束、外键或者触发器之类的保证;隔离性指的是支持多个事务隔离级别(包括读未提交、读已提交、可重复读以及串行化);持久性主要是通过redolog日志,记录事务操作后的数据状态,如果系统发生故障,系统恢复后可以根据日志重新执行已经提交的事务,保证数据的一致性。

另:虽然Redis不支持原子性,但是redis支持隔离性(单线程模型天然支持)和持久性(Redis的持久化机制

9、Redis性能优化

(1)使用批量操作减少网络传输

比如MgetMset指令,可以批量获取或设置多个指定key的value、通过sadd可以批量向集合添加多个元素

(2)对于不支持批量操作的命令,可以使用pipeline流水线**,把一批redis命令封装成一组,一次性提交到redis服务器,**减少网络传输次数

(3)Lua脚本也支持批量操作多条命令,Lua的操作是原子性的**,一段Lua脚本执行过程中不会有其他脚本或命令同时执行,保证了操作不会被其他指令插入或打扰,这是pipeline不具备的,但是Lua脚本也有问题,就是如果运行时候出错了,之前已经执行的操作不会撤销,没办法像mysql一样实现回滚。(解决方案:在执行前验证所有条件和参数是否正确;使用Lua的pcall**捕获和处理错误)

10、Redis bigKey问题?

如果一个key对应的value所占用的内存比较大,那这个key就可以看作是bigkey。

(1)判定的依据:

String类型的value超过1MB

复合类型(List、Hash、Set、Sorted Set等)的value包含的元素超过5000个(不过,对于复合类型的value来说,不一定包含的元素越多,占用的内存就越多)

(2)产生的原因:

bigkey通常是由于下面这些原因产生的:

程序设计不当,比如直接使用String类型存储较大的文件对应的二进制数据。

对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。

未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。

(3)bigkey的影响:

可能导致客户端超时阻塞:由于Redis执行命令是单线程处理,然后在操作大key时会比较耗时,那么就会阻塞Redis,从客户端这一视角看,就是很久都没有响应。

网络阻塞:每次获取大key产生的网络流量较大,如果一个key的大小是1MB,每秒访问1000次,那么每秒会产生1000MB的流量。

工作线程阻塞:如果使用del删除大key时,会阻塞工作线程,这样就没办法处理后续的命令。

(3)如何发现bigkey:

使用redis自带的--bigkeys指令**(会扫描redis的所有key**,所以会对性能有一点影响,并且这种方式只能找到最大的那个key)

使用redis自带的scan命令按照一定的模式和数量返回匹配的key,获取到key之后可以根据数据结构类型通过string或者hlenllen等命令返回其长度或者成员变量数量。

(4)怎么处理bigkey:

首先可以分割bigkey:将一个bigkey分割为多个小key。例如,将一个含有上万字段数量的Hash按照一定策略(比如二次哈希)拆分为多个Hash。

手动清理:可以使用unlink命令来异步删除一个或多个指定的key。

采用更合适的数据结构:例如,使用Bitmap保存状态信息(0/1)。

11、hotKey/热点key问题?

如果一个key的访问次数比较多且明显多于其他key的话,那这个key就可以看作是热点Key。例如在Redis实例的每秒处理请求达到5000次,而其中某个key的每秒访问量就高达2000次,那这个key就可以看作是hotkey。

出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀活动的商品。

(1)HotKey的危害:

处理hotkey会占用大量的CPU和带宽,可能会影响Redis实例对其他请求的正常处理。此外,如果突然访问hotkey的请求超出了Redis的处理能力,就可能发生缓存击穿,导致Redis就会直接宕机(缓存击穿)。这种情况下,大量请求将直接落到后面的数据库上,可能会导致数据库崩溃。

因此,hotkey很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。

(2)如何解决hotKey问题?

使用Redis集群:将热点数据分散存储在多个Redis节点上。

读写分离:针对读多写少的场景,主节点处理写请求,从节点处理读请求。

12、缓存穿透、缓存击穿、缓存雪崩?

第一个是缓存穿透,它是指查询一个不存在的数据,这个数据既不在缓存中,也不在数据库中,导致每次请求都会直接打到数据库上。

造成这个问题的可能原因的话主要是可能有黑客恶意构造不存在的查询条件如随机生成的无效ID)。

缓存穿透会让数据库承受大量无效请求,导致性能下降或崩溃。解决方案主要有两种,一个是使用布隆过滤器,在请求到达缓存之前,先用布隆过滤器判断数据是否存在。另一个是缓存无效key,当查询结果为空时,将无效的key写入缓存,并设置较短的过期时间。这样可以避免重复查询数据库,但是如果是黑客恶意构建的随机key,这个方法也没用,主要还是需要通过布隆过滤器解决。

第二个是缓存击穿,它是指某个热点数据在缓存中过期后,大量并发请求同时访问该数据,导致所有请求都打到数据库上。

缓存击穿会让数据库瞬间承受高并发压力,导致性能下降或崩溃。解决方案主要有三种,一种是采用永不过期策略,对于热点数据,可以不设置过期时间,通过后台定时任务主动刷新缓存。另外,可以对热点数据提前预热将其存入缓存中并设置合理的过期时间,比如秒杀场景下,设置数据在秒杀活动结束之前都不过期;还有就是加锁在缓存失效时,只允许一个线程去查询数据并更新缓存,其他线程等待。可以通过分布式锁实现。

第三个是缓存雪崩,它是指大量缓存在同一时间过期,导致大量请求直接打到数据库上。造成这个问题的原因主要有两个,一个是缓存的过期时间设置为相同的值,导致集中失效。另一个是缓存服务宕机或不可用,导致所有请求都直接访问数据库。

解决方案的话,针对redis不可用的情况,可以考虑采用redis集群,或者构建多级缓存,例如使用本地缓存caffeine+redis的组合,redis缓存出现问题时,还可以从本地缓存中获取部分数据(本地缓存的容量设置:内存占用控制在应用JVM堆的10%-30%,避免GC压力,优先缓存最高频访问的小对象;redis单个实例建议不超过10G,过大影响持久化和故障恢复)。

image.png

针对大量缓存同时失效的情况,解决方案是为缓存设置随机的过期时间,避免集中失效。例如,在基础过期时间上加上一个随机值

另外,在某些情况下还可以采取降级策略,在缓存不可用时,返回默认值或静态页面,避免请求直接打到数据库。

13、布隆过滤器的原理、优缺点和应用场景

(1)布隆过滤器的核心原理

基本结构:

由一个位数组(Bit Array)和多个哈希函数(Hash Functions)组成。位数组初始化时所有位均为0。当插入元素时,通过多个哈希函数计算出多个哈希值,将这些哈希值对应的位置设为1。查询元素时,通过相同的哈希函数计算哈希值,检查对应的位是否全为1:如果存在0,则元素一定不在集合中。如果全为1,则元素可能在集合中(因为可能发生哈希冲突,所以存在误判的可能)。

(2)布隆过滤器的优缺点

优点:

空间效率高:相比哈希表或集合,布隆过滤器占用的内存极小,因为布隆过滤器是通过位数组存储的。

查询速度快:插入和查询的时间复杂度为O(k)(k为哈希函数数量),与数据规模无关。

无须存储元素本身:仅保存哈希后的位数组,适合敏感场景。

缺点:

存在误判率(FalsePositive):可能错误地判断一个不存在的元素为“存在”(无法避免)。误判率与位数组长度、哈希函数数量、元素数量相关。

(3)布隆过滤器的应用场景

缓存穿透防护:在Redis中,将查询过的不存在的key存入布隆过滤器。当请求到来时,先通过布隆过滤器判断是否存在,避免查询数据库。

14、如何保证Redis和MySQL的数据一致性?

image.png

具体解释旁路缓存+延迟双删:

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

15、Redis实现分布式锁的原理?

Redis分布式锁利用Redis单线程特性+原子操作实现对共享资源的互斥访问。

加锁阶段我们会使用set命令的NXPX选项,NX表示只有当lock_key不存在时才创建锁,PX是设置锁的过期时间,我们还需要指定客户端生成的唯一标识(通常是使用雪花算法生成)。

释放锁不能简单的使用del命令删除键,这可能会误删其他客户端持有的锁,我们应该使用Lu*脚本原子性的检查当前锁是否由该客户端持有,只有在匹配的情况下才删除锁。(使用Lua脚本是因为他是原子性的,避免了读取-检查-删除过程中可能出现的竞争条件)

对于长任务,需要通过看门狗机制定期续期锁

Redis集群部分

16、Redis切片集群?

Redis切片集群是通过多个Redis实例组成的每个实例存储部分的数据。具体是通过哈希槽机制来分配数据的,整个键空间划分成2的14次方个槽,数据过来之后会对数据的key经过哈希计算之后对2^14次方取余,从而定位到对应的节点去。客户端在发送请求时,会通过集群的任意节点进行连接,如果该节点存储了对应的数据就直接返回,如果没有就会根据请求的键值计算哈希槽,并路由到正确的节点去

Redis集群中节点之间信息如何同步/如果检查某个节点的状态?

Redis集群中每个节点都保存了集群的完整拓扑信息,包括每个节点的id,ip地址,端口,负责的哈希槽范围等。节点之间通过Gossip协议进行状态交换,每个节点会定期向随机的其他节点发送其状态信息,接收到信息的节点会根据接受的数据更新自己的状态,并继续把更新之后的状态传播给其他节点。通过周期性的信息交换,节点也可以检测其他节点的存活状态,如果某个节点没有在预定时间内响应,则节点会被标记为故障节点,其他节点可以采取措施,比如重新分配槽位,从而保证系统的高可用性

17、Redis主从复制(主从集群架构实现读写分离)?

Redis主从架构是指,每次写操作只请求主节点,读操作只请求从节点,这样就能减轻主节点的压力,适用于读多写少的场景。主节点可以通过全量复制和增量复制来保证主从节点数据的一致。

全量复制一般发生在第一次同步时,主要分为三个阶段,首先由从节点向主节点发生进行数据同步的请求,会携带两个参数,一个是主服务器的id,一个是复制的进度offset(第一次同步是-1),主服务器收到从节点的请求后会执行bgsave指令生成RDB文件,并传给从节点,从节点收到后会先清空当前数据,然后加载RDB文件,在主节点生成RDB、RDB文件传输过程中,从节点加载RDB文件这三个时间间隙,主节点会把这段时间的写操作缓存到缓冲区,等从节点加载好RDB之后,主节点会把缓冲区的写操作也发生给从节点

全量同步完成后,同步进入命令传播阶段,以维持数据的持续一致。主节点每执行写命令(或者配置成每隔一定时间或者一定次数的写),都会异步地将这个命令发送给所有从节点。从节点接收并执行相同的命令,从而保证数据实时更新。

如果因为网络波动导致从节点与主节点的连接短暂中断,重连后,从节点会再次发送数据同步请求。这次请求会带上自己已复制的偏移量offset。主节点检查该offset之后的数据是否还在自己的缓冲区中。(缓冲区:一个固定大小的、循环写入的队列,用于保存最近传播的写命令)如果offset之后的数据还在积压缓冲区中,主节点只需要将从offset开始到当前时刻的写命令发送给从节点。这就是部分同步,效率非常高。如果offset之后的数据已不在积压缓冲区中(中断时间太长,积压缓冲区被新数据覆盖了):则会触发一次新的全量同步

几点注意事项:

复制是异步的:主节点向从节点发送命令是异步的,不保证从节点立刻收到。这意味着存在极短时间的数据不一致窗口。

复制延迟:如果从节点处理命令的速度跟不上主节点,或者网络有延迟,就会导致复制延迟(Replication Lag)。从节点的数据会略旧于主节点。

避免全量同步:全量同步非常消耗资源和时间(尤其是大数据集)。应通过合理设置积压缓冲区大小来尽量避免网络闪断引发的全量同步。

从节点过期键:从节点不会主动删除过期键,而是等待主节点过期后发送DEL命令给它。

无盘复制:Redis支持无盘复制,主节点可以直接通过Socket将RDB数据发送给从节点,而不需要先在磁盘生成RDB文件。

Redis主从同步会有延迟,如果任务需要读取实时的数据,从节点还没来得及同步数据该怎么办?

强制读取主节点:对于需要实时数据的操作,可以直接读取主节点;将需要实时性的关键数据查询路由到主节点、对数据实时性要求不高的查询走从节点。

使用WAIT命令:Redis的WAIT命令可以阻塞当前客户端,直到指定数量的从节点完成同步之后再读取(严重增加延迟,大幅降低系统吞吐量和响应速度)。

监控复制延迟:通过监控目标从节点的复制偏移量,轮询检查目标从节点的偏移量,直到同步了主节点的写操作再读取。

18、Redis哨兵机制

哨兵机制是为了解决主节点宕机,从节点没有顶替成主节点导致服务器无响应的问题。

哨兵节点会对redis的主从服务节点进行监控,当主节点发生故障,哨兵节点会选择一个合适的从节点提升为主节点,并通知其他节点和客户端进行更新操作。

怎么判断主节点挂了?

第一种是主观下线:哨兵节点会每隔1s发生一个ping命令给所有节点,如果哨兵节点超过一段时间还未收到对应节点的pong回复,就会认为这个节点下线了

第二个是只有主节点才有的,客观下线:有可能因为网络抖动导致哨兵误判,所以会由所有哨兵节点发起投票,只有超过集群整数一半以上的哨兵节点认为主节点下线,才算是客观下线,这个时候就需要进行主从切换。

Redis主节点的选举?

先会把已经下线的节点排除,从正常的从节点中选取,会根据从节点的优先级进行选择,优先选优先级高的也就是优先级值比较小的;如果优先级相同则查看主从复制的offset,越大说明同步的数据越多,优先级越高;如果offset也一样,那就比较id,选id小的。

19、Redis脑裂现象?

一般是由于网络分区,导致主节点与哨兵节点和从节点分区了,导致哨兵节点联系不上主节点于是选举出了新的主节点,多个主节点同时负责写的操作,导致数据不一致

解决:

设置最小的从节点个数:主节点必须至少有N个从节点连接正常,主节点才能接受写请求;

延缓从节点选举:在哨兵端增加一个延迟,给网络恢复一个机会。哨兵故障转移的超时时间,适当调大这个值(例如从30秒调到60秒),可以在主节点下线后,让哨兵等待更长的时间再开始故障转移。这给了网络分区自我修复的时间窗口。如果网络在超时时间内恢复,旧主节点重新被哨兵感知,就不会触发不必要的故障转移(从节点选举)。

20、如果mysql有2000W数据,redis只存20W,怎么保证redis里的都是热点数据?

(1)冷启动预热,根据业务经验以及历史数据,先把已知的高频key先塞进去,比如秒杀商品这种;

(2)动态识别热点key:一个是在客户端和Redis服务器之间增加一个代理层,所有请求都经过代理,代理层可以无侵入地统计所有请求的Key和频率,然后上报给监控系统,优点:对Redis服务器本身性能零影响,数据最准确;缺点:需要引入和维护额外的代理组件,增加了架构的复杂性;

另一个可以使用京东的hotkey开源工具,他的原理是在客户端侧部署一个Agent代理。Agent会本地统计每个Key的访问次数。如果某个Key在指定时间窗口内达到了预设的阈值,就将其上报给一个集中的监控Server。Server聚合所有客户端的上报信息,就能准实时地发现集群范围内的热点Key;

(3)使用redis自带的内存淘汰机制,LRU可以自动剔除最近最少访问的key,LFU可以剔除最久未使用的key。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
Mrchen
下载 APP