面试-Redis
数据结构
1.Redis底层的数据结构
常见的有五种数据类型: String(字符串) , Hash(哈希) , List(列表) , Set(集合) , Zset(有序集合);
新增的四种数据类型: BitMap , HyperLogLog , GEO , Stream;
String类型的应用场景:缓存对象,常规计数,分布式锁,共享session信息;
List类型:消息队列; 但有问题:1.生产者需要自行实现全局唯一ID; 2.不能以消费者组的形式消费数据;
Hash类型:缓存对象,购物车等;
Set类型:聚合计算, 比如点赞,共同关注,抽奖活动等;
Zset类型:排序场景, 比如排行榜,电话和姓名的排序等;
BitMap: 二值状态计的场景,比如签到,判断用户登录状态,连续签到用户总数等;
HyperLogLog:海量数据基数统计场景,比如百万级网页UV计数;
GEO:存储地理位置信息的场景,比如滴滴叫车;
Stream: 消息队列, 相比于List,有两个特有的性质:1.可以自动生成全局唯一消息ID 2.支持以消费组形式消费数据;
2.Zset:
类似于用Zset实现排行榜功能:
1.添加排行榜数据:
1 | ZADD rank 100 user1 |
2.查看排行榜(从高到低)
1 | ZREVRANGE rank 0 -1 WITHSCORES |
3.查看(前三名)
1 | ZREVRANGE rank 0 2 WITHSCORES (左右都包) |
4.查询某个用户的排名
1 | ZREVRANK rank user1 |
5.查询某个用户的点赞数
1 | ZSCORE rank user1 |
6.查询点赞100~200的用户
1 | ZRANGEBYSCORE rank 200 100 WITHSCORES |
7.增加点赞数(如果是-1就是减少一个点赞)
1 | ZINCRBY rank 1 user1 |
8.获取排行榜总人数
1 | ZCARD rank |
2.Set和Zset的区别:
一、Set 和 ZSet 的核心区别
| 对比维度 | Set(集合) | ZSet(有序集合) |
|---|---|---|
| 是否有序 | ❌ 无序 | ✅ 按 score 有序 |
| 是否重复 | ❌ 不允许 | ❌ 不允许 |
| 排序依据 | 无 | score(分值) |
| 是否支持排名 | ❌ | ✅ |
| 底层结构 | HashTable | SkipList + Hash |
| 是否支持范围查询 | ❌ | ✅ |
| 典型场景 | 去重、关系集合 | 排行榜、积分、延迟队列 |
Set:
1️⃣ 添加元素
1 | SADD tags java redis mysql |
2️⃣ 删除元素
1 | SREM tags redis |
3️⃣ 判断是否存在
1 | SISMEMBER tags java |
4️⃣ 获取所有元素
1 | SMEMBERS tags |
5️⃣ 获取元素个数
1 | SCARD tags |
6️⃣ 随机获取元素(抽奖常用)
1 | SRANDMEMBER tags 2 |
7️⃣ 随机弹出元素
1 | SPOP tags |
8️⃣ 集合运算(面试重点 ⭐⭐⭐)
1 | SINTER set1 set2 # 交集 |
Zset:
1️⃣ 添加 / 更新元素
1 | ZADD rank 100 user1 |
2️⃣ 增减分(核心)
1 | ZINCRBY rank 10 user1 |
3️⃣ 获取排行榜(高 → 低)
1 | ZREVRANGE rank 0 9 WITHSCORES |
4️⃣ 查询某个成员排名
1 | ZREVRANK rank user1 |
5️⃣ 查询成员分数
1 | ZSCORE rank user1 |
6️⃣ 按分数范围查询
1 | ZRANGEBYSCORE rank 60 100 WITHSCORES |
7️⃣ 按排名范围查询
1 | ZRANGE rank 0 9 WITHSCORES |
8️⃣ 删除元素
1 | ZREM rank user1 |
9️⃣ 获取成员数量
1 | ZCARD rank |
🔟 清理数据(排行榜维护)
1 | ZREMRANGEBYRANK rank 100 -1 |
3.Zset底层怎么实现的:
Redis 的 ZSet 底层使用「Hash 表 + 跳表(SkipList)」实现:
Hash 用于根据 member 快速查 score,跳表用于按 score 有序排列并支持范围查询和排名。
跳表的结构(核心)<具体图解看小林>;
链表在查找时需要从到尾依次查找,时间复杂度时O(N); 跳表是在链表的基础上改过来的,实现了一种多层的有序链表;时间复杂度是O(logN)
1️⃣ 多层链表结构
1 | Level 3: 1 ---------> 9 |
- 最底层:完整数据
- 上层:抽样出来的索引
- 层数越高,节点越少
2️⃣ 节点结构(实现角度)
每个节点包含:
1 | value / score // 元素和元素的权重 |
二、层级结构是怎么来的?(核心机制)
1️⃣ 每个节点都有“高度(level)” 一个节点能存储多个高度信息;
- 节点不是只在一层
- 而是:
1 | Level 1 一定有 |
示意:
1 | 节点 A:Level 1 |
2️⃣ 随机生成层数(最关键)层高最大限制是64
插入节点时:
1 | P(节点升一层) = 1/2 或 1/4 |
示例(概率递减):
1 | 只有1 层:50% |
层数越高,节点越少
这正是“索引层”的来源。
4.跳表怎么设置层高的:
跳表在创建节点时,会生成(0~1)的随机数,如果这个数小于0.25(也就是1/4的概率),那么层数就增加一层,然后继续生成随机数,直到随机数大于0.25;
5.Redis为什么使用跳表不使用B+树:
一、跳表 vs B+ 树(核心对比)
| 对比点 | 跳表(SkipList) | B+ 树 |
|---|---|---|
| 主要使用场景 | 内存 | 磁盘 / 数据库 |
| 实现复杂度 | 简单 | 非常复杂 |
| 查询复杂度 | O(log n) | O(log n) |
| 节点结构 | 链表 + 多层索引 | 多叉树 |
| 维护成本 | 低 | 高 |
二、为什么 不使用 B+ 树
1. B+ 树是为「磁盘 IO」设计的
B+ 树的核心优势是:
- 减少磁盘 IO 次数
但 Redis:
- 数据 全在内存
- 没有磁盘随机 IO 问题
B+ 树的最大优势在 Redis 场景中用不上
2. B+ 树实现和维护成本高
B+ 树需要处理:
- 节点分裂
- 节点合并
- 复杂的父子指针关系
B+树插入和删除可能会导致节点分裂和合并;
3.写入性能与代价:
B+树插入数据导致页分裂时,需要移动大量数据或者改变树的结构;
跳表的插入和删除操作是局部的,只需要修改节点前后指针,并随机生成层高即可;
6.压缩链表的实现;
7.Redis中的listpack;
<看小林>
8.哈希表是怎么扩容的:
是通过rehash扩容的, 当数据增多触发rehash操作时:
1.给哈希表2分配一个比哈希表1大2倍的空间;
2.把哈希表1中的数据迁移到哈希表2中;
3.哈希表1中的空间被释放,在哈希表2中创建一个空白的哈希表3,为下一次rehash做准备;
rehash在数据迁移时如果数据量太大,会导致阻塞Redis,所以Redis采用的是渐进式Rehash;
步骤:
1.给哈希表2分配空间;
2.在rehash期间,每次哈希表进行 新增,删除,查找,或更新操作时, 会顺便将哈希表1中的旧数据迁移到哈希表2上;
rehash期间,元素的操作会在两张表上进行;
9.Redis中字符串是用什么存储的?为什么不用c语言中的字符串?
Redis 中的字符串使用 SDS(Simple Dynamic String)存储;
Redis 的 SDS 本质是 带元信息的动态字符串,简化结构如下:
1 | struct sdshdr { |
O(1)复杂度获取字符串
SDS 在数据结构中显式维护了字符串长度字段 len,因此获取字符串长度时不需要像 C 语言字符串那样遍历查找 \0,可以直接 O(1) 得到长度,效率更高。
SDS 是二进制安全的。
由于字符串的真实长度由 len 字段记录,而不是依赖 \0 作为结束符,因此 SDS 可以存储任意二进制数据,包括图片、序列化对象等,不会因为中间包含 \0 而被截断。
SDS 不会发生缓冲区溢出。
SDS 通过 alloc 字段记录已分配的空间大小,在进行拼接或修改操作前会先检查剩余空间是否足够,不够时自动扩容,从机制上避免了 C 字符串常见的缓冲区溢出问题。
10.Redis的Zset,在项目里具体用法是什么?
1.需要给元素排序,取排名,按分数范围筛选的场景;
常见的有:排行榜,延迟任务队列,带权重的消息队列;
延迟任务队列: 比如电商订单30分钟未支付自动取消,定时发送通知; 用任务ID作为元素,把”任务执行时间戳(创建时间 + 30 分钟)”作为分数,后台开一个线程不断用ZRANGEBYSCORE查询”分数小于当前时间戳”的任务,执行完就用ZREM删掉这样就能实现定时任务,不用复杂的调度框架;
带权重的消息队列: 给消息用分数设置权重,消费者优先处理分数高的消息,保证核心业务的优先级;
线程模型
1.Redis为什么快?
单线程Redis吞吐量就能达到10W/秒;
原因:
1.大部分操作都是在内存中完成,并且采用了高效的数据结构;
2.单线程模型避免了线程之间切换的开销;
3.采用了IO多路复用机制处理大量客户端Socket请求;
2.Redis哪些地方使用了多线程?
Redis单线程指的是(接收客户端请求->解析请求->进行数据读写操作->发送数据给客户端)这个过程是一个线程(主线程)来完成的;
但是Redis程序并不是单线程的,Redis在启动时会启动后台线程(BIO);
Redis 的核心命令执行是单线程的,但在 I/O、后台任务和部分网络处理上使用了多线程。
Redis 使用多线程的地方(重点 )
1️⃣ 后台持久化线程(一直都有)
使用场景
- RDB 持久化
- AOF 重写
- 主从复制(部分阶段)
实现方式
- 使用 fork 子进程(不是线程)
- 避免阻塞主线程
这是 多进程,不是多线程,容易被追问区分
2️⃣ 异步删除(Redis 4.0+)
使用场景
UNLINKFLUSHDB ASYNCFLUSHALL ASYNC
为什么用线程?
- 大 key 删除会阻塞主线程
- 删除涉及大量内存释放
特点
- 主线程只做逻辑删除
- 实际释放内存交给 后台线程
3️⃣ 网络 I/O 多线程(Redis 6.0+,重点)
使用场景
- Socket 读写
- 协议解析(部分)
特点
- 命令执行仍然是 单线程
- 多线程只负责:
- 读请求
- 写响应
配置项
1 | io-threads 4 |
4️⃣ 定期任务 / BIO 线程
Redis 内部有 BIO 线程池:
| BIO 类型 | 用途 |
|---|---|
| BIO_CLOSE_FILE | 关闭文件 |
| BIO_AOF_FSYNC | AOF 刷盘 |
| BIO_LAZY_FREE | 异步释放内存 |
本质都是 “慢操作下放”
3.Redis怎么实现IO多路复用?<详细过程看小林>
IO 多路复用是指一个线程同时监听多个 IO 事件,当某个连接就绪时再进行处理,而不是为每个连接创建一个线程。
Redis 基于事件驱动模型实现 IO 多路复用,通过封装的 ae 事件框架,在不同操作系统下选择最优的多路复用机制,例如在 Linux 上使用 epoll。主线程通过 epoll_wait 同时监听大量客户端 socket 的读写事件,当某个连接就绪时触发对应的回调函数,完成请求读取、命令解析、执行以及结果返回。整个过程中命令执行仍由单线程完成,从而避免了线程切换和锁竞争,同时借助 IO 多路复用在单线程下实现了高并发网络处理。
“多路”指的是多个网络连接客户端,”复用”指的是复用同一个线程;
4.Redis的网络模型
Redis 采用基于 Reactor 模式的单线程 I/O 多路复用网络模型,
使用 select / poll / epoll / kqueue 等机制,在一个线程中高效处理大量客户端连接
多线程网络 I/O
但命令执行依然单线程
I/O 多路复用 ≠ 多线程
- Redis 6.0 之前:
👉 I/O 多路复用 + 单线程- Redis 6.0 之后:
👉 I/O 多路复用 + 多线程处理网络读写
👉 命令执行仍然是单线程
Redis 使用 I/O 多路复用机制监听大量连接。
在 Redis 6.0 之前,网络 IO 和命令执行都由单线程完成;
在 6.0 之后,引入了多线程来并行处理网络读写和协议解析,
但命令执行仍然是单线程,因此 I/O 多路复用和多线程并不矛盾。
在 Redis(尤其是 6.0+)的语境里,网络 I/O 一般“包含”
网络读写 + 协议解析(以及响应序列化)
I/O 多路复用和事件分发由主线程单线程完成,Redis 6.0 以后将 socket 读写、RESP 协议解析以及响应序列化交给 I/O 线程并行处理,而命令执行仍然是单线程
事务
1.如何实现Redis原子性
核心原因:命令执行是单线程的
Redis 在任意时刻只会有一个命令在执行, 命令执行过程中不会被打断, 因此 单条命令天然是原子操作;
一般用lua脚本来保证多条命令的原子性
或者用事务(MULTI / EXEC)保证多条命令的原子性(不推荐)
1 | MULTI |
Redis 事务的原子性特点:
Redis 事务不保证回滚,
但保证 命令顺序 & 原子执行
Redis 为什么不需要锁?
因为:
1 | 单线程执行命令 |
日志
1.Redis有哪两种持久化方式?分别的优缺点是什么?
Redis 有两种持久化方式:RDB 和 AOF。
RDB 适合快速恢复和备份,AOF 提供更高的数据安全性,生产环境通常 RDB + AOF 混合使用
1. AOF(Append Only File)
是什么?
以日志方式记录每一条写命令
2. RDB(Redis DataBase)
是什么?
定期生成某一时刻 Redis 内存数据的快照,并保存到磁盘
一.AOF的实现方法?
1. 命令追加(Append)
Redis每执行完一个命令,就会把命令以追加的形式写入文件中;
2. AOF 缓冲区(AOF Buffer)
Redis 不会每条命令都立刻写磁盘,而是:
- 先写入 AOF 缓冲区(内存)
- 再统一刷盘
目的:
- 减少系统调用
- 提升性能
3. 刷盘策略(appendfsync)把AOF中的数据写入Redis
决定 AOF 数据什么时候真正落盘
| 策略 | 行为 | 安全性 | 性能 |
|---|---|---|---|
always |
每条命令都 fsync | 最高 | 最差 |
everysec(默认) |
每秒 fsync 一次 | 高 | 较好 |
no |
交给 OS | 最低 | 最好 |
everysec 是性能和安全的平衡
AOF 重写(Rewrite)机制
为什么需要重写?
- AOF 只增不减
- 文件会越来越大
二.RDB是如何实现的?
RDB 通过在某一时刻对 Redis 内存数据做快照,并由子进程将快照写入磁盘文件来实现持久化,利用 fork + 写时复制保证性能。
Redis提供了两个命令来生成RDB文件,分别是SAVE和BGSAVE,区别在于是否在主线程里执行:
SAVE:
执行流程
1 | 客户端发送 SAVE |
BGSAVE:
一次 RDB 快照的完整流程:
1 | 触发 RDB |
生成的文件通常是:dump.rdb
子进程拥有主进程内存的“逻辑副本”, 实际物理内存共享
三、RDB 的三个核心实现机制
1.fork 子进程(关键)
2. 写时复制(Copy-On-Write,COW)
3. 原子替换文件
写时复制是操作系统在 fork 后提供的一种内存隔离机制。当 Redis 主线程修改某个内存页时,操作系统会复制该页,使父进程看到新数据,而子进程仍然看到 fork 时的旧数据。子进程基于 fork 时刻的内存快照生成 RDB 文件,后续的修改不会写入本次 RDB,也不存在“备份数据写入下一次 RDB”的过程。
三、RDB 数据到底是“怎么来的”?(关键)
正确理解:读 → 序列化 → 写磁盘
在 BGSAVE 中:
1️⃣ fork 后
2️⃣ 子进程 直接读取内存中的数据
3️⃣ 把数据 序列化成 RDB 格式
4️⃣ 写入磁盘文件
📌 注意:
没有任何一步是“把内存页复制到 RDB 文件”
四、那写时复制(COW)到底干了什么?
COW 只解决一个问题:
子进程读到的内存数据,在整个快照期间保持不变
举个完整例子(强烈推荐看)
fork 时刻
1 | key = 1 |
- 父进程 & 子进程
- 共享同一块内存页
fork 之后,主线程修改数据
1 | SET key 2 |
发生的事:
- OS 发现该页被共享
- 复制该内存页
- 主进程写新页(key=2)
- 子进程继续读旧页(key=1)
📌 于是:
- RDB 里是 key=1
- 内存中最终是 key=2
五、所以:没被修改的数据去哪了?
👉 一直在原来的内存页里
- 子进程可以直接读取
- 一样会被写进 RDB 文件
📌 没有“没被复制就没进 RDB”这回事
六、用一句话打穿你的困惑
COW 决定“是否复制内存页”,
RDB 决定“是否把数据序列化写磁盘”,
两者完全不是一回事。
七、再用一个终极类比(非常重要)
复印书 📚
- 内存:书架上的书
- fork:给你一张“书架访问权限”
- COW:别人改某一页时,复印一页给他
- RDB:你坐在书架前,一页页抄到笔记本里
📌 你抄书(写 RDB)
不需要先把整本书复印一遍
RDB 的触发方式:
1️⃣ 手动触发
| 命令 | 行为 |
|---|---|
SAVE |
阻塞主线程(不推荐) |
BGSAVE |
后台 fork(常用) |
2️⃣ 自动触发(配置)
1 | save 900 1 |
两者优缺点:
Redis 持久化方式优缺点对比表(图表形式)
| 持久化方式 | 优点 | 缺点 |
|---|---|---|
| RDB (快照) | ✅ 文件体积小,二进制压缩 ✅ Redis 重启恢复速度快 ✅ 对运行时性能影响小(异步 BGSAVE) ✅ 适合备份和灾难恢复 | ❌ 数据安全性低,可能丢失最近一次快照后的数据 ❌ fork 子进程成本高,大内存场景易产生延迟抖动 ❌ 不够实时,不适合强一致性场景 |
| AOF (追加日志) | ✅ 数据安全性高(最多丢 1 秒数据) ✅ 以写命令记录,数据完整性强 ✅ 文件可读,可手工修复 ✅ 更适合在线业务 | ❌ 文件体积大,依赖 rewrite 压缩 ❌ Redis 重启恢复慢(需重放命令) ❌ 持续写磁盘,性能开销高于 RDB |
缓存淘汰和过期删除
1.过期删除策略和内存淘汰策略有什么区别?
1.内存淘汰策略是在内存满的时候,Redis会触发内存淘汰策略来淘汰一些不必要的内存资源;
2.过期删除策略是将已经过期的键值对进行删除,Redis采用的删除策略是惰性删除+定期删除;
2.介绍Redis中的内存淘汰策略:
一、Redis 为什么需要内存淘汰策略
- Redis 基于内存存储
- 内存资源有限
- 当
used_memory > maxmemory时
必须淘汰部分数据,否则写操作会失败
二、Redis 八种内存淘汰策略(重点)
Redis 的淘汰策略可以从 两条维度 来记:
- 是否设置过期时间
- 按什么规则淘汰
不进行数据淘汰的策略(1种):
1. noeviction(默认策略)
- 不淘汰任何数据
- 内存满了之后:
- 写操作(SET / LPUSH 等)直接返回错误
- 读操作正常
📌 适用场景:
- 数据绝对不能丢(如金融核心数据)
进行数据淘汰的策略:
二、基于「过期时间」的淘汰策略
2️⃣ volatile-ttl
- 只淘汰 设置了过期时间的 Key
- 优先淘汰 剩余 TTL 最短 的 Key
场景:缓存数据都设置了过期时间
3️⃣ volatile-random
- 从 设置了过期时间的 Key 中
- 随机淘汰
场景:对命中率要求不高
4️⃣ volatile-lru ⭐
- 从 设置了过期时间的 Key 中
- 淘汰 最近最少使用(LRU) 的 Key
场景:
- 典型缓存系统(常用)
5️⃣ volatile-lfu ⭐
- 从 设置了过期时间的 Key 中
- 淘汰 使用频率最低(LFU) 的 Key
场景:
- 热点数据明显、访问频率差异大
三、基于「所有 Key」的淘汰策略
6️⃣ allkeys-random
- 从 所有 Key 中
- 随机淘汰
场景:
- 数据价值相近
7️⃣ allkeys-lru ⭐⭐⭐(最常用)
- 从 所有 Key 中
- 淘汰 最近最少使用(LRU) 的 Key
场景:
- 通用缓存场景(面试最推荐)
8️⃣ allkeys-lfu ⭐⭐⭐
- 从 所有 Key 中
- 淘汰 使用频率最低(LFU) 的 Key
场景:
- 高并发、热点 Key 非常明显
3.介绍Redis中的过期删除策略:
Redis的过期删除策略是选择惰性删除+定期删除两者的配合使用;
Redis惰性删除策略是由db.c文件中的expireNeeded函数实现的,Redis在访问或者修改key之前,都会调用expireNeeded函数来检查,检查key是否过期;过期则删除,不过期则正常返回键值对给客户端;
4.Redis的缓存失效会不会立即删除?
不会,Redis的过期删除策略是选择惰性删除+定期删除两者的配合使用;
惰性删除的做法是,不主动删除过期键,每次从数据库访问key时,都会检测key是否过期,如果过期即删除;
定期删除是,每隔一段时间随机从数据库中抽取一定数量的key进行检查,并删除其中的过期key;
5.不过期立即删除的原因:
在key过多时,删除过期key可能会占用相当一部分cpu时间,在cpu时间紧张的情况下,会对服务器的响应时间和吞吐量造成影响;
集群
1.Redis主从同步中的增量和完全同步怎么实现?
redis中从结构只有一个主节点,可以有多个从节点;
从节点需要执行slaveof命令来确定主节点;
从节点连接主节点的第一次需要做全量同步;
主从节点数据同步分为全量同步和增量同步;
全量同步有三个阶段:
1.从节点发送自己的replid 和 offset 给主节点来和主节点的 replid 来匹配,匹配成功说明不是第一次连接,不需要做全量同步只需要做增量同步,否则需要做全量同步; 如果是全量同步,主节点会返回自己的replid 和 offset给从节点,让从节点继承自己的replid;
2.主节点执行bgsave生成RDB操作,并且将未来得及存储在RDB中的命令存储到缓冲区repl_baklog中, 然后将RDB传给从节点,从节点执行brush指令,清空自身然后执行RDB文件;
3.主节点将缓存在repl_baklog中的数据发送给从节点,从节点执行剩下的命令实现数据同步;
增量同步两个阶段:
1.和全量同步第一阶段一样,replid匹配说明是做增量同步;
2.主节点将缓存在repl_baklog中的数据发送给从节点,从节点执行剩下的命令实现数据同步;
repl_backlog相当于一个环形数组,可以覆盖原先传输给从节点的数据;但是如果从节点宕机,repl_backlog就会发生错误,覆盖未传输的数据,就要等从节点修复好执行全量同步;
执行全量同步的两种情况:
1.第一次连接
2.从节点宕机导致repl_backlog覆盖未传输的数据;
修正:
第一阶段从节点还会和prior_replid匹配,
如果 replid = 主节点当前 replid 且 offset 合法 → 执行增量同步
如果 replid = 主节点的 prior_replid (历史id)也可以尝试执行增量同步(PSYNC 2 特性)
否则才执行全量同步
第一次连接不一定增量同步,
如果主节点重启过,产生新的replid,
从节点有主节点的 旧 replid(主节点 prior_replid == 从节点 replid)
offset 仍然在 backlog 范围内
仍然可以增量同步!
全量同步流程(完整步骤)
① 从节点发送 PSYNC,请求开始同步
② 主节点返回 FULLRESYNC
1 | FULLRESYNC <replid> <offset> |
主节点:
- 生成新的 replication id
- 告知当前复制起点 offset
③ 主节点生成 RDB 快照
- fork 子进程
- 生成 RDB 文件(内存快照)
fork 期间:
- 主线程仍可处理请求
- 新写入命令会写入 replication buffer
④ 发送 RDB 给从节点
- 从节点接收 RDB
- 清空旧数据
- 加载新数据
⑤ 发送增量命令
- 主节点把:
- RDB 期间缓存的写命令
- 加载完成后新的写命令
- 发送给从节点
到此主从数据 完全一致
🔹 增量同步流程
① 从节点重连后发送 PSYNC
1 | PSYNC <replid> <offset> |
② 主节点判断
- replid 是否一致
- offset 是否仍在 repl_backlog_buffer 范围内
③ 返回 CONTINUE
1 | +CONTINUE |
④ 主节点发送缺失的命令
- 从 offset 之后
- 从 repl_backlog_buffer 中补发
无需 RDB,效率极高
2.Redis主从集群可以保证数据一致性吗?
Redis主从和集群在CAP理论都属于AP模型,即在面临网络分区时选择保证可用性和分区容忍性,而牺牲强一致性;这意味着在网络分区的情况下,Redis主从复制和集群可以继续提供服务并保持可用,但可能出现部分节点之间的数据不一致;
3.哨兵机制的原理是什么?
一、什么是 Redis 哨兵机制
Redis Sentinel 是一套 分布式高可用(HA)解决方案,主要作用:
- 监控(Monitoring)
监控 Master 和 Slave 是否正常运行 - 故障发现(Failure Detection)
判断 Master 是否下线 - 自动故障转移(Failover)
Master 挂了,自动选举新的 Master - 通知(Notification)
将变更通知客户端或运维系统
4.哨兵机制的选主节点的算法介绍一下?
当Redis集群的主节点故障时,Sentinel集群将从剩下的从节点中选举一个新的主节点,有以下步骤:
1.故障节点主观下线;
2.故障节点客观下线;
3.Sentinel集群选举Leader;
4.Sentinel Leader决定新主节点;
1. 故障节点主观下线(SDOWN)
发生对象:
单个 Sentinel 对 Master 的判断
判定方式:
- Sentinel 会周期性向 Master 发送
PING - 在配置的
down-after-milliseconds时间内:- 未收到有效回复
2. 故障节点客观下线(ODOWN)
发生对象:
Sentinel 集群的共识判断
判定流程:
Sentinel 发现 Master SDOWN
向其他 Sentinel 发送:
1
is-master-down-by-addr
其他 Sentinel 返回对故障节点的判断
如果:
- 同意下线的 Sentinel 数量 ≥
quorum
- 同意下线的 Sentinel 数量 ≥
结果:
- Master 被标记为 客观下线(ODOWN)
- 如果是从节点操作到此为止,如果是主节点,则需要从从节点中选举一个节点升级为主节点;
3.Sentinel 集群选举 Leader
如果需要从Redis集群中选一个节点作为主节点,首先需要从Sentinel集群中选举一个Sentinel节点作为Leader;每一个Sentinel节点都能成为Leader,当主节点被判定为客观下线(ODOWN)后,所有 Sentinel 会发起投票选举过程,每个 Sentinel 在一次故障转移中只能投出一票,获得超过半数 Sentinel 投票的节点将被选为 Leader Sentinel。
4.Sentinel Leader决定新主节点
Sentinel Leader 负责后续的故障转移流程,包括从存活的从节点中选择新的主节点并完成主从切换,从而避免多个 Sentinel 同时执行切换操作导致冲突:
1.过滤故障节点;
2.选择优先级slave-priority最大的从节点作为主节点,如果不存在则继续;
3.选择复制偏移量(数据写入量的字节,记录了多少数据;主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续;
4.选择runid(Redis每次启动的时候随机生成runid作为Redis的标识)最小的从节点作为主节点;
5.Redis 分片集群模式(Redis Cluster)及优缺点;
一、什么是 Redis 分片集群模式(Redis Cluster)
Redis Cluster 是 Redis 提供的 去中心化分布式集群方案,通过**数据分片(Sharding)**将数据分布在多个主节点上,实现 高可用 + 高扩展性。
核心机制:
- 将整个键空间划分为 16384 个 Hash Slot(哈希槽)
- 每个 Master 负责一部分 Slot(分配槽有: 平均分配和手动分配两种)
- 每个 Master 可配置多个 Slave
- 客户端可直接与任意节点通信(无代理)
二、Redis Cluster 的工作原理(简述)
- 客户端对 Key 做 CRC16 运算
- 计算
CRC16(key) % 16384得到 Slot - 根据 Slot 定位到对应的 Master
- 若访问错误节点,返回
MOVED / ASK重定向
Redis Cluster 主要优缺点
| 优点 | 缺点 |
|---|---|
| 支持数据分片,单节点压力小 | 架构复杂,运维成本高 |
| 支持水平扩展,可动态扩容 | 跨 Slot 的多 Key 操作不支持 |
| 多主写入,性能高 | 不支持强一致性,可能丢数据 |
| 主从 + 自动故障转移,高可用 | 事务 / Lua 脚本受 Slot 限制 |
6.Cluster集群客户端是怎么知道该访问哪个分片的?
Redis Cluster 中,客户端通过 Hash Slot 计算 确定 Key 属于哪个分片,如果访问了错误节点,集群会通过 MOVED / ASK 重定向 指引客户端访问正确的分片。
场景
1.为什么用Redis?
Redis具有高性能和高并发两种特性;
Redis数据存储在内存中的,查询的速度非常快;底层数据结构也是高效的;
单台设备的Redis的QPS(Query Per Second)是MySql的10倍,Redis单机的QPS可以轻松破10W;
Redis很适于作数据库的缓存;
2.为什么Redis比MySql要快?
1.内存存储: Redis 的数据主要存储在内存中,内存访问是纳秒级,MySQL 的数据主要存储在磁盘中,即使有 Buffer Pool,本质仍依赖磁盘 IO,随机磁盘 IO 相比内存 IO 慢几个数量级
2.简单数据结构: Redis 使用为内存场景优化的 Key-Value 数据结构(如哈希表、跳表),读写路径短,常见操作复杂度为 O(1) 或 O(log N);而 MySQL 主要基于 B+ 树索引,需要经历索引查找、可能的回表以及行记录解析,执行链路更长,因此在简单读写场景下性能明显不如 Redis。
3.线程模型:
Redis:
- 命令执行是 单线程
- 通过 IO 多路复用(epoll) 处理高并发连接
- 无锁竞争、无上下文切换
MySQL:
多线程模型
存在:
- 锁竞争
- 上下文切换
- 事务调度
3.本地缓存和Redis缓存的区别
本地缓存是进程级内存缓存,访问速度最快但无法共享;Redis 是分布式缓存,支持多实例共享和高可用,但需要网络通信。
**本地缓存: **本地缓存是将数据存储在应用进程的内存中,通过本地方法直接访问,不涉及网络通信,因此访问速度最快;但每个应用实例都有独立的一份缓存数据,无法在多实例之间共享,数据随着进程的重启而失效,一致性和扩展性较差,通常适用于热点数据或一致性要求不高的场景。
分布式缓存(Redis): 分布式缓存以 Redis 为代表,将数据存储在独立的缓存服务内存中,通过网络提供统一访问,支持多应用实例共享数据,并具备过期机制、持久化和高可用能力;虽然相比本地缓存存在一定的网络开销,但在分布式系统中能够更好地保证数据一致性和系统扩展性,适合高并发访问场景。
4.Redis的应用场景是什么?
Redis是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于 缓存,消息队列,分布式锁;
Redis 实现分布式锁的基本命令
1 | SET lock_key unique_value NX EX 30 |
参数含义
| 参数 | 作用 |
|---|---|
| NX | key 不存在才设置(保证互斥) |
| EX 30 | 30 秒过期(防死锁) |
| unique_value | 唯一标识(UUID / 线程ID) |
为什么不用 SETNX + EXPIRE?
- 因为不是原子操作
- 可能
SETNX成功后进程崩溃,EXPIRE没执行 → 死锁
正确的解锁方式(Lua 脚本)
原子校验 + 删除
1 | if redis.call("get", KEYS[1]) == ARGV[1] then |
Lua 脚本在 Redis 中是原子执行的
Redis 是怎么实现消息队列的?
Redis 并不是专业 MQ,但可以当轻量 MQ 用。
1️⃣ 基于 List 实现(最早、最简单)
核心命令
1 | LPUSH queue msg |
2️⃣ 基于 Pub/Sub(发布订阅)
命令
1 | SUBSCRIBE channel |
| 优点 | 缺点 |
|---|---|
| 实时性强 | 消息不持久化 |
| 广播模式 | 订阅者不在线就丢 |
3️⃣ 基于 Stream(重点,面试推荐)
Redis 5.0 引入:真正的消息队列
核心命令
1 | XADD stream * field value |
5.Redis支持并发操作吗?
支持并发访问,但不支持并发执行命令。
- ✅ 多个客户端 可以同时连接、同时发请求
- ❌ 同一时刻 只有一个命令在执行
单个Redis命令具有原子性,多个Redis命令通过事务保证原子性;
6.Redis分布式锁的实现原理?什么场景下用到分布式锁?
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用;
Redis 分布式锁通过 SET lock_key unique_value NX EX 10 实现,利用 NX 保证互斥,EX 10防止死锁,解锁时使用 Lua 脚本校验 value 原子删除。
适用于多实例部署下的共享资源控制,如秒杀、分布式定时任务和防止重复操作。但在主从切换和网络分区场景下存在一致性风险,不适合强一致性业务。
lock_key就是key键;
unique_value是客户端生成的唯一标识,区分来自不同客户端的锁操作;
NX代表只有在lock_key不存在时,才对lock_key进行设置操作;
EX 10表示设置lock_key的过期时间为10s,避免客户端发生异常而无法释放锁;
7.Redis的大key问题是什么?
一、大 Key:
Key 对应的 value 占用内存过大,或包含的元素数量过多,即使 key 本身很短。不是 key 名字长,而是 value 大
一般认为字符串类型的String ≥ 1MB:几乎一定是大 Key;或者集合类型的元素数量超过1万,也算是大key;
二、大 Key 会带来哪些问题?(重点)
1️⃣ 阻塞 Redis 主线程(最严重)
Redis 单线程执行命令, 一条命令执行几百 ms ~ 几秒, 整个 Redis 无法响应其他请求
2️⃣ 网络 IO 压力巨大
3️⃣ 内存碎片 & 内存不可控
- 一次性申请 / 释放大块内存,容易触发内存抖动
4️⃣ 影响主从复制 & 集群稳定性
- 大 Key 同步慢
- 复制延迟
- 集群节点负载不均
5️⃣ 过期 / 删除风险极高
1 | DEL big_key |
- 同步删除
- 直接卡住 Redis
这是线上事故高发点
三、怎么判断 Redis 是否存在大 Key?(必会)
1️⃣ 官方工具(推荐)
1 | redis-cli --bigkeys |
- 扫描所有 key
- 输出各类型最大 key
⚠️ 线上慎用(会扫全库)
2️⃣ 采样方式(生产推荐)
1 | SCAN cursor COUNT 100 |
配合:
STRLENHLENLLENSCARDZCARD
String
1 | STRLEN key |
Hash
1 | HLEN key |
List
1 | LLEN key |
Set
1 | SCARD key |
ZSet
1 | ZCARD key |
SCAN 是渐进式,不阻塞
3️⃣ 监控慢查询
1 | SLOWLOG get |
发现:
DELHGETALLLRANGE
四、大 Key 如何解决?(核心)
1️⃣ 拆分 Key(最根本)
❌ 错误
1 | user:1001 → 一个 Hash 存全部数据 |
✅ 正确
1 | user:1001:base |
2️⃣ 大 Key 删除 —— 用 UNLINK(必背)
❌
1 | DEL big_key |
✅
1 | UNLINK big_key |
📌 UNLINK 异步删除,不阻塞主线程
3️⃣ List / Hash 拆分存储
1 | list:page:1 |
4️⃣ 控制元素数量
- List 限制长度
- Hash 限制 field 数
- 定期清理
8.什么是热key?
一、什么是热 Key?
热 Key:
在短时间内被大量并发访问的 Key,即使它本身很小,也会对 Redis 造成巨大压力。
1.QPS集中在特定的key: Redis实例的总QPS为10,000,而其中一个key的每秒访问量达到了7000;
2.带宽使用率集中在特定的key: 对一个拥有上千个成员且总大小为1MB的HASH Key每秒发送大量的HGETALL操作请求;
3.CPU使用时间占比集中在特定的key: 对一个拥有数万个成员的Key(Zset类型),发送大量的ZRANGE操作请求;
9.怎么解决热key问题?
利用本地缓存 (Local Cache):
将识别出的热点数据同步到应用服务器的内存中(使用 Guava Cache 或 Caffeine)。请求先访问本地内存,不访问 Redis,从而彻底分流压力。Key 分散(加随机后缀):
将热点 Key 复制为多个副本并散落在不同的分片(Slot)上。- 例如:将
hot_key存储为hot_key_1,hot_key_2,hot_key_n。 - 读取时:客户端随机选择一个后缀进行请求,将单点压力平摊到整个集群。
- 例如:将
读写分离:
对于读多写少的热点,增加从节点(Slave)数量,通过负载均衡将读请求分散到多个从节点。多级缓存(本地 + Redis)
1
2
3L1:本地缓存
L2:Redis
L3:DB- 热点请求直接命中 L1
- Redis 压力大幅降低
限流 + 降级(兜底方案)
场景
- 突发流量
- 秒杀、活动
手段
- 接口限流
- 返回兜底数据
- 服务降级
10.如何保证Redis和MySql数据缓存一致性问题?
Redis 是缓存,MySQL 是数据源,无法做到强一致,只能保证最终一致。
1️⃣ 读流程(读缓存)
1 | 读请求 |
2️⃣ 写流程(关键)
✅ 正确顺序(必背)
1 | 更新 MySQL |
如果先删缓存,会写回旧数据
删缓存失败怎么办?(面试加分点)
方案 1️⃣:重试 + 延迟删除(常用)(延迟双删)
1 | 更新 DB |
📌 防止并发读回写旧值
方案 2️⃣:异步消息补偿(进阶)
- 删除失败 → 发 MQ
- 消费者重试删除
11.缓存雪崩,击穿,穿透是什么?怎么解决?
1.什么是缓存穿透?
缓存穿透 是指查询一个在数据库和缓存中都不存在的数据。由于缓存无法命中(Miss),这个请求会直接穿透缓存,直达数据库。如果有大量这样的请求同时发生,数据库将不堪重负,甚至可能被压垮。
核心问题: 查询的是一个系统内根本不存在的数据。
解决方法:
方案一:缓存空对象(Null Object Caching / 缓存默认值)
这是最常用、最直接的解决方案。
思路:当从数据库查询不到数据时,我们仍然将这个“空结果”(例如 null)进行缓存,并为其设置一个较短的过期时间(比如 1-5 分钟)。
流程:
请求查询 Key。
缓存未命中。
查询数据库,发现数据不存在。
将 Key 和一个表示“空”的特殊值(如 “NULL”)写入缓存,并设置过期时间。
后续再有相同 Key 的请求时,缓存直接返回这个空值,而不会访问数据库。
优点:
实现简单,效果立竿见影。
缺点:
内存浪费:如果恶意攻击者构造大量不同的无效Key,会导致缓存中存储大量无用的空对象,占用内存空间。
数据短期不一致:如果在空对象缓存有效期内,数据库里新增了该数据,会导致短期内读到旧的空数据。可以通过设置较短的过期时间或主动删除空缓存来缓解。
方案二:布隆过滤器(Bloom Filter)
这是一个更高效、更专业的内存节约型解决方案。
思路:在缓存之前,加一道“守卫”——布隆过滤器。它是一个概率型数据结构,可以告诉你 “某个元素一定不存在” 或 “可能存在”。
工作原理:
初始化:将所有可能存在的、合法的Key(例如所有有效的用户ID)预先加载到布隆过滤器中。
查询过程:
当一个查询请求过来时,首先去布隆过滤器判断这个Key是否存在。
如果布隆过滤器说 “不存在”,那么这个Key一定不存在于数据库中。此时可以直接返回空结果,无需查询缓存和数据库。
如果布隆过滤器说 “可能存在”,那么再按正常流程去查询缓存和数据库。
优点:
空间效率极高:占用内存非常小,因为它不存储数据本身,只存储数据的“指纹”(哈希位)。
性能极好:查询时间复杂度是 O(k),k是哈希函数的个数,速度非常快。
缺点:
存在误判率:布隆过滤器判断“可能存在”时,有极小的概率是误判(即Key其实不存在)。但这个概率可以通过调整布隆过滤器的参数(如位数组大小、哈希函数数量)来控制到非常低。
删除困难:标准的布隆过滤器不支持删除元素(但有其变种如 Counting Bloom Filter 支持)。
需要数据预热:需要系统在启动或某个时刻,将有效的数据同步到布隆过滤器中。
2.缓存雪崩:
1.什么是缓存雪崩?
缓存雪崩 是指在某一时刻,大量缓存数据同时失效(或缓存服务宕机),导致所有原本应该访问缓存的请求,瞬间都涌向了数据库。数据库无法承受如此巨大的瞬时压力,从而造成数据库响应缓慢甚至崩溃,进而导致整个系统瘫痪。
核心问题: 大量缓存集中失效或缓存服务不可用。
场景一:大量缓存Key同时过期
这是最典型的缓存雪崩场景。
原因:在给缓存数据设置过期时间时,如果采用了统一的过期时间(例如,都在凌晨零点过期),那么所有这些缓存都会在同一时刻失效。
例子:一个电商网站,在每天零点刷新优惠券、秒杀商品等信息,如果这些缓存都设置在零点过期,那么零点时所有相关请求都会直接访问数据库。
场景二:Redis集群宕机
原因:Redis服务节点因为断电、网络故障、内存爆满、硬件问题等原因宕机。
例子:主从集群中,主节点宕机且从节点未能成功切换,或者整个Redis集群不可用。
场景三:缓存预热问题
原因:系统刚重启后,缓存是空的(冷启动)。如果此时有大量用户请求涌入,所有请求都会穿透到数据库。
解决方法:
方案一:差异化过期时间(过期时间加随机值)
方案二:缓存永不过期 + 后台更新
方案三:构建高可用的Redis集群
方案四:服务降级与熔断
方案五:请求限流与队列
3.缓存击穿:
- 什么是缓存击穿?
缓存击穿 是指一个热点Key在缓存过期的瞬间,同时有大量的并发请求这个Key。这些请求发现缓存过期后,都会同时去访问数据库,瞬间给数据库带来巨大的压力,就像在缓存屏障上”击穿”了一个洞。
- 产生缓存击穿的常见场景
热点商品秒杀:某个热门商品参与限时秒杀活动,缓存过期时大量用户同时点击。
爆款新闻/视频:突发事件或热门内容的详情页,缓存失效时被大量用户访问。
明星/网红动态:顶级明星发布动态时,其个人主页缓存失效。
系统定时任务:某些定时刷新的热点数据,在刷新时刻被高频访问。
方案一:互斥锁(Mutex Lock)
这是最常用、最有效的解决方案。
思路:当缓存失效时,不让所有请求都去访问数据库,而是只让一个请求去数据库查询并重建缓存,其他请求等待,直到缓存重建完成
方案二:逻辑过期(永不过期 + 异步更新)
思路:缓存实际上不设置过期时间,而是在value中存储一个逻辑过期时间。当发现数据逻辑过期时,触发异步更新,当前请求仍然返回旧数据。
方案三:热点Key永不过期
思路:对于特别热点的Key,直接设置为永不过期,通过后台任务或数据更新时主动刷新缓存。
12.如何设计秒杀场景处理高并发以及超卖现象?
秒杀系统通过前端和网关限流削减流量, 核心库存操作放在 Redis 中使用 Lua 脚本原子扣减,防止超卖;
下单流程通过 MQ 异步化,避免高并发直接打数据库;
MySQL 作为最终兜底校验库存,并通过补偿机制保证一致性。
第 1 步:前端就开始挡人(不是摆设)
为什么?
线上 80% 请求是无效的(疯狂点、脚本)
真实做法
- 倒计时结束前按钮禁用
- 人机校验(滑块)
- 接口签名(防刷)
📌 前端挡掉 ≈ 50% 垃圾流量
第 2 步:网关限流(救命)
1 | 用户 → API Gateway |
限什么?
- IP 限流
- 用户限流
- 总 QPS 上限(如 5 万 / 秒)
超过怎么办?
- 直接返回:
系统繁忙
📌 不做这一步,后面全白搭
第 3 步:先不碰 DB,只碰 Redis(核心)
Redis 里放什么?
1 | stock:product:1001 = 1000 |
3.1 一人一单校验(真实线上必有)
1 | -- 如果用户已经抢过,直接返回 |
📌 防刷 + 防重复下单
3.2 Redis 扣库存(真正防超卖)
1 | local stock = tonumber(redis.call("get", KEYS[1])) |
📌 Lua:一条原子操作
📌 Redis 单线程:绝对不会并发超卖
执行完 Redis 后发生了什么?
- 库存已经减少
- 用户资格已锁
- DB 还没动
第 4 步:立刻返回用户结果(真实做法)
返回什么?
1 | { |
📌 不能等下单完成
📌 秒杀系统 ≠ 实时下单系统
二、那订单是啥时候写进 MySQL 的?
第 5 步:写 MQ(削峰)
1 | Redis 扣库存成功 |
MQ 里放什么?
1 | { |
📌 MQ 是 DB 的缓冲区
第 6 步:订单服务慢慢消费(安全)
1 | MQ |
写订单时还要不要校验?
✅ 一定要
1 | UPDATE product |
📌 DB 是最后一道保险
三、如果中途失败,真实线上怎么兜底?
情况 1:Redis 扣成功,MQ 失败
解决
- 发送失败重试
- 多次失败 → 补偿库存
情况 2:订单创建失败
解决
- 回滚 Redis 库存
- 删除用户资格 key
情况 3:用户抢到不付款
解决
- 延迟队列(30 分钟)
- 未支付 → 回滚库存
四、为什么不用直接加分布式锁?
真实原因
❌ QPS 太高
❌ 锁竞争严重
❌ Redis Lua 已经是“无锁原子”
📌 秒杀不用分布式锁是正确答案
二、黑马点评的真实秒杀架构
1 | 用户 |
三、黑马点评是如何防止超卖的?(重点)
1️⃣ 库存不放在 MySQL,放在 Redis
活动开始前
1 | seckill:stock:voucherId = 1000 |
📌 MySQL 不参与高并发
2️⃣ 使用 Lua 脚本完成三件事(原子)
Lua 做了什么?
- 判断库存是否 > 0
- 判断用户是否已经下过单
- 扣减库存 + 标记用户已下单
Lua 脚本逻辑(核心)
1 | -- 1. 判断库存 |
📌 Redis 单线程 + Lua 原子执行 = 永不超卖
四、黑马点评是如何抗高并发的?
1️⃣ 秒杀接口只做“很轻”的事
秒杀接口 不做:
❌ 创建订单
❌ 操作 MySQL
❌ 复杂业务逻辑
它只做:
- 执行 Lua
- 返回结果
📌 QPS 能轻松扛住
2️⃣ 使用阻塞队列 / MQ 削峰
黑马点评的实现
- 使用 Java
BlockingQueue - 单线程异步下单
1 | Redis 抢成功 |
📌 真实生产中可换 MQ(Kafka / RocketMQ)
五、订单是如何安全写入 MySQL 的?
1️⃣ 再次校验(一人一单)
1 | SELECT COUNT(*) FROM voucher_order |
📌 防止极端情况下重复订单
2️⃣ 数据库库存兜底(最终防线)
1 | UPDATE seckill_voucher |
📌 即使 Redis 出问题,DB 也不会超卖