Redis面试
- 常见缓存读写策略
- [Redis生产问题和解决方案(缓存雪崩 缓存穿透 缓存击穿(热点key问题))](#Redis生产问题和解决方案(缓存雪崩 缓存穿透 缓存击穿(热点key问题)))
- 持久化机制
- 集群(实现高可用)
- 内存管理
-
- 过期删除(惰性删除+定期删除)
- 内存淘汰
-
- 不进行数据淘汰(Redis3.0后默认)
- 进行数据淘汰(可配置策略)
-
- [在设置了过期时间的数据中进行淘汰 volatile](#在设置了过期时间的数据中进行淘汰 volatile)
- [在所有数据范围内淘汰 allkeys](#在所有数据范围内淘汰 allkeys)
- 分布式锁
- 性能优化
-
- [一、 设计与数据模型](#一、 设计与数据模型)
- [二、 客户端与命令使用层面](#二、 客户端与命令使用层面)
- [三、 运维视角](#三、 运维视角)
- [四、 架构层面](#四、 架构层面)
常见缓存读写策略
Cache Aside(旁路缓存)策略
写
先更新数据库中的数据,再删除缓存中的数据。
读
如果读取的数据命中了缓存,则直接返回数据
如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
Read/Write Through(读穿 / 写穿)策略
写
应用程序的"写"请求,必须直接发送到缓存组件。缓存组件负责同步: 缓存组件(或一个智能的客户端库)接收到写请求后,会原子性地、同步地执行以下两个操作:a. 更新数据库: 将新的数据写入底层数据库。b. 更新缓存: 将新的数据写入自身的缓存。
应用要直接写缓存,但缓存系统会保证数据库也随之更新。 这正是 Write-Through 模式得名的原因------写请求"穿透"缓存,直达数据库。
读
应用直接读缓存。缓存命中则返回;未命中则由缓存组件负责从数据库加载并写入自身,然后返回。
| 特性 | 读穿 / 写穿 | 旁路缓存 |
|---|---|---|
| 核心哲学 | 缓存是数据主入口,对应用透明,是一种封装 | 缓存是数据库的辅助旁路,应用显式管理 |
| 读流程 | 缓存未命中时,缓存自身从DB加载 应用 -> 缓存。 | 存未命中时,应用从DB加载并回填 |
| 写流程 | 缓存自身同步更新DB | 应用 -> 数据库,然后应用使缓存失效 |
| 性能 | 写操作延迟较高(需同步写DB) | 写操作延迟较低(因为可以做优化,比如合并多个缓存失效1操作,异步更新DB删除缓存) |
| 适用场景 | 一致性要求高、希望应用逻辑简单的场景 | 需要最高性能、能接受短暂不一致的通用场景 |
Write Back(写回)策略
写
只更新缓存,将缓存数据设置为脏的,不更新数据库。对于数据库,批量异更新
Redis生产问题和解决方案(缓存雪崩 缓存穿透 缓存击穿(热点key问题))
这些问题都是害怕数据库出问题,而不是说缓存本身。数据库和缓存性能差距巨大,QPS相差1-2个数量级。
缓存穿透是查询数据库中不存在的数据,缓存雪崩是大量缓存Key同时失效,导致大量请求到达数据库。
你查的数据本来有,但因为集体失效导致数据库压力大,是雪崩。
你查的数据本来就没有,导致每次请求都直接打到数据库,是穿透。

缓存雪崩
大量的缓存数据同时过期(失效),或者缓存服务整体宕机,导致所有原本应该访问这些缓存的请求,瞬间都涌向了后端数据库(如MySQL)。
错开过期时间(针对同时)
高可用集群(解决单点故障)
缓存穿透
缓存穿透是查询数据库中不存在的数据
缓存空对象
即使查询不到,也缓存一个空值(如""或null)并设置一个较短的过期时间。
布隆过滤器
在缓存之前加一层布隆过滤器,用于快速判断某个key是否一定不存在于数据库中。如果布隆过滤器说不存在,则直接返回,避免查询数据库。
缓存击穿(热点Key问题)
一个热点key在缓存过期的瞬间,大量请求同时涌入,击穿缓存,直接访问数据库。
互斥锁
大量打到数据库的请求让第一个拿到锁,其他的拿不到锁就休眠不去打到数据库重新尝试拿缓存,拿不到缓存就尝试拿锁,拿不到锁就再等会继续,,直到第一个写入缓存。当然也要释放锁
热点数据永不过期(用逻辑过期)

持久化机制
AOF持久化
AOF的写入分为两个步骤:
命令追加(Append to Buffer):
主线程执行完写命令后,会将该命令以协议格式追加到内存中的AOF缓冲区(aof_buf)。
这个操作是在主线程中完成的! 如果写入的是一个非常大的Key(例如一个包含几万元素的HMSET命令),构造和写入这个庞大的命令到缓冲区本身就会阻塞主线程较长时间。
同步磁盘(Sync to Disk):
根据 appendfsync 的配置,决定如何将缓冲区中的数据写入并同步到磁盘的AOF文件中。
这就是 everysec, always, no 策略发挥作用的地方。

fsync()是一个系统调用,能立马将数据同步到硬盘,而不是像write一样先到内核缓冲区。
写入(Write):当Redis执行一个写命令后,它会将这条命令追加到AOF缓冲区(in-memory buffer) 中。
同步(Sync):但是,Redis不会主动调用 fsync() 或 fdatasync() 系统命令来强制将缓冲区中的数据刷到磁盘上。
刷盘的时机:数据的刷盘工作完全交给了操作系统的I/O机制。操作系统会根据自己的策略(例如缓冲区满了、有一个固定的时间周期等)来决定何时将内核页面缓存(page cache)中的AOF文件数据真正写入物理磁盘。
三种写回策略

AOF重写
普通的AOF文件(appendonly.aof)记录的是导致数据库状态发生改变的所有写命令(例如 SET, HSET, SADD, LPUSH 等),以Redis协议的格式追加写入。所以它是一个连续的、按时间顺序排列的命令日志。
写入 AOF 日志的操作是在主进程完成的,因为它写入的内容不多,所以一般不太影响命令的操作。
但是AOF重写时,要读取所有缓存的键值对,所以要耗时,故开子进程。
存在问题,如果子进程生成AOF文件的时候,一部分处理过的生成了对应AOF日志的键值又被修改了。最后就会丢失这个修改。
如何解决?
利用写时复制"和"AOF重写缓冲区
写时复制指的是,在写入发生后,指向自己新复制的那一块物理内存,而另一个进程继续指向原来的那一块。

创建子进程:主进程fork出子进程。子进程拥有与主进程完全一致的内存数据快照。(页表对应的是相同的物理内存)这块内存被标记为只读以保护子进程的数据视图。
开始重写:子进程开始读取内存数据,并将其转换为命令写入新的AOF文件。同时,主进程开启 AOF重写缓冲区。
主进程接收新命令:例如 INCR count(原来值是10)。当主进程尝试写入时,操作系统会介入,将被修改的内存页(即存放 counter 值的那一小块内存)复制一份。
写时复制发生:操作系统复制 count 所在的内存页,主进程在新的副本上将其值改为 11。
子进程不受影响:它仍然在旧的副本上工作,看到的 count 值还是 10。
命令被写入缓冲区:这个 INCR count 命令被主进程追加到 AOF重写缓冲区。
子进程完成:子进程基于旧快照(count=10) 生成的新AOF文件写完了。这个文件里的记录是 SET count 10。
追加新命令:主进程把 AOF重写缓冲区 里的所有命令(包含 INCR count)追加到新AOF文件的末尾。
替换文件:现在这个新的AOF文件包含了:
前半部分:子进程生成的快照命令(SET count 10)
后半部分:重写期间发生的新命令(INCR count)
当Redis重启加载这个文件时,先执行 SET count 10,再执行 INCR count,最终得到的结果 count=11,这与数据库的最终状态完全一致!
AOF记录的是操作命令。如果要做恢复。得有执行命令的过程。
如果用RDB,那么直接把他读入内存就可以。
RDB快照
BG 是 BackGround(后台)的缩写。
BGSAVE 的全称是 BackGround SAVE(后台保存)。
执行bgsave过程中,Redis依然可以继续处理操作命令,具体是通过写时复制技术做到的
RDB和AOF合体
混合使用AOF日志和内存快照(混合持久化)
混合持久化工作在AOF日志重写过程,也就是说原来的AOF重写,子进程写入的是AOF日志,而现在写入的是RDB快照的全量数据,之后再写入AOF格式的增量数据(将重写缓冲区中的内容写入)

集群(实现高可用)
主从复制
一个主节点负责写,多个从节点负责读。
哨兵模式
在主从复制的基础上,增加了哨兵进程来监控主从节点的健康状态。当主节点宕机时,哨兵能自动将一个从节点提升为新的主节点,并让其他从节点指向新的主节点。
切片集群模式(cluster集群)
有主从 ,无哨兵 ,分片

Redis Cluster是从Redis3.0版本开始,官方提供的一种实现切片集群的方案。
哈希槽Hash Slot

Redis切片集群(官方Redis Cluster)固定使用16384(16K)个哈希槽(Hash Slot),并且这个数字是硬编码的,不可配置。
分片 (Sharding):
数据被自动分割到多个 主节点 (Master) 上,每个主节点负责整个集群数据的一个子集。
哈希槽 (Hash Slot):
集群共有 16384 个槽。每个键通过 CRC16 校验后对 16384 取模来决定放入哪个槽。集群中的每个主节点负责一部分哈希槽。
哈希槽怎么映射到具体的主节点中?
- 平均分配。默认均分
- 手动分配
每个主节点都应该至少有一个从节点。
高可用 (High Availability):
每个主节点都应至少有一个 从节点 (Slave)。当主节点发生故障时,其从节点会自动晋升为新的主节点,继续提供服务。
Redis 分片集群(Cluster)模式中,没有也不需要独立的哨兵 (Sentinel)进程。
哨兵机制的作用是实现主从节点故障转移,但是Redis切片集群,持续地与其他节点进行通信,互相监控健康状态。当需要判断一个主节点是否失效时,集群中的其他主节点会共同参与投票,这个过程类似于哨兵集群的投票机制。一旦集群确认某个主节点下线,它就会自动从其从节点中选举出一个新的主节点来接管服务。
连接到Redis Cluster时,只能使用数据库0(db0)。Redis开发团队认为,大多数用户实际上并不需要多个数据库。去掉多数据库支持可以简化内部逻辑。
内存管理
过期删除(惰性删除+定期删除)
- 惰性:客户端访问键 → Redis检查过期字典 → 如果过期 → 立即删除 → 返回空值
- 定期:定时任务启动 → 随机抽样键 → 检查过期 → 删除过期键 → 控制执行时间(避免循环过度出现线程卡死)→ 判断本轮过期key比例→ 如果超过则返回随机抽样那个步骤
内存淘汰
当超过配置的maxmemory,会触发内存淘汰机制
不进行数据淘汰(Redis3.0后默认)
当超过maxmemory,直接返回错误,不再提供服务。
进行数据淘汰(可配置策略)
在设置了过期时间的数据中进行淘汰 volatile

- volatile-random - 随机淘汰
- volatile-ttl - 优先淘汰最早过期
- volatile-lru - 淘汰最久未使用
- volatile-lfu - 淘汰最少使用
在所有数据范围内淘汰 allkeys
- allkeys-random 随机
- allkeys-lru 最久未使用
- allkeys-lfu 最少使用
分布式锁
分布式锁的核心是保证在分布式系统中的互斥访问。Redis 因其高性能和原子操作非常适合实现。
SET key unique_value NX PX timeout 这条原子命令来加锁,确保加锁和设置过期时间是原子的。EX表示过期时间单位是秒,PX表示毫秒
bash
# 获取锁:如果 'my_lock' 不存在,则设置其值为 'uuid_12345',并指定过期时间为 30 秒 (30000毫秒)
SET my_lock uuid_12345 NX PX 30000
释放锁时,我会通过 Lua 脚本先比较 unique_value看看是不是自己的 再删除,保证只能释放自己持有的锁,避免误删
性能优化
一、 设计与数据模型
-
选择合适的数据结构
- 典型案例 : 用
String存储一个用户对象 vs 用Hash存储。- 如果要频繁修改用户某个字段(如年龄),
Hash的HSET比读写整个String性能高得多,且内存占用更优。
- 如果要频繁修改用户某个字段(如年龄),
- 复杂场景 : 比如需要存储用户最近10次登录记录,可以使用
List;需要存储点赞用户列表且要求去重,使用Set;需要排行榜,使用ZSet。 - 面试金句:"在 Redis 中,选择合适的结构通常是最大、最有效的优化。"
- 典型案例 : 用
-
避免 Big Key(大Key)
- 定义: 通常指单个 String 类型的 Value 大于 1M,或复合类型(Hash, List, Set, ZSet)元素数量超过 10000个(这个阈值可调整)。
- 危害 :
- 操作耗时: 读写一个 Big Key 会消耗大量网络和 CPU 资源,导致请求延迟增高。
- 阻塞风险 : 使用
DEL删除一个大 Key 可能导致 Redis 服务短暂阻塞。 - 主从同步延迟:大Key占用内存多,同步过程需要传输大量数据,导致主从之间的网络延迟增加。
- 数据分片不均匀:对于集群模式,数据分片不均匀,某个数据分片的内存远超过其他数据分片。
- 解决方案 :
- 将大对象拆分成多个小对象。例如,一个大的 Hash 可以按字段前缀拆分成多个小的 Hash。
- 外部对象存储 + 元数据索引。对于文档中的超大内容,特别是二进制文件(如PDF、图片、视频),最适合使用此方案。
操作方法:将文档的原始大文件(或大文本内容)上传到专业的对象存储服务(如 AWS S3, Azure Blob Storage, 阿里云 OSS, 腾讯云 COS 等)。(元数据,就是"关于数据的数据"。 它描述了数据的背景信息,但它本身不是数据的主体内容。
可以把它想象成商品的包装盒或标签
对)象存储会返回一个唯一的文件访问地址(URL)。在知识库系统(如Redis/数据库)中,只存储这个文件的元数据和URL。
对象存储是为海量文件设计的,成本低
xml
Key: doc:{doc_id}
Value: { "title": "我的文档", "author": "张三", "file_url": "https://my-bucket.s3.amazonaws.com/path/to/doc.pdf", "upload_time": "2023-10-27" }
- 规避 Hot Key(热Key)
- 定义: 某个 Key 在短时间内被极高频率地访问。比如在秒杀中。
- 危害: 对单个分片(无论是单实例还是集群模式)造成巨大压力,可能打满网卡或 CPU,成为系统瓶颈。
- 解决方案 :
- 本地缓存: 在应用层使用 Guava Cache 或 Caffeine 做本地缓存,降低对 Redis 的访问压力。注意设置合理的过期时间和缓存更新策略。
- Key 拆分 : 将一个热 Key 拆分成多个 Key,分散到不同节点。例如,
user:info:123可以拆成user:info:123:shard1、user:info:123:shard2,在访问时对后缀进行随机或取模。 - Redis 6.0+: 使用客户端缓存(Client-side Caching)功能。
二、 客户端与命令使用层面
-
使用批量操作和管道(Pipeline)
- 问题 : 循环执行 N 次
SET会产生 N 次网络往返(RTT),延迟极高。 - 解决方案 :
- 批量命令 : 如
MSET、HMGET,一次处理多个键值对。 - 管道(Pipeline): 将多个命令打包在一次请求中发送给服务器,大大减少 RTT。适用于需要执行多个连续命令且后一个命令不依赖前一个命令结果的场景。
- 批量命令 : 如
- 注意: Pipeline 内的命令数量也要控制,避免造成单次请求数据量过大。
- 问题 : 循环执行 N 次
-
避免阻塞类命令
- KEYS : 在生产环境绝对禁止使用。它会遍历所有 Key,导致 Redis 服务短暂不响应。使用
SCAN替代。 - FLUSHALL/FLUSHDB: 清空数据库,同样会造成阻塞。如需使用,请确认环境并做好预案。
- 长时间执行的 Lua 脚本: Lua 脚本在执行期间是原子的,会阻塞其他所有命令。确保脚本逻辑轻量。
- KEYS : 在生产环境绝对禁止使用。它会遍历所有 Key,导致 Redis 服务短暂不响应。使用
-
连接池
- 避免频繁地创建和关闭 Redis 连接。使用连接池(如 Lettuce, Jedis)来管理连接,提升性能。
三、 运维视角
-
内存优化
- 配置最大内存 : 使用
maxmemory参数防止内存耗尽。 - 选择合适的淘汰策略 : 使用
maxmemory-policy配置,常用volatile-lru(对设置了过期时间的 Key 使用 LRU 淘汰)或allkeys-lru。 - 使用 Redis 4.0+ 的主动碎片整理 : 配置
activedefrag yes来减少内存碎片。
- 配置最大内存 : 使用
-
持久化优化
- RDB: 生成快照。
- AOF :
appendfsync always: 每个写命令都刷盘,数据最安全,性能最低。appendfsync everysec: 每秒刷盘,是性能和安全性的折中方案(推荐)。appendfsync no: 由操作系统决定刷盘时机,性能最好,但数据丢失风险最高。
- 混合持久化(Redis 4.0+) : 开启
aof-use-rdb-preamble yes,在重写 AOF 文件时,先以 RDB 格式写入全量数据,再追加增量 AOF 命令。兼顾了启动速度和数据安全。
四、 架构层面
-
读写分离
- 通过主从复制(Replication),将读请求分散到多个从节点,减轻主节点压力。适用于读多写少的场景。
-
数据分片(Sharding)
- 当单个实例内存、CPU 或网络成为瓶颈时,必须进行分片。
- 方案 :
- Redis Cluster: 官方原生方案,支持数据分片和高可用。是大多数场景的首选。
-
使用多实例
- 即使在单台服务器上,也可以运行多个 Redis 实例,绑定到不同的 CPU 核心,充分利用多核 CPU 资源。