Redis查漏补缺
本文章只是对本人在Redis中八股的不足进行查漏补缺,搭建属于我的知识体系,使用的是适用于本人的语言逻辑,不建议作为八股学习🤫。
1. Redis为什么这么快
- Redis是纯内存操作的,读写都发生在内存当中,处理速度是纳秒级别的
- Redis的数据接口采用了多种优化结构
- Redis执行命令是单线程的,可以避免线程切换和锁带来的性能问题
- Redis虽然执行命令是单线程的 但是采用了IO多路复用技术,可以单线程同时处理多个读写
- Redis采用自己设计的通讯协议RESP,这个协议是使客户端和服务端之间通讯的序列化和反序列化开销特别小
为什么Redis单线程反而更快
- 单线程可以避免线程切换和锁,所以采用单线程+IO多路复用 其他的看上一条
Redis 真的完全是单线程吗?
- Redis在主线程进行处理命令,读写数据,在后台线程处理RDB AOF 例如 bgsave 和 fsync
2. Redis数据类型的底层结构
String
Redis虽然是用C写的 但是Redis的String 没有用C的字符串标识, 而是自己构建的简单动态字符串SDS
SDS结构中有len字段保存字符串长度 不像C字符串需要遍历获取长度
SDS不依赖\0结束,因此不但可以存文本 还可以可以存储二进制
SDS在修改字符串时会检查空间是否足够,不会自动扩容,防止缓冲区溢出
3. Redis过期Key删除策略
过期Key删除策略有三种
- 定时删除:为每个key设置定时器,到期立刻删除,会占用CPU
- 惰性删除:只有访问key时才检查是否过期,如果过期就删除,对CPU友好,但浪费内存
- 定期删除:每隔一段时间随机抽查一部分设置了TTL的key,如果发现过期就删除
Redis采用的是 惰性删除+定期删除
这样就可以兼顾CPU和内存效率
4. Redis内存淘汰策略
Redis有三类内存淘汰策略
第一种是 noeviction策略 ,内存满直接报错
第二种是 volatile策略 ,只从设置过期时间的key中淘汰
第三种是 allkeys策略 , 从所有key中淘汰
还有就是默认内存淘汰策略:no-eviction拒绝:禁止淘汰数据,内存不足会拒绝读写
5. Redis在你项目中的用途
- 作为分布式Session,Java中的session只在一台服务器上运行,而redis可以让每台服务器都可以获取session/token
- 分布式锁,我这个项目使用的是Redisson的分布式锁,Redison分布式锁的底层是基于Redis的SETNX命令实现加锁,同时设置TTL防止死锁。在释放锁的时候,会使用Lua脚本来保证操作的原子性。防止误删。
- 同时Redisson为分布式锁添加了Watch Dog来实现锁自动续期,如果线程还未执行完,看门狗会延长该锁的TTL
- 消息队列,本项目是用RocketMQ来实现消息队列功能的,但是redis也可以使用stream数据类型来实现消息队列,他也有主题和消费组的概念,同时也支持消息持久化以及ACK机制
- 布隆过滤器,是redis的一种数据类型,本项目使用布隆过滤器来确保用户名和短链接不重复,通常是加唯一索引,是分布式锁+查数据库实现不重复,这里采用性能更高的布隆过滤器来实现不重复的功能
- 其他,比如使用Bitmap来实现统计签到,通过Sorted Set来维护排行榜,通过 HyperLogLog 统计网站 UV 和 PV
6. 布隆过滤器怎么实现的
布隆过滤器是redis中的数据类型,判断一个元素是否在一个集合中
他包含一个位数组和一组哈希函数,位数组的初始值为0,插入一个元素,将该元素经过哈希函数映射到位数组多个位置上,并将这些位置的值设置为1。
查询一个元素是否存在,将该元素对应的位数组位置是否都为1,如果有任意位置为0,则元素不存在。
所以不存在的元素可能会被布隆过滤器误判存在,而存在的元素不可能被误判不存在。
因此我们该项目采用布隆过滤器来检测用户名,短链接可以忍受误判,而大大提高性能。
7. 你的项目如何保证双写一致性的
- 我们这个项目采用的是旁路缓存模式(Cache Aside Pattern) , 写操作完全绕过缓存,直接操作数据库
- 在写操作时 先操作数据库,再删除缓存。
- 在读操作时 先读取缓存,若命中直接返回,没命中从DB读数据,写缓存,然后返回。
为什么先操作数据库,再删除缓存,能反过来吗
- 不能,如果返回来,再我们删除缓存后要操作数据库时,另一个线程未能读取到缓存,而从DB读旧值,将旧值写入缓存,从而缓存与数据库不一致。
先操作数据库,再删缓存一定安全吗
- 并不是绝对安全,有可能一个线程在操作数据库->写缓存期间有其他请求完成了数据库更新,产生不一致,但是这个窗口时间较短,发生概率不高。
8. Redis持久化策略
RDB
RDB快照 : 通过创建一个二进制的快照RDB对数据进行持久化。
Redis是单线程的,RDB创建快照时会阻塞吗
- 答案是不会的,因为Redis只是单线程的执行命令,而它可以创建出子进程。
save在主线程创建快照。 会造成主线程阻塞bgsavefork出一个子进程,子进程创建快照。 不会阻塞
- 即使是子进程去创建RDB快照,也是非常耗时的 所以可以延长备份时间
COW(Copy on Write)写入式复制思想是什么:
子进程要读数据保存到RDB文件时,主线程要修改这块数据
那么就会把这个数据复制一份,主线程修改备份,子线程去保存快照
RDB优点 : 使用的二进制,恢复速度快
RDB缺点 : Redis宕机或重启后会丢失创建最近RDB之后的数据,
AOF
AOF : 每执行一条命令,redis将其写入AOF缓存区,再写入AOF文件中,最后根据持久化方式的配置决定何时刷入磁盘。
AOF 工作流程是怎么样的
- 首先是命令追加(append),所有的写命令都会追加到AOF缓存区中
- 然后文件写入(write),将缓存区的数据写入AOF文件,这一步需要调用
write函数,此时还没刷入磁盘 - 文件同步(fsync),也就是持久化的核心,根据conf文件中的配置策略,Redis会在不同时机调用
fsync函数,使数据刷入磁盘,fsync会阻塞直到写入磁盘后返回 - 文件重写(rewrite),如果AOF文件过大,但是存入了一大堆无效命令(比如创建又删除),Redis会单独开一个子线程对其AOF文件进行重写压缩。
- 重启加载(load),Redis重启后,可加载AOF文件进行数据恢复。
AOF 持久化方式有哪些
- Redis的conf文件中存在三种不同的AOF持久化方式(fsync策略)
appendfsync always: 数据写入AOF文件之后,会立刻执行fsync函数将AOF文件里的数据刷入磁盘,主线程会阻塞,直到完全刷入磁盘才会返回。- 理论上最安全的持久化方式,不会存在任何数据丢失,但是每个写操作都会阻塞主线程,性能极差。
appendfsync no: 数据写入AOF文件之后,让操作系统决定何时同步,Linux一般是30s一次,将AOF文件里的数据刷入磁盘- 理论性能最好,但是会丢失大量数据
appendfsync everysec: 数据写入AOF文件之后,后台线程(aof_fsync线程)每秒调用fsync函数将AOF文件里的数据刷入磁盘。- 性能基本不受影响,不过通常Redis宕机会丢失1秒内数据
AOF为什么执行命令后记录日志
- MySQL等关系型数据库都是记录redo log,然后执行命令,这是因为数据库要保证事务的ACID
- 而Redis里存储的是缓存,最先要求的是他的性能和速度,所以Redis选择先执行命令,然后记录日志,不会对命令进行语法检查。
9. Redis 集群
redis cluster 使用Hash Slot分片机制,整个集群有16384个 slot ,key通过slot找到节点
redis cluster 有两种节点 master 和 slave 如果master挂了 slave会自动升级为master
redis 为什么使用 16384个 slot : 因为slot数越大,通信数据就越大,使用bitmap存储slot信息 16384个slot只有2kb
Redis的故障转移
- master挂掉之后slave会检测到 然后slave会发起选举 让选举的slave成为master
如何知道数据在哪一个节点
- Cluster通过对key进行CRC16 哈希计算,然后对16384取模得到slot,根据slot与节点的映射关系确定数据所在节点
具体流程就是key -> hash -> slot -> node
举个例子- 找到key: 假设 key = “user:1001”
- CRC16计算hash槽: CRC16(“user:1001”) = 12345(假设)
- hash取模计算slot: 12345 % 16384 = 12345
- 根据slot找出节点: slot 12345 -> 节点C
10. Redis 扩容
- Redis扩容分为两种
- 单机数据结构扩容(dict/SDS等)
- 集群扩容(增加节点 + slot移动)
数据结构扩容
最重要的是哈希表(dict)扩容
我们通常不使用一次迁移所有数据进行扩容,数据量大的话。一次迁移的时间复杂度是On
Redis使用渐进式Rehash进行哈希表的扩容,可以边处理请求,边搬数据,通过分批迁移数据,避免一次性重建哈希表带来的性能阻塞。首先我们需要知道Redis的哈希表结构
1
2
3
4dict
├── ht[0](旧表)
├── ht[1](新表)
├── rehashidx(迁移进度)如果负载因子过高,接近于1,会触发Redis使用渐进式Rehash扩容
- 首先创建更大的新表
ht[1],设置迁移进度为0 - 开始迁移,之后Redis每次操作(读/写/删)都会顺带把
ht[0]搬一部分数据到ht[1],然后迁移进度+1- 这时如果插入新数据,一律写入
ht[1]
- 这时如果插入新数据,一律写入
- 完成迁移,也就是
ht[0]为空之后,将迁移进度设置为-1 - 如果rehash期间执行删除操作,两个表都要查询并删除对应数据
- 首先创建更大的新表
11. 集群扩容
- 当数据太多,单机顶不住时
我们会扩容Redis Cluster,具体就是新增节点
然后给新的节点分配slot,注意Redis总共就有16384个slot
把就旧节点的数据迁移到新节点
12. Redis的IO模型
- Redis采用单线程+IO多路复用解决网络请求的
其中IO多路复用就是用一个线程同时监听多个连接
Redis的IO多路复用策略使用的是epoll
IO多路复用策略
IO多路复用,本质就是让一个线程同时监听多个连接
这里的复用不是说一个IO被使用多次,而是复用线程去监听多个IO
- 如果不用IO多路复用,每个连接都要使用一个线程,这样会造成性能降低
- 使用了IO多路复用
应用线程不用遍历所有连接
由内核同一监听,有事件了再处理
select
select是最早的IO多路复用机制- select的工作方式:
每次调用select,把所有连接传给内核
然后内核会遍历我们所有的连接,检查连接有没有就绪
假如一万个连接只有几十个活跃的,照样会按个遍历这一万个,所以性能很差
如果就绪就返回,如果没就绪则继续阻塞 - select的缺点:
- 每次调用select都会把连接集合从用户态拷贝到内核态
- 监听的fd(连接)有限,通常存到位图bitmap中,默认是1024
- select的工作方式:
poll
poll可以算是select的改进版
poll的工作方式和select相同,都是遍历然后检查是否就绪- 区别于select,poll不再用bitmap存储fd
而是用一个结构体数组,所以没有监听fd的数量限制 - 缺点也是每次调用,都要将连接集合从用户态拷贝到内核态
- 区别于select,poll不再用bitmap存储fd
epoll
具体如下
13. epoll
epoll是linux最核心的多路复用机制,改进很大
区别于select和poll:
1. 不用每次把fd(连接)传到内核
2. 不需要遍历fd集合找就绪事件
- 把”重复注册”和”无脑遍历”给优化掉了
epoll常用三个系统调用
epoll_create
创建一个epoll实例epoll_ctl
把某个fd(连接)注册/修改/删除到epoll实例里
比如:
在epoll里监听一个socket
删除某个socket
修改监听规则- 区别于select/poll:
只在fd增删改的时候传内核epoll_ctl,而不是每次调用都传内核
- 区别于select/poll:
epoll_wait
阻塞等待时间发生
一旦fd就绪,将fd存到就绪队列,内核会将就绪队列里的fd列表发给线程
这里就不需要全表扫描,而是 等到就绪的fd
epoll的优点:
- 不需要拷贝所有fd到内核态
fd注册/修改/删除会操作内核
然后就只需要epoll_wait等待结果 - 不再全表扫描遍历fd
内核维护一个就绪队列
fd就绪,内核会将其存入就绪队列
调用epoll_wait时,直接拿到就绪的fd
epoll底层:
- 红黑树
- 用于存储所有被监听的fd
使用红黑树,这样增删改的时候效率比较高
- 用于存储所有被监听的fd
- 就绪队列(ready list)
- 用来存放就绪的fd
当fd就绪,内核将其挂到就绪队列里
调用epoll_wait时,直接拿到就绪的fd
- 用来存放就绪的fd
- 标题: Redis查漏补缺
- 作者: yin_bo_
- 创建于 : 2026-03-08 14:14:16
- 更新于 : 2026-04-01 22:16:58
- 链接: https://www.blog.yinbo.xyz/2026/03/08/面试题/Redis查漏补缺/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。