Redis 常见面试题
认识 Redis
什么是 Redis?
我们直接看 Redis 官方是怎么介绍自己的。
Redis 官方的介绍原版是英文的,我翻译成了中文后截图的,所以有些文字读起来会比较拗口,没关系,我会把里面比较重要的特性抽出来讲一下。
Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
Redis 和 Memcached 有什么区别?
很多人都说用 Redis 作为缓存,但是 Memcached 也是基于内存的数据库,为什么不选择它作为缓存呢?要解答这个问题,我们就要弄清楚 Redis 和 Memcached 的区别。 Redis 与 Memcached 共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
Redis 与 Memcached 区别:
- Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
- Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
- Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;
为什么用 Redis 作为 MySQL 的缓存?
主要是因为 Redis 具备「高性能」和「高并发」两种特性。
1、Redis 具备高性能
假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。
如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题,后面我们会提到。
2、 Redis 具备高并发
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
Redis 数据结构
Redis 数据类型以及使用场景分别是什么?
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 五种数据类型的应用场景:
- String 类型:常用于共享 session 、分布式锁。
- Hash 类型:常用于缓存对象、购物车。
- List 类型:常用于消息队列。
- Set 类型:常用于集合计算,并集、差集、交集,点赞、共同关注。
- Zset 类型:常用于排行榜,排序。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
- BitMap(2.2 版新增):常用于 二值状态 统计 ,比如用户签到;
- HyperLogLog(2.8 版新增):常用于 海量数据基数 统计,比如 UV 计数;
- GEO(3.2 版新增):常用于存储 地理位置信息,比如滴滴叫车;
- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
TIP
想深入了解这 9 种数据类型,可以看这篇:2万字 + 20 张图 | 细说 Redis 常见数据类型和应用场景 (opens new window)
五种常见的 Redis 数据类型是怎么实现?
我画了一张 Redis 数据类型和底层数据结构的对应关图,左边是 Redis 3.0版本的,也就是《Redis 设计与实现》这本书讲解的版本,现在看还是有点过时了,右边是现在 Redis 7.0 版本的。
String 类型内部实现
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串):
- SDS 不仅可以保存文本数据,还可以保存二进制数据。能保存 图片、音频、视频 这样的 二进制数据。
- SDS 获取字符串长度的时间复杂度是 O(1)。
- Redis 的 SDS 的 API 是安全的,拼接字符串不会造成缓冲区溢出。
Hash 类型内部实现
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
- 如果 Hash 类型的元素个数小于 512 个,并且 每个元素的值小于 64 字节时,会使用压缩列表作为 Hash 类型的底层数据结构;
- 否则,会使用哈希表。
List 类型内部实现
List 类型的底层数据结构是由压缩列表或双向链表实现的:
- 如果 List类型的元素个数小于 512 个,并且 列表每个元素的值都小于 64 字节时,Redis 会使用压缩列表作为 List 类型的底层数据结构;
- 否则,会使用双向链表;
Set 类型内部实现
Set 类型的底层数据结构是由整数集合或哈希表实现的:
- 如果Set 类型的元素个数小于 512 个,并且 每个元素 都是整数时,会使用整数集合作为 Set 类型的底层数据结构;
- 否则,会使用哈希表。
ZSet 类型内部实现
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
- 如果 Zset 类型的元素个数小于 128 个,并且每个元素的值小于 64 字节时,会使用压缩列表底层数据结构;
- 否则,会使用跳表;
TIP
想深入了解这 9 种数据结构,可以看这篇:2万字 + 40 张图 | 细说 Redis 数据结构 (opens new window)
Redis 线程模型
Redis 是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行读写操作->把结果发送给客户端」这个过程是由一个线程(主线程)来完成的,主要是 命令的执行。
但是,Redis 程序并不是单线程的,是会启动 后台线程 的:
「关闭文件、AOF 刷盘、释放内存」这些任务 会有 后台线程 来处理。如果都放在 主线程 来 处理,这些任务 都是 很耗时的,那么 主线程 很容易发生阻塞,这样就无法处理 其他请求了。
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:
- BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
- BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
- BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
Redis 单线程 具体是怎样的?
Redis 6.0 版本之前的单线模式如下图:
图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:
- 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
- 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
- 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。
初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:
- 首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
- 接着,调用 epoll_wait 函数等待事件的到来:
- 如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
- 如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
- 如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
以上就是 Redis 单线模式的工作方式,如果你想看源码解析,可以参考这一篇:为什么单线程的 Red