一、介绍一下什么是 Redis,有什么特点?
Redis 是一个高性能的 key-value 内存数据库。
不同于传统的 MySQL 这样的关系型数据库,Redis 主要使用内存存储数据(当然也支持持久化存储到硬盘上),并非是使用 “表” 这样的结构来组织数据,而是使用键值对的方式。
Redis 没有关系型数据库的复杂查询以及约束等功能,换来的是简单易用和更高的性能。
特点:
- 使用内存存储(高性能)。
- ⽀持多种数据结构。
- 支持持久化。
- 单线程处理请求。
- ⽀持主从复制。
- ⽀持哨兵模式。
- ⽀持集群模式。
- 支持事务。
- ⽀持多语言客户端。
- ... ...
二、Redis 支持哪些数据类型
五种最核心的类型:
- String
- List
- Hashmap
- Set
- ZSet
在后续版本中也逐渐新增了一些新的数据类型:
- Bitmap,通过二进制位表示某个数字是否存在。
- Bitfield,把字符串当做位图,并进行位操作。
- Hyperloglog,基于位图的结构实现 “计数” 效果(统计某个数字出现几次,比 hashmap 节省空间)。
- Geospatial。地理信息,存储经纬度。并且可以进行一些空间计算(比如找出某个点附近 1km 内都有哪些点)。
- Stream,消息队列。
这几个类型都属于特定场景中才会使用的类型,不像前面五个类型通用性那么强。
参考:Understand Redis data types | Docs
三、Redis 数据类型底层的数据结构 / 编码方式是什么
- embstr:是针对短字符串的优化实现。小于等于 39 字节的字符串使用 embstr,大于则使用 raw。
- ziplist:压缩列表,本质上是个字节数组,可以节省内存空间。当有序集合、哈希表、列表元素少,并且元素都是短字符串的时候,会使用这个。
- linklist:链表,需要额外内存开销,容易引入内存碎片。
- skiplist:跳表,能够 O(logN) 的复杂度进行查找元素的复杂链表。
- quicklist:是个链表,每个元素是⼀个 ziplist。
- listpack:从 Redis 7 开始,引入了新的数据结构 listpack,用来代替 ziplist。
使用哪种编码是 redis 内部自动决定的,可以使用 object encoding key 来查看编码。
四、ZSet 为什么使用跳表,而不是使用红黑树来实现
跳表的插入 / 查询 / 删除时间复杂度都是 O(logN),和红黑树是一样的。但是跳表实现起来更简单,而且不需要重新平衡这样的操作。
五、Redis 的常见应用场景有哪些
- 缓存:一些经常被访问到的热点数据,可以使用 Redis 进行缓存,降低查询数据库的次数。
- 计数器:统计点击次数 / 访问次数 / 收藏次数等常见需求。
- 排行榜:可以基于 Redis 的 ZSet 轻松实现。
- 分布式会话:使用 Redis 存储会话信息,可以使用户访问到系统的不同模块时都使用同一个公共的会话。
- 分布式锁:对于分布式系统的并发访问控制,可以基于 Redis 来实现分布式锁,避免并发的竞争问题(类似于线程安全问题)。
- 消息队列:Redis 自身支持 Stream 数据类型,可以作为简单的消息队列使用。
- ... ...
六、如何测试 Redis 服务器的连通性
七、如何设置 key 的过期时间
八、Redis 为什么是单线程模型
Redis 内部的逻辑比较简单,一般的性能瓶颈都是出现在内存或者 IO 上,很少是 CPU。因此使用多线程并没有太大的收益,反而可能会引入线程安全问题。
从 Redis6.0 开始,引入多线程,此时只是使用多个线程去处理网络请求 + 协议解析,真正执行 Redis 命令仍然是单线程完成的,这样做可以进一步提高 IO 的处理效率。
九、Redis 里的 IO 多路复用是怎么回事
Redis 主要是基于了 Linux 提供的 epoll 机制来完成 IO 多路复用。
简单的说,所谓的 “IO多路复用” 就是用一个线程来管理多个 socket(文件描述符),并按需激活线程。
虽然一个 Redis 服务器要处理很多个客户端的请求,但是这些客户端的请求并非是 “同一时刻” 到达的。在一个单位时间内,可能只有两三个客户端的请求到来了,因此如果使用传统的每个线程处理一个连接的方式实现,大部分线程其实都是挂起的(不活跃的)。因此与其搞了一堆线程,但是线程在摸鱼,还不如就用一个线程来处理。上述这样的需求,被操作系统内核封装好了,就是 IO 多路复用。
Linux 中,IO 多路复用有三种实现:
- select
- poll
- epoll
其中,epoll 是最新的版本(2005 年左右引入的),也是最高效的版本。epoll 在内核中维护了一个红黑树,来管理所有的 socket,并且每个节点都关联了一个事件回调。当系统内核感知到网卡收到数据了,进一步判定这个数据是给哪个 socket 的,随之调用对应的回调,进一步唤醒用户线程,来处理这个收到的数据。此时这个用户线程是 “忙碌” 的,要不停的处理来自各个客户端的请求数据。
十、Redis 的持久化机制有哪些
RDB 和 AOF。
参考:【Redis 进阶】持久化(RDB & AOF)_redis 加载rdb-CSDN博客
十一、RDB 的持久化触发条件是怎样的
手动触发:使用 save 或者 bgsave 即可触发持久化。
自动触发:
- 在配置文件中通过 save m n 即可设定 m 秒内发生 n 次修改,就触发持久化。
- 从节点进行全量复制操作,触发持久化。
- 执行 shutdown 命令,关闭 redis server,触发持久化。
十二、AOF 的文件同步策略有哪些
十三、AOF 的重写机制是怎样的
参考:【Redis 进阶】持久化(RDB & AOF)_redis 加载rdb-CSDN博客
十四、Redis 的 key 的过期删除策略是怎样的
惰性过期
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。
定期过期
每隔一定的时间,会扫描一定数量的数据库的 expires 字典中⼀定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中 key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。
Redis 中同时使用了惰性过期和定期过期两种过期策略。
- 每隔 100ms 就随机抽取⼀定数量的 key 来检查和删除的。
- 在获取某个 key 的时候,redis 会检查⼀下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。
十五、如果大量的 key 在同一时间点过期会产生什么问题?如何处理?
如果大量的 key 过期时间设置的过于集中,到过期的那个时间点,Redis 可能会出现短暂的卡顿现象。
为何会出现卡顿呢?
Redis 针对过期 key 的删除,采取定期采样删除 + 惰性删除两种方式结合。
对于定期采样删除来说,Redis 会注册一个定时器,每隔一定时间触发一次采样删除。在删除的时候会先根据事先统计好的过期 key 的个数来决定后续策略。如果过期 key 的数目超过总 key 数目的 25% 以上,就会使 Redis 持续删除过期 key 直到最大时间删除时间(默认是 25ms)。之所以限制这个最大时间,就是为了防止 Redis 被卡住太久。但是即使如此,有些对性能要求较高的场景仍然会因为阻塞 25ms 导致性能下降严重。
解决方案:可以在过期时间上加一个随机值,使得过期时间分散一些。
很多时候过期时间并不一定非得卡的那么精准。比如设定 2s 之后过期,不⼀定非要正好的 2000ms,2001、2002、1999、1998 甚至 2010 这些时间都是问题不大的。因此让过期时间通过随机值分散,就可以避免同⼀时刻过期的 key 太多,从而降低触发 25% 这个阈值的可能性。
十六、Redis 的淘汰策略是怎样的
当 Redis 内存不足时,如果继续尝试添加新的 key,就会触发淘汰策略,把之前的旧 key 给自动删除掉。
Redis 提供的淘汰策略有以下几种:
- volatile-lru 当内存不足以容纳新写入数据时,从设置了过期时间的 key 中使用 LRU(最近最
- 少使用)算法进行淘汰。
- allkeys-lru 当内存不足以容纳新写入数据时,从所有 key 中使用 LRU(最近最少使用)算法进行淘汰。
- volatile-lfu 4.0 版本新增,当内存不足以容纳新写入数据时,在过期的 key 中,使用 LFU 算法进行删除 key。
- allkeys-lfu 4.0 版本新增,当内存不足以容纳新写入数据时,从所有 key 中使用 LFU 算法进行淘汰。
- volatile-random 当内存不足以容纳新写入数据时,从设置了过期时间的 key 中,随机淘汰数据。
- allkeys-random 当内存不足以容纳新写入数据时,从所有 key 中随机淘汰数据。
- volatile-ttl 在设置了过期时间的 key 中,根据过期时间进行淘汰,越早过期的优先被淘汰。
- noeviction 默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
关于 LRU 和 LFU
- LRU:Least Recently Used 最近最久使用算法,哪个 key 上次访问时间最长,就干掉。
- LFU:Least Frequently Used 最近最常使用算法,哪个 key 在最近⼀段时间里使用次数最少,就干掉。
在上述策略中,默认策略一般来说都是比较好的选择。
- 部署 Redis 的机器正常来说,不应该消耗到内存耗尽才做处理,应该有完善的监控报警体系,当内存接近上限的时候提前通知程序员,尽早进行扩容。
- 如果确实出现内存耗尽了,相比于 Redis 偷偷的删除一部分 key,带伤运行(当然,volatile 前缀系列的策略不算带伤),不如尽早显式的把问题暴露出来,及时处理。
- 除非是 Redis 中的数据完全不关键,否则不应该使用 allkeys 系列的策略。
十七、Redis 如果内存用完了,会出现什么情况
会触发 Redis 的淘汰策略,把⼀些 key ⾃动删除掉。
参考十六。
十八、Redis 为什么把数据放到内存中
效率。
访问内存的速度比访问硬盘的速度快 3-4 个数量级。
如下表可以看到:
进行一次内存的随机访问操作是 100ns,进行一次硬盘的随机访问操作是 10 000 000 ns,这个差距还是非常明显的。因此使用内存存储数据让 Redis 相比于 MySQL 等数据库,具有了非常显著的优势。当然,使用内存存储的劣势也是比较明显的:
- 内存的存储空间比硬盘小很多,不过随着硬件的发展,大内存的服务器越来越便宜了,市面上甚至可以看到 1TB 级别的内存的机器了。
- 内存的数据掉电后会丢失,因此 Redis 引入持久化的方式,把数据在硬盘上也备份一遍(当然,增删改查仍然是以内存为主),速度不影响的前提下,保证重启后数据不丢失。
十九、Redis 的主从同步 / 主从复制是怎么回事
解决的问题:
- 提高 Redis 的可用性,避免 Redis 机器 / 进程挂了之后无法提供服务。
- 降低 Redis 服务器的压力,从节点读,主节点写,让单个节点承担的压力更小。
需要有多个服务器,部署多个 Redis 服务器程序。其中⼀个作为主节点,其他作为从节点。在从节点 Redis 启动的时候,通过配置文件或者命令参数 --slaveof 指定当前节点的主节点是哪个。
当从节点启动之后,就会清空自身数据,并把主节点的所有数据都复制过来,并且主节点的数据后续如果进行修改,从节点也能自动随之更新。主节点可以写数据也可以读数据,从节点只能读数据。这样的话,主节点的数据就多了几个 “备份”,当主节点挂了仍然可以通过从节点来读取数据。
另一方面访问 Redis 大多还是读操作,通过从节点就可以分担主节点的读操作的压力。
比如一个 Redis 每单位时间要处理 100 次写请求和 10w 次读请求。
如果使用主从的方式部署,比如一个主节点,三个从节点,此时主节点负责处理 100 次写请求,主节点和从节点再负责处理 2.5w 次读请求,这样主节点的压力就降低到了原来的 1/4 了。
二十、Redis 的 pipeline(流水线 / 管道)是什么
在 Redis 客户端执行 N 个命令,大概的过程如下:
发送命令−> 命令排队−> 命令执⾏−> 返回结果
发送命令−> 命令排队−> 命令执⾏−> 返回结果
发送命令−> 命令排队−> 命令执⾏−> 返回结果
发送命令−> 命令排队−> 命令执⾏−> 返回结果
......
这个过程称为 Round trip time(RTT,往返时间)。
如果我们需要执行多个操作,就需要多个这样的 RTT,其中大量的时间都消耗在发送命令 / 返回结果的网络时间上了。
使用 pipeline 就可以把多个 redis 命令合并到一个请求中,节省网络开销。
发送命令−> 命令排队−> 命令排队−> 命令排队−> 命令执⾏ -> 命令执⾏−> 命令执⾏−> 返回结果
注意 :pipeline 中的这 N 个命令的执行并非是原子的,他们只是同乘⼀趟地铁的陌路人,彼此之间没什么关联关系。
通过 Redis 客户端使用管道,可以基于 Linux 的管道实现。比如把要执行的 N 个命令写入 cmd.txt 文本文件中,每个命令一行。
然后执行:
cat cmd.txt | redis-cli --pipe
通过 pipe 参数即可让 redis-cli 读取执行 cmd.txt 中的内容。如果通过管道执行的命令太多,可能会使服务器被阻塞住。
二十一、介绍一下 Redis 哨兵
参考:【Redis 进阶】哨兵 Sentinel(重点理解流程和原理)_redis sentinel原理-CSDN博客
二十一、Redis 哨兵如果发现主节点宕机了,接下来会做哪些事情
参考:【Redis 进阶】哨兵 Sentinel(重点理解流程和原理)_redis sentinel原理-CSDN博客
二十二、Redis 的集群是干什么的
参考:【Redis 进阶】集群(重点理解流程和原理)-CSDN博客
二十三、Redis 的哈希槽是怎么回事
参考:【Redis 进阶】集群(重点理解流程和原理)-CSDN博客
二十四、Redis 集群的最大节点个数是多少
Redis 作者建议不要超过 1000 个。
参考:why redis-cluster use 16384 slots? · 议题 #2576 · redis/redis (github.com)
二十五、Redis 集群会有些操作丢失吗
Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。比如在写成功一个 key 之后,正好主节点宕机,此时由于这个数据还没有来得及同步到从节点上,也没来得及 AOF 写入日志,就丢失了。
二十六、Redis 集群如何选择数据库
Redis 集群不支持选择数据库,只能使用默认的数据库 0。
二十七、介绍下一致性哈希算法
参考:【Redis 进阶】集群(重点理解流程和原理)-CSDN博客
二十八、如何理解 Redis 的事务和 MySQL 的事务有什么区别
二十九、Redis 和事务相关的命令有哪些
MULTI、EXEC、DISCARD、WATCH
三十、为什么在生产环境上不应该使用 keys * 命令
keys * 类似于 sql 的 select *,如果存储的数据量特别大,那么这个操作就会耗时非常长,甚至阻塞住 Redis,使 Redis 难以处理其他请求,造成生产环境故障。
三十一、如何使用 Redis 作为消息队列
有三种典型的做法:
- 直接使用 List。Redis 中提供了 BLPOP 和 BRPOP,可以阻塞式的获取元素,就可以让消费者通过 List 来进行阻塞式取元素,生产者通过 RPUSH 或者 LPUSH 即可完成生产操作。
- 使用 PUB / SUB 命令。
- 使用 Redis 5 提供的 Stream 类型。
整体来说,Stream 这个方案提供的功能相对更加完整,但是即使如此,更多的时候还是使用专业的消息队列更合适一些。如果确实某个场景对于 MQ 的功能需求不高,且不想引入额外的 mq 组件依赖,Redis 也可以使用。
三十二、如何使用 Redis 实现分布式锁
参考:【Redis 进阶】Redis 典型应用 —— 分布式锁-CSDN博客
三十三、什么是缓存穿透、缓存雪崩、缓存击穿
参考:【Redis 进阶】Redis 典型应用 —— 缓存(cache)-CSDN博客
三十四、什么是热 key 问题,如何解决
某些 key 的访问频率非常高,称为 “热 key”。有些热 key 可能达到非常热的情况,以一己之力就能把 redis 打挂。
虽然 Redis 通过集群部署的方式能够分散请求的压力。但是由于热 key 是同一个 key,对应访问的机器也是同一组机器,这就会导致其他组的机器虽然硬件资源富裕,但是也无法帮上忙。
如何解决:
- 进一步扩大 Redis 集群的规模,尤其是针对热 key 所属分片,部署更多的 slave 节点分担读压力。
- 应用服务器对热 key 识别出来,并单独的进行二次哈希,也就相当于是把⼀个 key 分散到多个 Redis 分片上存储。
- 应用服务器对热 key 识别出来,把热 key 使用单独的 Redis 集群部署,并赋予更多的机器。
- 使用应用程序本地缓存,降低 Redis 的压力。
三十五、如何实现 Redis 高可用
主从 + 哨兵 + 集群。
三十六、Redis 和 MySQL 如何保证双写一致性
什么是 “双写⼀致性”?
当用户修改数据的时候,需要修改数据库,同时也要更新缓存中的数据。
否则再直接读缓存的数据就是 “脏数据” 了。但是如果直接写数据库并且写 Redis,此时万一有一方写入失败,就容易出现数据不一致的情况。
如何解决?
方案一:延时双删。
- 先删除缓存数据。
- 再更新数据库。
- 再次删除缓存数据。
“双删” 的目的是多一重保证。如果第一次删除失败,第二次删除仍然能够兜底。
只要把 Redis 的数据删了,后续访问该数据的时候就会自动的从数据库读取,并且把结果也写写入 Redis 了,就可以保证 Redis 和数据库一致。
如果直接修改 Redis,由于修改 Redis 和修改 MySQL 并非是原子的,多个应用服务器并发执行的时候,可能就会出现类似于 “线程安全” 的问题了。
服务器 1 和服务器 2 同时都写 Redis 和 MySQL,其中服务器 1 写 Redis 的结果被服务器 2 写 Redis 的结果覆盖了,服务器 2 写 MySQL 的结果被服务器 1 写 MySQL 的结果覆盖了,此时就不一致了。因此,“数据一致” 问题就转换成了能成功删除 Redis 数据的问题。
是否可能第二次删除也失败了呢?
答案是肯定的,但是概率是大大降低了。
方案二:删除缓存重试。
先删除缓存数据,如果删除失败,则把失败的 key 放到一个 mq 中,稍后进行重试。只要删除不成功,这个重试就会反复一直进行。
上述操作确实能更严格的保证删除效果,但是代码实现起来比较复杂。幸运的是,有大佬把这个过程封装好了,我们可以直接使用。例如,阿里巴巴提供了开源工具 canal,可以比较方便的获取到 mysql 的 binlog,并基于此实现上述逻辑。
三十七、生成 RDB 期间,Redis 是否可以处理写请求
- save。这个操作会阻塞 Redis 的主线程,此时无法处理外界的写请求。
- bgsave。这个操作会让 Redis 生成子进程,子进程负责生成 RDB,父进程负责处理写请求。
此时新写的数据不一定会被写入 RDB 文件中,需要触发下一轮 RDB 的时候才能真正确保持久化了。
三十八、Redis 的常用管理命令有哪些
# dbsize 返回当前数据库 key 的数量
# info 返回当前 redis 服务器状态和⼀些统计信息
# monitor 实时监听并返回redis服务器接收到的所有请求信息
# shutdown 把数据同步保存到磁盘上, 并关闭redis服务
# config get parameter 获取⼀个 redis 配置参数信息(个别参数可能⽆法获取)
# config set parameter value 设置⼀个 redis 配置参数信息(个别参数可能⽆法获取)
# debug object key 获取⼀个 key 的调试信息
# debug segfault 制造⼀次服务器当机
# flushdb 删除当前数据库中所有 key, 此⽅法不会失败, ⼩⼼慎⽤
# flushall 删除全部数据库中所有 key, 此⽅法不会失败, ⼩⼼慎⽤
三十九、Redis 用到的网络通讯协议是怎样的
Redis Serialization Protocol(RSP),这个是 Redis 专门实现的应用层协议,用于 Redis 客户端和服务器之间的通信。
RSP 是一种纯文本协议,协议规则参考:Redis serialization protocol specification | Docs
四十、Redis 如何遍历 key
使用 keys * 虽然能一次性获取到所有 key,但是这个操作开销可能非常大,会把 Redis 卡死。更靠谱的方法是使用 scan 命令。
SCAN cursor [MATCH pattern] [COUNT count]
每次 scan 时间复杂度 O(1)。
每次 scan 都能返回一批 keys,同时告知我们下次应该从哪里开始进行 scan。需要使用多次 scan 才能完成整个遍历。
Cursor 表示遍历 key 的光标。从 0 开始,每次执行 scan 都会返回下次开始的光标,当返回结果为 0,则说明遍历结束。
通过 count 可以限制每次获取到的 key 的个数。
Redis 是按照 哈希 的方式来管理 keys 的,因此在遍历的时候得到的 keys 序列并非是 “有序” 的。
每次调用 scan 会返回下一次 scan 的游标和本次的 keys。下次再 scan 的时候根据这个游标继续遍历即可。同理,还提供了 hscan(哈希)、sscan(set)、zscan(zset),用于遍历,用法类似。渐进式遍历解决了阻塞问题,但是如果遍历过程中,键存在变化,则导致重复遍历或者遗漏。
四十一、Redis 如何实现 “查找附近的人”
可以使用 Geospatial 类型,存储每个人的地理位置。
geoadd key [经度] [纬度] member [经度] [纬度] member .......
使用
georadius key [经度] [纬度] [距离]
查询以给定位置为圆心,距离为半径,里面有哪些 member 符合条件。
四十二、什么是 Redis 的 "bigkey" 问题,如何解决
bigkey 指的是某个 key 对应的 value 占据较多的存储空间。比如 value 是字符串类型,是一个非常长的字符串,或者 value 是 hash 或者 set 类型,里面的元素特别多。这样的 bigkey 会导致读写的时候性能下降,如果是集群分片部署,也会引起不同分片的数据倾斜。
解决方案:核心思路就是拆分,把一个大的 key 拆成多个小的 key,每个 key 对应 value 的一部分数据。可以使用 redis-cli --bigkey 查找 bigkey。删除 bigkey 不要直接使用 del,也可能阻塞 Redis,使用 unlink 命令在后台删除更合适。