前言
揭秘面试中常见的Redis题目,本文将带你深入探索那些看似晦涩却充满真知灼见的面试题目。内容精选自小林coding的博客、Redis官方文档以及AI助手的专业解答,确保了信息的准确性与实用性。每一条答案都经过我仔细的查证和整理,致力于为你和我自己提供最权威的面试备战资料。投入时间阅读,你将会发现这份努力是值得的,文章末尾还藏有惊喜“彩蛋”,不容错过!
1、什么是Redis?
Redis是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存、消息队列和分布式锁等场景。
2、Redis和Memcached的共同点和区别
-
共同点
1、都是基于内存的数据库,一般都用来做缓存使用
2、都有过期策略
3、两者的性能都非常高
-
区别
1、Redis支持的数据类型更丰富(String、Hash、List、Set、Zset),而Memcached只支持最简单的key-value数据类型
2、Redis支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载进行使用,而Memcached没有持久化的功能,数据全部存在内存中,Memcached重启或者挂掉后,数据就没了
3、Redis原生支持集群模式,Memcached没有原生的集群模式,需要依赖客户端来实现往集群中分片写入数据
4、Redis支持发布订阅模型,Lua脚本,事物等功能,而Memcached不支持
3、为什么用Redis作为MySQL的缓存?
-
高性能
MySQL的数据是在硬盘上读取的,这个耗费的时间肯定是比在内存中读取数据慢的。所以将MySQL中的数据存到Redis上加快数据的读取速度。如果MySQL中的数据改变后更新Redis的数据即可。
-
高并发
单台设备的Redis的QPS(Query Per Second,每秒钟处理完成请求的次数)是高于MySQL的,所以使用Redis作为MySQL的缓存可以增加系统的高并发性。
4、Redis常见的数据结构和使用场景
结构类型 | 使用场景 |
---|---|
String字符串 | 缓存队形、常规计数、分布式锁、共享session信息等 |
List列表 | 消息队列 |
Set集合 | 聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖 |
Hash散列 | 缓存对象、购物车 |
Zset有序集合 | 排序场景、比如排行榜、电话和姓名的排序等 |
5、Redis是单线程吗?
Redis不是单线程。Redis的单线程指的是「接收客户端的请求→解析请求→进行数据读写等操作→发送数据给客户端」这个过程是由一个(主线程)来完成的,这也就是为什么我们常说Redis是单线程的原因。
6、Redis单线程模式是怎样的?
Redis的单线程模式是指Redis的核心功能由一个线程来完成。这个线程负责所有的客户端请求,包括:
- 接收客户端链接
- 解析客户端命令
- 执行命令
- 将结果返回给客户端
Redis的单线程模式具有以下优点
- 简单易实现:单线程模式的代码量少,易于理解和维护
- 性能高:单线程模式避免了多线程之间的上下文切换,可以提高性能
- 一致性好:单线程模式可以避免多线程竞争,保证数据的一致性
Redis的单线程模式的缺点
- 并发能力有限:单线程模式只能处理一个请求,因此并发能力有限
- 不适合CPU密集型操作:如果命令执行需要大量的CPU资源,那么单线程模式可能会导致性能下降
为了解决单线程模式的并发能力有限的问题,Redis在6.0版本之后引入了多线程IO。多线程IO可以并行处理多个客户端连接,从而提高并发能力。
但是,Redis的核心功能仍然是单线程执行的。这是因为Redis的核心功能是键值对操作,而键值对操作是内存操作。内存操作的速度非常快,因此单线程执行就可以满足大多场景的需求。
总而言之,Redis的单线程模式是一种简单实现,性能高、一致性好的模式。但是,它也存在并发能力有限和不适合CPU 密集操作的缺点。在实际应用中,需要根据具体的应用场景来选择合适的模式。
7、Redis 6.0 之前为什么使用单线程?而Redis 6.0 之后为什么引入了多线程?
Redis的性能瓶颈在于内存大小和网络I/O的限制,如果想使用服务器的多核可以启动多个节点或者采用分片集群的方式,并且单线程的使用降低了维护成本,安全性高。所以Redis6.0之前使用单线程完全可以满足大部分场景。
虽然单线程模式可以满足大部分场景,但是随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O上,所以引入多个线程来进行处理I/O处理上。
8、Redis的多线程模式是怎样的?
Redis多线程IO模式下,Redis会使用多个线程来处理客户端连接。每个线程负责处理一个或多个客户端连接,从而提高并发能力。
多线程IO模式的具体工作流程如下:
- 主线程负责接收客户端连接请求,并将连接请求放入等待队列
- IO线程从等待队列中取出连接请求,并建立连接
- IO线程读取客户端发送的命令
- 主线程执行命令
- 主线程将命令执行的结果写回客户端
多线程IO模式的优点:
- 并发能力高:多个线程可以并行处理多个客户端请求,并将连接请求放入等待队列
- 效率高:IO线程可以并行处理网络IO操作,而主线程可以专注于处理命令执行,从而提高效率
多线程IO模式的缺点
- 代码的复杂度增加:多线程IO模式的代码量比单线程模式多,因此代码复杂度也更高。
- 潜在的线程安全问题:多线程模式下,需要考虑线程安全问题,例如数据竞争
总而言之,多线程IO模式是一种高并发、高效率的模式。但是,它也存在代码复杂度高和潜在的线程安全问题的缺点。在实际应用中,需要根据具体的应用场景来选择合适的模式。
9、Redis如何实现数据不丢失?
Redis的读写操作都是在内存中,所以Redis的性能才会高,但是当Redis重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在Redis重启就能够从磁盘中恢复原有的数据。
Redis共有三种数据持久化的方式:
- AOF日志:每执行一条写操作命令,就把该命令以追加的方式写到一个文件里
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘
- 混合持久化方式:集成AOF和RDB的优点
10、AOF的配置策略
写回策略 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,最大程度保证数据不丢失 | 每个写命令都要写回硬盘,性能开销大 |
Everysec | 每秒写回 | 性能适中 | 宕机时会丢失1秒内的数据 |
No | 由操作系统控制写回 | 性能好 | 宕机时丢失的数据可能会更多 |
11、RDB的配置策略
save 900 1
save 300 10
save 60 10000
- 900秒之内,对数据库进行了至少1次的修改
- 300秒之内,对数据库进行了至少10次的修改
- 60秒之内,对数据库进行了至少10000次的修改
12、混合持久化介绍
RDB优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF优点是丢失数据少,但是数据恢复不快。
为了集成两者的优点,Redis4.0提出了混合使用AOF日志和内存快照,也叫混合持久化,既保证了Redis重启速度,又降低数据丢失风险。
混合持久化工作在AOF日志重写过程,当开启了混合持久化时,在AOF重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以RDB方式写入到AOF文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区的增量命令会以AOF方式写入到AOF文件,写入完成后通知主进程将新的含有RDB格式和AOF文件替换就的AOF文件。
也就是说,使用了混合持久化,AOF文件的前半部分是RDB格式的全量数据,后半部分是AOF格式的增量数据。
混合持久化的优点:
- 混合持久化结合了RDB和AOF持久化的优点,开头为RDB的格式,使得Redis可以更快的启动,同时结合AOF的优点,有减低了大量数据丢失的风险。
混合持久化的缺点:
- AOF文件中添加了RDB格式的内容,使得AOF文件的可读性变得很差。
- 兼容性差,如果开启了混合持久化,那么混合持久化AOF文件,就不能用在Redis4.0之前的版本了。
13、Redis如何实现服务的高可用?
Redis可以使用集群的方式来实现高可用,具体常表现为主从复制模式和哨兵模式。
-
主从复制
主从复制模式是Redis最常用的高可用方案。在这种方案中,将一个Redis实例作为主节点,其他Redis实例作为从节点。主节点负责读写操作,从节点负责从主节点同步数据。当主节点出现故障时,可以将其中一个从节点提升为主节点,继续提供服务(这里一般需要手动)。
-
哨兵模式
哨兵模式是Redis官方推荐的高可用方案。哨兵模式由一个或多个哨兵节点组成,哨兵节点负责监控主节点和从节点的状态。当主节点出现故障时,哨兵节点会自动将其中一个从节点提升为主节点,并通知客户端进行连接切换。
14、集群脑裂导致数据丢失怎么办?
首先脑裂可以简单解释为:一个人有两个大脑不知道到底该受谁控制。
然后出现脑裂的原因是:在Redis集群中主节点的网络突然发生了问题,它与所有的从节点都失联了,但此时的主节点和客户端的网络是正常,这个客户端并不知道Redis内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧节点缓存到了缓冲区里,因为从节点之间的网络问题,这些数据都是无法同步给从节点的。这时,哨兵也发现了主节点失联了。它就认为主节点挂了(但实际上主节点正常运行,只是网络出了问题),于是哨兵就会从「从节点」中选举出一个leader作为主节点,这时集群就有两个主节点了—脑裂出现了。然后,网络突然就好了,哨兵因为之前已经选举出一个新的主节点了,它就会把旧的主节点降级为从节点,然后从节点会向新主节点请求数据同步,此刻之前客户端的上传的数据就是丢失。
最后解决方案是:在Redis的配置文件中调整两个参数
- min-slaves-to-write x,主节点必须要有至少x个从节点连接,如果小于这个数,主节点会禁止写数据
- min-slaves-max-lag x,主从数据复制和同步延迟不能超过x秒,如果超过,主节点会禁止写数据
15、Redis使用的过期删除策略是什么?
Redis是可以对key设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而这个工作就是过期键值删除策略。
每当我们对一个key设置了过期时间时,Redis会把该key带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有key的过期时间。
当我们查询一个key时,Redis首先检查该key是否存在于过期字典中:
- 如果不存在,则正常读取键值
- 如果存在,则会获取该key的过期时间,然后与当前系统时间进行对比,如果比系统时间大,那就没有过期,否则判定该key已过期
Redis使用的过期删除策略是「惰性删除+定期删除」这两种策略配合使用
-
惰性删除策略
惰性删除策略的做法是:不主动删除过期键,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key
惰性删除策略的优点
- 只会在访问key时才会对过期的键值进行删除,所以只会使用很少的系统资源,因此惰性删除策略对CPU友好
惰性删除策略的缺点
- 如果一个key一直访问不到即使它过期也会一直存在于数据库中造成内存空间浪费,所以惰性删除策略对内存不友好
-
定期删除策略
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的key进行检查,并删除其中过期key
定期删除策略的优点:
- 通过限制删除操作执行的时长和频率,来减少删除对CPU的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用
定期删除策略的缺点
- 难以确定删除操作执行的时长和频率。如果执行的太频繁反而会对CPU不友好,如果执行的太少,那么也会出现过期key长时间存在于数据库的问题造成内存浪费。
所以惰性删除策略和定期删除策略都各有优缺点,所以Redis选择这两种策略配合使用
16、Redis持久化时,对过期键会如何处理?
Redis持久化文件有两种格式:RDB和AOF。
RDB文件分为两个阶段,RDB文件生成阶段和加载阶段
-
RDB文件生成阶段:从内存状态持久化成RDB(文件)的时候,会对key进行过期检查,过期的键「不会」被保存到新的RDB文件中,因此Redis中的过期键不会对生成新RDB文件产生任何影响。
-
RDB加载阶段:RDB加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况
- 如果Redis是「主服务器」运行模式的话,在载入RDB文件是,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入RDB文件的主服务器造成影响。
- 如果Redis是「从服务器」运行模式的话,在载入RDB文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键载入RDB文件的从服务器也不会造成影响
AOF文件分为两个阶段,AOF文件写入阶段和AOF重写阶段
- AOF文件写入阶段:当Redis以AOF模式持久化时,如果数据库某个过期键还没被删除,那么AOF文件会保留此过期键,当此过期键被删除后,Redis会向AOF文件追加一条DEL命令来显式地删除该键值
- AOF重写阶段:执行AOF重写时,会对Redis中的键值对进行检查,已过期的键不对被保存到重写后的AOF文件中,因此不会对AOF重写造成任何影响
17、Redis主从模式中,对过期键会如何处理?
当Redis运行主从模式下时,从库不会进行过期扫描,对从库过期的处理是被动的。也就是即使从库中的key过期了,如果有客户端访问从库时,依然可以得到key对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在key到期时,发布一条del指令,同步到所有的从库,从库执行这条del指令来删除过期的key。
18、Redis内存满了,会发生什么?
在Redis的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在Redis的配置文件中可以找到,配置项为maxmemory。
19、Redis内存淘汰策略有哪些?
Redis内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
-
不进行数据淘汰策略
noeviction(Redis3.0之后,默认的内存淘汰策略):它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
-
进行数据淘汰的策略
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和在「所有数据范围内进行淘汰」这两类策略。
在设置了过期时间的数据中进行淘汰
- volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早期的键值
- volatile-lru:淘汰所有设置了过期时间中的最久未使用的键值
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值
在所有数据范围内进行淘汰
- allkeys-random:随机淘汰任意键值
- allkeys-lru:淘汰整个键值中最久未使用的键值
- allkeys-lfu:淘汰整个键值中最少使用的键值
20、LRU 算法和 LFU 算法有什么区别?
LRU(Least Recently Used,最近最少使用)算法
- LRU算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也会更高”
- LRU算法维护了一个链表,链表中包含所有缓存的键值对,链表的头节点是最近被访问的键值对,链表尾部节点是最近最少被访问的键值对
- 当需要淘汰数据时,LRU算法会从链表的尾部开始删除键值对
LFU(Least Frequently Used, 最少使用)算法
- LFU算法根据数据的访问频率来进行淘汰数据,其核心思想是“访问频率越低,越有可能被淘汰”
- LFU算法维护了一个哈希表,哈希表的键是缓存的键值对,哈希表的value是键值对的访问次数
- 当需要淘汰数据时,LFU算法会从哈希表中找到访问次数最少的键值对进行淘汰
特性 | LRU算法 | LFU算法 |
---|---|---|
淘汰策略 | 根据历史访问记录 | 根据访问频率 |
数据结构 | 链表 | 哈希表 |
优点 | 简单易实现,性能较高 | 可以淘汰长期未使用的数据,更节省内存 |
缺点 | 不适用于访问模式不一致的数据 | 实现复杂,性能相对较低 |
21、缓存雪崩
缓存雪崩产生的原因是:大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在Redis中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统雪崩,这就是缓存雪崩的问题。
对于缓存雪崩的问题,可以采用两种方案解决
- 将缓存失效时间随机打散:我们可以在原有的失效时间基础上增加一个随机值(比如1到10分钟)这样每个缓存的过期时间降低了其重复性,也就降低了缓存集体失效的概率
- 设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题
22、如何避免缓存击穿?
缓存击穿其实是缓存雪崩的子集其产生的原因是:缓存中的某热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,就是是缓存击穿的问题。
对于缓存击穿的问题,可以采用两种方案解决
- 互斥锁方案(Redis中使用setNX方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
23、如何避免缓存穿透
缓存穿透的原因是:当用户访问数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存穿透的发生一般有这两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务
应对缓存穿透的方案,常见的方案有三种
- 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在API入口处,我们要判断请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库
- 设置空值或者默认值:当我们的线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库
- 使用布隆过滤器(一种空间效率极高的概率型数据结构)快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询Redis和布隆过滤器,而不会查询数据库,保证了数据库正常运行,Redis自身也是支持布隆过滤器的
24、解释布隆过滤器
布隆过滤器是一种数据结构,它可以用来判断一个元素是否在一个集合中。它比传统的哈希表更节省空间,但有一定的误报率。
布隆过滤器由一个二进制数组和一系列哈希函数组成。当一个元素被加入集合时,会通过哈希函数将其映射到数组中的几个比特位,并将这些比特位设置为1。
当查询一个元素是否在集合中时,会再次对该元素进行哈希计算,并检查相应的比特位是否都为1。如果所有的比特位都为1,则将该元素可能在集合中;如果有一个或多个比特位为0,则该元素一定不在集合中。
布隆过滤器的优点:
- 空间效率高:布隆过滤器只需要一个二进制数组,空间复杂度为O(n),其中n是集合中元素的个数
- 查询速度快:布隆过滤器的查询时间复杂度为O(k),其中k是哈希函数的个数
布隆过滤器的缺点
- 存在误报率:由于哈希碰撞的存在,布隆过滤器可能会错误地判断一个元素存在于集合中
- 不支持删除操作:一旦一个元素被加入到布隆过滤器中,将无法将其删除
应用场景:
- 缓存:布隆过滤器可以用来判断一个元素是否存在缓存中,以减少对数据库的访问
- 去重:布隆过滤器可以用来判断一个元素是否已经被处理过,以避免重复处理
- 垃圾邮件过滤:布隆过滤器可以用来判断一封邮件是否是垃圾邮件
25、如何设计一个缓存策略,可以动态缓存热点数据呢?
由于数据存储受限,系统并不是将所有数据都需要存放到缓存中,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存策略。
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据
以电商平台场景中的例子,现在要求只缓存用户经常访问的Top1000的商品。具体实现如下:
- 先通过缓存系统做一个排序队列(比如存放1000个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前
- 同时系统会定期过滤掉队列中排名最后的200个商品,然后再从数据库中随机读取出200个商品加入到队列中
- 这样当请求每次到达的时候,会先从队列中获取商品ID,如果命中,就根据ID再从另一个缓存结构中读取实际的商品信息,并返回。
在Redis中可以用zadd方法和zrange方法来完成排序队列和获取200个商品的操作
26、说说常见的缓存更新策略
常见的缓存更新策略共有三种:
- Cache Aside(旁路缓存)策略
- Read/Write Through(读穿/写穿)策略
- Write Back(写回)策略
Cache Aside(旁路策略)是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」
写策略的步骤:
- 先更新数据库中的数据,再删除缓存中的数据
读策略的步骤
- 如果读取的数据命中了缓存,则直接返回数据
- 如果读取的数据没有命中缓存,则从数据看中读取数据,然后将数据写入到缓存,并且返回给用户
Read/Write Through(读穿/写穿)策略原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
Read Through策略
- 先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用
Write Through
- 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成
- 如果缓存中数据不存在,直接更新数据库,然后返回
Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行
27、Redis如何实现延迟队列
延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种
- 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消
- 打车的时候,在规定时间没有车主接单,平台会取消你的订单并提醒你暂时没有车主接单
- 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单
在Redis可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet有一个Score属性可以用来存储延迟执行的时间
使用zadd xx(队列名称)score1 value1命令就可以一直往内存中生产消息。再利用zrangebyscore查询符合条件的所有待处理的任务,通过循环执行队列任务即可
28、Redis的大key如何处理?
大key表示在数据库中,key对应的value很大
一般而言,下面这两种情况被称为大key
- String类型的值大于10KB
- Hash、List、Set、Zset类型的元素的个数超过5000个
大key会造成的影响
- 客户端超时阻塞:由于Redis执行的命令是单线程处理,然后在操作大key时会比较耗时,那么就会阻塞Redis,从客户端这一视角看,就是很久很久都没有响应
- 引发网络阻塞:每次获取大key产生的网络流量较大,如果一个key的大小是1MB,每秒访问量为1000,那么每秒会产生1000MB的流量,这对于普通千兆网卡的服务器来说是灾难性的
- 阻塞工作线程:如果使用del删除大key时,会阻塞工作线程,这样就没办法处理后续的命令
- 内存分布不均:集群模型在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS也会比较大
查找大key
-
redis-cli-bigkeys命令查找大key
redis-cli -h 127.0.0.1 -p 6379 -a "password" -- bigkeys
使用的时候注意事项:
- 最好选择在从节点上执行该命令。因为主节点上执行,会阻塞主节点
- 如果没有从节点,那么可以选择在Redis实例业务压力的低峰值阶段进行扫描查询,以免影响到实例的正常运行;或者可以使用 -i参数控制扫描间隔,避免长时间扫描降低Redis实例的性能
该方式的不足之处
- 这个方法只能返回每种类型中最大的那个bigkey,无法得到大小排在前N位的bigkey
- 对于集合类型来说,这个方法只统计集合元素个数的多少,而不是实际占用的内存量。但是,一个集合中的元素个数多,并不一定占用的内存多。因为,有可能每个元素占用的内存很小,这样的话,即使元素个数有很多,总内存开销也不大
-
使用SCAN命令查找大key
使用SCAN命令对数据库扫描,然后用TYPE命令获取返回的每一个key的类型
对于String类型,可以直接使用STRLEN命令获取字符串的长度,也就是占用的内存空间字节数。
对于集合类型来说,有两种方法可以获得它占用的内存大小
- 如果能够预先从业务层知道集合元素的平均大小,那么,可以使用下面的命令获取集合元素的个数,然后乘以集合元素的平均大小,这样就能获得集合占用的内存大小了。List类型:LLEN命令;Hash类型:HLEN命令;Set类型:SCARD命令;Sorted Set类型:ZCARD命令
- 如果不能提前知道写入集合元素的大小,可以使用MEMORY USAGE命令,查询一个键值对占用的内存空间
-
使用RdbTools工具查找大key
使用RdbTools第三方开源工具,可以用来解析Redis快照的(RDB)文件,找到其中的大key。
比如,下面这条命令,将大于10kb的key输出到一个表格文件
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
删除大key
删除操作的本质是要释放键值对占用的内存空间,不要小瞧内存的释放过程
释放内存只是第一步,为了更加高效地管理内存空间,在应用程度释放内存时,操作系统需要把释放掉的内存快插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成Redis主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成Redis连接耗尽,产生各种异常。
因此删除大key给出两种方法
-
分批次删除
对于删除大Hash,使用hsacn命令,每次获取100个字段,再用hdel命令,每次删除一个字段
def del_large_hash(): r = redis.StrictRedis(host='redis-host1', port=6379) large_hash_key ="xxx" #要删除的大hash键名 cursor = '0' while cursor != 0: # 使用 hscan 命令,每次获取 100 个字段 cursor, data = r.hscan(large_hash_key, cursor=cursor, count=100) for item in data.items(): # 再用 hdel 命令,每次删除1个字段 r.hdel(large_hash_key, item[0])
对于删除大List,通过ltrim命令,每次删除少量元素
def del_large_list(): r = redis.StrictRedis(host='redis-host1', port=6379) large_list_key = 'xxx' #要删除的大list的键名 while r.llen(large_list_key)>0: #每次只删除最右100个元素 r.ltrim(large_list_key, 0, -101)
对于删除大Set,使用sscan命令,每次扫描集合中100个元素,再用srem命令每次删除一个键
def del_large_set(): r = redis.StrictRedis(host='redis-host1', port=6379) large_set_key = 'xxx' # 要删除的大set的键名 cursor = '0' while cursor != 0: # 使用 sscan 命令,每次扫描集合中 100 个元素 cursor, data = r.sscan(large_set_key, cursor=cursor, count=100) for item in data: # 再用 srem 命令每次删除一个键 r.srem(large_size_key, item)
对于删除大ZSet,使用zremrangebyrank命令,每次删除top100个元素
def del_large_sortedset(): r = redis.StrictRedis(host='large_sortedset_key', port=6379) large_sortedset_key='xxx' while r.zcard(large_sortedset_key)>0: # 使用 zremrangebyrank 命令,每次删除 top 100个元素 r.zremrangebyrank(large_sortedset_key,0,99)
-
异步删除
从Redis4.0版本开始,可以采用异步删除法,用unlink命令代替del来删除
这样Redis会将这个key放到一个异步线程中进行删除,这样不会阻塞主线程
除了主动调用unlink命令实现异步删除之外,我们还可以通过配置参数,达到某些条件的时候自动进行异步删除
主要有4种场景,默认都是关闭的:
lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no slave-lazy-flush no
- lazyfree-lazy-eviction:表示当Redis运行内存超过maxmemory时,是否开启lazy free机制删除
- lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启lazy free机制
- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的del键的操作,比如rename命令,当目标键已存在,Redis会先删除目标键,如果这些目标键是一个big key,就会造成阻塞删除掉问题,此配置表示在这种场景中是否开启lazy free的机制删除
- slave-lazy-flush:针对slave(从节点)进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的数据,它表示此时是否开启lazy free机制删除
29、Redis管道有什么用?
管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个Redis命令,从而提高整个交互的性能。
使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。
但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞
要注意的是,管道技术本质上是客户端提供的功能,而非Redis服务端的功能
30、如何使用Redis实现分布式锁的?
Redis的SET命令有个NX参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
- 如果key不存在,则显示插入成功,可以用来表示加锁成功
- 如果key存在,则会显示插入失败,可以用来表示加锁失败
基于Redis节点实现分布式锁,对于加锁操作,我们需要满足三个条件
- 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
- 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
- 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端
SET lock_key unique_value NX PX 10000
- lock_key 就是 key 键;
- unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
- NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
- PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
Redis分布式锁的优点:
- 性能高效(这是选择缓存实现分布式锁最核心的出发点)
- 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
- 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)
Redis分布式锁的缺点:
- 超时时间不好确定:如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源
- Redis主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性:如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
超时时间确定:
- 我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。
Redis解决分布式的不可靠性使用了一个分布式锁算法 Redlock(红锁):
- Redlock算法的基本思路,是让客户端和多个独立的Redis节点依次申请加锁,如果客户端能够和半数以上的节点成功的完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
Redlock算法的加锁三个过程
-
第一步是,客户端获取当前时间(t1)。
-
第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:
- 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。
- 如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。
-
第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。
31、Redis事务支持回滚吗?
Redis不支持事务回滚。
彩蛋在这!!!
如果你手中有令人兴奋的Redis面试题目,欢迎在评论区分享!我将全力以赴为你提供详尽解答。同时,别忘了期待我的下一篇力作,将聚焦Python面试题目。精彩内容,即将呈现,敬请保持关注!