← 全部材料 Redis
REDIS · 14 个高频考点 · 7.x

缓存的问题从来不在缓存本身

Redis 的命令简单,难的是它挂了怎么办、数据和库对不上怎么办、一个慢命令把整个实例卡住怎么办。这份把每个"怎么办"拆成可以照着答的步骤。命令示例按 Redis 7.2 的行为写。

01先把模型想清楚单线程执行、内存有限、可能丢数据——后面所有设计都是这三条的推论。
02再看故障场景穿透、击穿、雪崩、一致性、锁失效,每个都给出触发条件和兜底方案。
03最后落到选型什么该放 Redis、什么不该放,比记住命令更重要。
1

单线程的 Redis 为什么还能这么快

not because it is single-threaded, but despite it
Redis 快的原因和"单线程"没关系,它是在单线程的前提下还这么快:纯内存访问、没有锁和上下文切换的开销、epoll 多路复用、以及为每种类型定制的编码。单线程换来的确定性(每条命令原子执行)才是它真正的价值,代价是一条慢命令会卡住所有人。

✕ 面试里容易说错的

  • Redis 是单线程的 —— 不准确,6.0 起网络 IO 是多线程,还有后台线程做持久化和异步删除
  • 单线程所以快 —— 反了,单线程是限制,不是原因
  • Redis 比 MySQL 快是因为数据结构好 —— 主因是内存和磁盘的差别

✓ 准确的说法

  • 命令执行是单线程的,所以每条命令天然原子
  • 快是因为纯内存 + 无锁无切换 + epoll + 编码优化
  • 代价是慢命令会阻塞全局,所以生产环境要禁用 KEYS、FLUSHALL 这类命令
面试怎么答Redis 快有三个原因:一是纯内存操作,这是最主要的;二是单线程执行命令,省掉了锁竞争和线程上下文切换的开销,而且天然保证了单条命令的原子性;三是用了 epoll 的 IO 多路复用,一个线程就能盯着几万个连接。再加上每种数据类型都针对数据量做了编码优化。要注意 6.0 之后网络 IO 是多线程的,但命令执行仍然是单线程,所以"单线程"这个说法指的是执行模型。单线程的代价是一条慢命令会阻塞所有客户端,所以生产上要用 SCAN 代替 KEYS,用 UNLINK 代替 DEL 删大 key,并且配好慢查询日志。
2

五种类型,和它们的底层编码

each type has two or three encodings
面试问"Redis 有哪些数据结构",答五种类型只是及格。真正拉开差距的是同一种类型在不同数据量下用不同编码:小的时候用一块连续内存(listpack / intset),大了才换成哈希表和跳表。转换是单向的,一旦转过去就不会退回来。

✕ listpack 换成 hashtable 就回不去了

  • 以为删元素能省内存 —— 编码不降级,内存不会还给系统
  • 以为 listpack 慢 —— 128 个元素以内,一次内存扫描比哈希还快,缓存命中率高
  • 以为 ZSet 一定用跳表 —— 小 ZSet 用的是 listpack,ZRANGE 直接顺序扫

✓ 实际用起来要注意

  • 知道阈值在哪,才能判断一次写入会不会触发编码转换(转换是 O(N) 的,会短暂阻塞)
  • 大 key 删完记得用 MEMORY USAGE 复查,必要时重建 key
  • 榜单类数据超过 128 条就自动变跳表,内存大概翻 2~3 倍,要提前算容量
面试怎么答五种基本类型是 string、list、hash、set、zset,但真正要答的是它们的编码。string 有 int、embstr、raw 三种,44 字节是分界线。list 在 7.0 之后统一用 quicklist,链表节点里装 listpack。hash 和 zset 在元素少的时候都用 listpack,超过 128 个元素或者单个 value 超过 64 字节就转成 hashtable 和 skiplist。zset 用跳表是因为要支持范围查询,同时还有一个 dict 用来 O(1) 查分数。set 全整数时用 intset。要注意编码转换是单向的,转过去不会退回来,所以删元素不会降内存。
3

跳表:为什么有序集合不用红黑树

skiplist beats red-black tree here
ZSet 要按分数排序、要范围查询、要能快速定位排名。红黑树也能做到 O(log N),但 Redis 选了跳表,原因有三个:范围查询只需要顺着最底层链表走、实现简单到不用背平衡规则、插入删除只影响局部指针。
和红黑树的对比范围查询 ZRANGEBYSCORE 10 30 跳表:定位到 10 之后,沿着底层链表一路往右走,命中即收集 红黑树:同样能做,但要中序遍历,实现更绕 实现复杂度 跳表:插入时随机掷层数,然后改几个指针,没有旋转、没有变色 红黑树:插入删除要处理 5 种失衡情况和一堆旋转 排名 ZRANK 跳表的每个节点存了 span(跨越了几个节点),累加就是排名 红黑树要额外维护子树大小,插入删除时一路更新
面试怎么答ZSet 用跳表而不是红黑树,主要三个原因。第一是范围查询:跳表最底层就是一条有序链表,定位到起点顺着走就行,而 ZSet 最高频的操作恰恰是 ZRANGEBYSCORE 这类范围查询。第二是实现简单:跳表靠随机层数维持平衡,插入删除只改局部指针,不需要旋转和变色,代码量和出错概率都小得多。第三是排名:跳表节点里存了 span,累加就能得到 ZRANK,红黑树要额外维护子树大小。跳表的期望复杂度是 O(log N),层数按 1/4 的概率向上增长,最高 32 层。要注意小 ZSet 用的是 listpack,不是跳表。
4

过期删除与内存淘汰

two different questions, often confused
这是两个不同的问题:过期删除问的是"这个 key 到点了怎么清掉",内存淘汰问的是"内存满了该扔谁"。前者靠惰性删除 + 定期删除,后者靠八种策略。面试里最常被混着问,答的时候要先把这两件事分开。

✕ 常见的错误答案

  • "过期了就立刻删" —— 惰性 + 定期抽样,不是实时
  • "内存满了用 LRU" —— 默认是 noeviction,会直接报 OOM 错误
  • "淘汰策略能删任何 key" —— volatile 系列只删有 TTL 的

✓ 怎么选

  • 纯缓存场景:allkeys-lru 或 allkeys-lfu
  • 缓存和持久数据混在一个实例:用 volatile-lru,并且只给缓存 key 设 TTL
  • 绝对不能丢的数据:noeviction,让写入直接失败,由业务决定怎么办
面试怎么答过期删除和内存淘汰是两件事。过期删除用惰性删除加定期删除:访问时才判断是否过期,同时每 100 毫秒随机抽 20 个带 TTL 的 key 检查,过期比例超过 25% 就重复抽样。所以过期 key 不一定会立刻释放内存。内存淘汰是在 maxmemory 满了之后触发,一共八种策略,默认 noeviction 会直接报错。allkeys 系列可以淘汰所有 key,volatile 系列只淘汰设了过期时间的。LRU 是近似实现,靠随机采样 maxmemory-samples 个 key 挑最久未使用的,采样数越大越准。LFU 按访问频率淘汰,能抗住全表扫描带来的缓存污染。
5

持久化:RDB、AOF 与混合

snapshot vs log, and why you want both
RDB 是某一刻的全量快照,恢复快但会丢数据;AOF 是每条写命令的日志,丢得少但文件大、恢复慢。4.0 之后推荐混合持久化:AOF 文件的开头是一段 RDB 快照,后面追加增量命令,兼顾恢复速度和数据安全。
三条路线怎么选只要 RDB 重启恢复最快,文件小 但可能丢几分钟数据 —— 只能用于纯缓存 只要 AOF everysec 最多丢 1 秒 但文件比 RDB 大几倍,重启时要重放全部命令,几十 GB 的实例能恢复几分钟 RDB + AOF 混合(推荐) 文件前半段是 RDB,直接加载;后半段是增量命令 恢复快 + 丢得少,代价是文件不可读
面试怎么答RDB 是定时快照,fork 子进程写盘,靠 COW 省内存,文件小、恢复快,但两次快照之间的数据会丢。AOF 是写命令日志,appendfsync 有三种:no 最多丢 30 秒、everysec 最多丢 1 秒、always 每条刷盘但性能差很多。AOF 文件会越来越大,所以要重写,重写不是压缩旧文件,而是根据当前内存里的数据重新生成最少的命令,重写期间的新命令先写进缓冲区,完成后追加。4.0 之后推荐混合持久化,AOF 文件前半部分是 RDB 格式,加载快,后半部分是增量命令,兼顾两者。要注意 bgsave 的 fork 在写入量大的时候会因为 COW 占用大量额外内存,这是线上做备份容易踩的坑。
6

主从复制:全量、增量与那个 backlog

replid + offset decides which path
从库连上来先问一句"我上次同步到哪儿了",主库看 replid 和 offset 决定走哪条路:对不上就全量(bgsave 一份 RDB 传过去),对得上且数据还在 repl backlog 里就增量(只补那段命令)。backlog 是个固定大小的环形缓冲区,太小就会频繁触发全量同步。

✕ 读写分离的常见幻觉

  • 写完立刻读从库,一定读得到 —— 异步复制,可能读不到
  • 从库越多读得越快 —— 从库要接收全量写流量,太多会拖累主库
  • 主从切换后立刻恢复 —— 新主库 replid 变了,其他从库要全量重同步

✓ 实际该怎么做

  • 强一致的读走主库,或者写完延时再读(延时不靠谱,但简单)
  • 用 WAIT 或半同步复制换取"至少一个从库收到",但要接受延迟
  • 切换前评估全量同步的代价:新主库会被 bgsave 和传输打一波
面试怎么答从库第一次连上来走全量同步:主库 bgsave 生成 RDB 发给从库,同时把期间的新命令写进 replication buffer,传完再补发,从库加载完进入命令传播。断线重连时用 PSYNC 带上 replid 和 offset,如果 replid 匹配且 offset 还在 repl backlog 范围内,就只补增量;否则退回全量。backlog 是固定大小的环形缓冲区,默认 1MB,太小会导致频繁全量同步,生产上要按写入量调大。要注意主从复制是异步的,从库必然有延迟,写完立刻读从库可能读到旧数据;主库故障切换后新主库的 replid 会变,其他从库常常被迫全量同步。
7

哨兵与 Cluster:两种完全不同的扩容

sentinel for HA, cluster for sharding
哨兵解决"主库挂了怎么办",Cluster 解决"一台机器装不下怎么办"。哨兵是独立进程,盯着主从集群做自动故障转移,但数据仍然只有一份;Cluster 是把数据按 16384 个槽切到多个主节点上,每个主节点自带从节点。两者不能混着用。
两种错误的处理方式客户端请求的 key 不在这个节点上: MOVED 3999 10.0.0.12:6379 → 槽 3999 永久归那个节点,客户端要更新本地槽位表,然后重试 ASK 3999 10.0.0.12:6379 → 槽 3999 正在迁移中,这次你先去那个节点问一下,但别更新本地表 区别:MOVED 是"以后都去那儿",ASK 是"就这一次"
面试怎么答哨兵和 Cluster 解决的是不同问题。哨兵是独立的监控进程,负责自动故障转移:单个哨兵发现主库无响应是主观下线,达到 quorum 数量是客观下线,然后哨兵之间选一个 leader 执行切换,选新主库、让从库指向新主库、通知客户端。数据仍然只有一份,没有分片。Cluster 是分片方案,把 key 用 CRC16 算出 0 到 16383 的槽,每个主节点负责一段槽,每个主节点再带从节点做高可用。选 16384 是因为心跳要带槽位图,2KB 比较合适,65536 会到 8KB 太占带宽,而集群规模一般不超过一千个节点。Cluster 下多 key 操作必须在同一个槽,要用 hash tag 保证。
8

穿透、击穿、雪崩:三个词别搞混

miss on nonexistent / hot key expiry / mass expiry
三个词对应三种完全不同的故障:穿透是查一个根本不存在的数据,缓存和数据库都没有;击穿是一个热点 key 刚好过期,瞬间全部打到库上;雪崩是大量 key 同时过期或者缓存整体不可用。名字像,解法完全不同。
三种问题的判断方式穿透:命中率异常低,且请求的 key 高度重复(可能被恶意刷) → 看监控里的 cache_miss 里有多少是"库里也没有" 击穿:某个特定 key 的 QPS 突然打到数据库,其他 key 正常 → 看慢查询,会发现同一条 SQL 在短时间内被打了成千上万次 雪崩:数据库 QPS 整体飙升,缓存命中率断崖式下跌 → 通常伴随"某个时间点"或"缓存实例重启"

✕ 别把这几个混着答

  • 穿透 = 数据库里也没有,属于"无效查询"
  • 击穿 = 数据存在,只是缓存刚好失效
  • 雪崩 = 大面积失效,或者缓存整体挂了

✓ 对症下药

  • 穿透 → 布隆过滤器 + 空值缓存
  • 击穿 → 互斥锁重建 + 逻辑过期
  • 雪崩 → 随机 TTL + 多级缓存 + 熔断限流
面试怎么答穿透是查询一个数据库里也不存在的 key,每次都会穿透到库,解法是缓存空值或者用布隆过滤器提前挡掉。击穿是一个热点 key 过期的瞬间,大量请求同时打到数据库,解法是用 SET NX 加锁只让一个请求去重建,或者用逻辑过期让请求先拿旧值、后台异步更新。雪崩是大量 key 同时过期或者缓存服务整体不可用,解法是过期时间加随机值、多级缓存、以及限流降级兜底。三个问题的共同点是都可能把数据库打挂,但触发条件和解法完全不同,答的时候要分开讲。
9

缓存和数据库怎么保持一致

delete the cache, do not update it
只要用了缓存,就不存在强一致,只能做到"最终一致 + 短暂不一致窗口足够小"。业界的主流做法是 Cache Aside:先更新数据库,再删缓存。注意是删不是更新——更新的问题是并发写会互相覆盖,删掉让下次读重新加载反而安全。
不一致窗口怎么进一步压缩方案一:延迟双删 更新库 → DEL cache → sleep 500ms → DEL cache 第二次删是为了清掉"窗口期被读请求写回去的旧值" 缺点:sleep 时间靠猜,且拖慢写接口 方案二:订阅 binlog(Canal / Debezium) 数据库变更后异步删缓存,不依赖业务代码 缺点:多一个组件,有秒级延迟 方案三:给缓存设短 TTL 兜底 最终一致的最后一道保险,TTL 到了自然就对了
面试怎么答缓存和数据库只能做到最终一致。主流方案是 Cache Aside,先更新数据库再删除缓存。之所以是删除而不是更新,一是更新缓存可能算了很多没人用的值,二是并发写的时候两个请求的到达顺序不同,可能把旧值写进缓存。之所以不能先删缓存再更库,是因为删完到更库之间有空窗期,读请求会把旧值重新加载进缓存,而且之后不会再有人删它。先更库再删缓存也有一个极短的并发窗口,可以用延迟双删或者订阅 binlog 来缩窗,最后用 TTL 兜底。如果业务要求强一致,那就不该用缓存。
10

分布式锁:从 SET NX 到 Redlock 的争议

the lock is easy, the failure modes are not
加锁只要一行 SET key val NX PX 30000,难的是释放时不能删别人的锁、业务没跑完锁就过期、主从切换导致两个客户端同时持锁。前两个是工程问题,第三个是 Redlock 争论的核心。
三个必须处理的失效场景1. 业务没跑完,锁过期了 客户端 A 拿到锁,处理了 40 秒(锁 30 秒) → 锁自动释放,B 拿到锁,A 和 B 同时改同一份数据 解法:看门狗(Redisson 的 watchdog),每 10 秒续一次期 2. 释放时删了别人的锁 A 的锁过期,B 拿到锁;A 执行完 DEL,把 B 的锁删了 → 解法:value 存唯一标识,用 Lua 比对后再删 3. 主从切换导致双持锁 A 在主库加锁成功,主库还没同步到从库就挂了 从库被提升,B 在新主库上加锁也成功 → 两个客户端同时认为自己持有锁
面试怎么答分布式锁用 SET key value NX PX 毫秒 一条命令完成,value 必须是每个客户端唯一的标识。释放时不能直接 DEL,要先 GET 比对 value 再用 Lua 原子地删除,否则会把别人的锁删掉。锁的过期时间要覆盖业务耗时,不够就用看门狗续期,Redisson 默认 30 秒、每 10 秒续一次。主从架构下还有个问题:主库加锁成功但还没同步就故障切换,新主库上另一个客户端也能加锁成功,于是两个客户端同时持锁。Redlock 想用多实例多数派解决,但它依赖时钟假设,而且在客户端发生 GC 停顿时无法感知锁已过期,这个方案在需要严格正确性的场景并不被推荐。要求更高可以用 ZooKeeper 或 etcd,本质是要有单调递增的 fencing token 让存储层兜底。
11

事务、Lua 与 Pipeline 的边界

three things people call "transaction"
这三样经常被混为一谈,但保证的东西完全不同:Pipeline 只是把多条命令打包发出去,减少网络往返,不保证原子性;MULTI/EXEC 保证"一起执行、中间不插队",但出错不回滚;Lua 才是真正的"原子执行一段逻辑"。
三者对比Pipeline 多条命令一次发出,攒着一起收结果 减少 RTT,10 条命令 1 次往返 不保证原子:中间可能插进别的客户端的命令 MULTI / EXEC 命令入队,EXEC 时按顺序连续执行,不被打断 ★ 运行期错误不回滚:语法错在入队时就报,运行错(比如对 string 做 LPUSH)只影响那一条 ★ 不支持条件判断,你没法在事务里"先读再决定写什么" Lua 整段脚本在 Redis 里一次执行完,期间不处理其他命令 可以做条件判断、可以循环 ★ 脚本执行时间默认上限 5 秒(busy-reply-threshold),超了会阻塞 ★ 脚本必须短小,别在里面做重活
面试怎么答三者的保证强度依次递增。Pipeline 只是把多条命令一次发送、一次收结果,目的是减少网络往返,不保证原子性,中间可能插入其他客户端的命令。MULTI/EXEC 保证事务里的命令连续执行不被打断,但它不支持回滚,运行期错误只影响出错的那一条,语法错误则在入队阶段就会报错;它也不支持条件判断,所以没法"先读再决定写什么"。Lua 脚本在 Redis 里一次执行完,期间不处理其他命令,所以是真正原子的,还能做条件判断和循环,常用来实现分布式锁释放和库存扣减。要注意脚本必须短小,执行期间会阻塞整个实例,而且所有 key 要通过 KEYS 传参,Cluster 下还要求同槽。
12

大 key 与热 key:两个相反的病

big key kills latency, hot key kills throughput
大 key 是单个 key 的数据量太大,危害是一次操作就能卡住主线程(删一个几百万元素的 hash 要几百毫秒);热 key 是单个 key 的访问量太高,危害是把一个分片的 CPU 打满。前者是空间问题,后者是流量问题,治理手段完全不同。
危险动作与替代方案DEL 一个 100 万元素的 hash → 主线程逐个释放,阻塞几百毫秒到几秒 → 换成 UNLINK:只解除引用,实际内存在后台线程慢慢释放 HGETALL 一个 10 万字段的 hash → 一次返回几百 MB,网络和客户端内存都会崩 → 换成 HSCAN 分批,或用 HMGET 只取需要的字段 KEYS * → 全库扫描,直接阻塞 → 换成 SCAN 游标分批

✕ 常见处理方式的问题

  • 用 DEL 删大 key —— 主线程阻塞,线上会抖动
  • 用 KEYS 找大 key —— 本身就会阻塞,比大 key 更危险
  • 给热 key 加锁 —— 锁会成为新的瓶颈,QPS 越高越糟

✓ 正确姿势

  • UNLINK 异步删除,或者 HSCAN 分批删字段
  • 用 --bigkeys / SCAN 在低峰期扫描,或用离线分析 RDB 文件
  • 热 key 用本地缓存挡在前面,而不是在 Redis 层想办法
面试怎么答大 key 是单个 key 的数据量太大,危害主要是操作时会阻塞主线程,比如 DEL 一个上百万元素的 hash 要几百毫秒,期间整个实例都卡住;还有网络传输和客户端内存的开销,集群下还会导致数据倾斜。治理方法是拆分、用 UNLINK 异步删除、用 HSCAN 分批读,扫描用 --bigkeys 或 SCAN 而不是 KEYS。热 key 是单个 key 的访问量过高,危害是把某个分片的 CPU 打满,治理方法是加本地缓存把请求挡在 Redis 之外、读写分离分散到从库、或者把 key 复制多份打散。这两个问题一个是空间一个是流量,手段完全不同。
13

典型场景:别只会说"缓存"

rank, counter, rate limit, session
面试最后常问"你项目里 Redis 用来干什么"。能说清为什么这个场景适合 Redis,比报出十个场景有用。判断标准就一条:这个操作是不是高频、对延迟敏感、且能接受最终一致。
几个场景为什么适合 Redis排行榜 ZSet 的跳表按分数有序,ZREVRANGE 拿 TopN、ZREVRANK 拿名次都是 O(log N) 换 MySQL:要么 ORDER BY 全表排序,要么维护排名表,都远不如这个 分布式 Session 多个服务实例共享登录态,读写都在内存里 过期时间就是 TTL,天然不用清理 分布式 ID / 计数器 INCR 是原子的,不会出现"两个请求拿到同一个号" 简单队列 List 的 LPUSH + BRPOP 就是阻塞队列 但要注意:没有 ACK 机制,消费者崩了消息就丢了 → 要可靠投递用 Stream(有消费者组和 ACK)
面试怎么答Redis 适合高频、低延迟、能接受最终一致的场景。排行榜用 ZSet,因为跳表按分数有序,ZREVRANGE 取 TopN、ZREVRANK 取个人名次都是 O(log N),比数据库排序高效得多。计数器用 INCR,单条命令原子,避免了读改写之间的并发问题。限流可以用 INCR 加 EXPIRE 实现固定窗口,精确一点用 ZSet 做滑动窗口,生产上更常用 Lua 实现的令牌桶。分布式 Session 让服务实例变成无状态,扩缩容不用粘性会话。队列可以用 List 的 BRPOP,但它没有 ACK,消费者崩溃会丢消息,要可靠投递得用 Stream。反过来,需要复杂查询、强一致的金额、或者单个几百 KB 的大对象,都不适合放 Redis。
14

速答清单

last 30 minutes before the interview
考前扫一遍。答不上来的,回到对应那一节看推导,别背结论。
01Redis 是单线程的吗?
命令执行是单线程的,但 6.0 之后网络 IO 是多线程的,持久化、异步删除这些也有后台线程。说"单线程"指的是执行模型,所以每条命令天然原子。
02单线程为什么还这么快?
主要是纯内存操作,其次是省掉了锁竞争和线程上下文切换,再加上 epoll 多路复用让一个线程能盯几万个连接,还有针对每种类型的编码优化。快不是因为单线程,而是"在单线程的前提下还快"。
03单线程的代价是什么?
一条慢命令会阻塞所有客户端。所以生产环境禁用 KEYS、FLUSHALL,删除大 key 用 UNLINK,Lua 脚本必须短小,并且配好慢查询日志。
04五种基本类型分别是什么?
string、list、hash、set、zset。追问要能说出各自的编码:string 有 int/embstr/raw,list 是 quicklist,hash 小的时候 listpack、大了 hashtable,set 全整数时 intset,zset 小的时候 listpack、大了 skiplist。
05string 的 44 字节是什么意思?
embstr 和 raw 的分界线。44 字节以内用 embstr,把 redisObject 头和内容一次性分配,只做一次内存分配和释放;超过 44 字节用 raw,分开分配。
06zset 为什么用跳表不用红黑树?
范围查询更方便,最底层就是有序链表;实现简单,不需要旋转和变色;节点里存了 span,可以 O(log N) 算排名。红黑树都能做,但复杂度和代码量都更高。
07跳表的层数是怎么定的?
每次插入随机决定,Redis 里以 1/4 的概率向上长一层,最高 32 层。所以不需要任何平衡操作,期望复杂度是 O(log N)。
08过期 key 是怎么删除的?
惰性删除加定期删除。访问时判断是否过期,同时每 100ms 随机抽 20 个带 TTL 的 key 检查,过期比例超过 25% 就再抽一轮。所以过期 key 不一定立刻释放内存。
09内存淘汰有哪几种策略?
八种:noeviction、allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl。默认是 noeviction,内存满了直接报错。volatile 系列只淘汰设了过期时间的 key。
10LRU 是真 LRU 吗?
不是,是近似 LRU。随机采样 maxmemory-samples 个 key(默认 5),淘汰其中最久未使用的。采样数越大越接近真 LRU。真 LRU 要维护双向链表,成本太高。
11LFU 和 LRU 怎么选?
LFU 按访问频率淘汰,能抗住"全表扫一遍"这种把冷数据刷成热数据的场景。纯缓存推荐 allkeys-lfu;如果访问分布比较均匀、想简单点,用 allkeys-lru。
12RDB 和 AOF 的区别?
RDB 是定时全量快照,fork 子进程写盘,文件小、恢复快,但会丢两次快照之间的数据。AOF 是写命令日志,appendfsync 有 no/everysec/always 三档,最多丢 1 秒,但文件大、恢复慢。
13混合持久化是什么?
4.0 之后支持,AOF 文件的前半段是 RDB 格式的快照,后半段是增量命令。恢复时先加载 RDB 部分,再重放增量,兼顾恢复速度和数据安全。7.0 默认开启。
14bgsave 的时候内存为什么会涨?
fork 之后父子进程共享物理内存,靠 copy-on-write。父进程写入时会复制被修改的页,所以写入量越大,额外占用的内存越多。写入很猛的实例做快照,峰值内存可能接近翻倍。
15AOF 重写是怎么做的?
不是压缩旧文件,而是 fork 一个子进程,根据当前内存里的数据直接生成最少的命令。重写期间的新命令同时写进旧 AOF 和重写缓冲区,重写完成后拼接。
16主从复制的全量和增量怎么区分?
从库发 PSYNC 带上 replid 和 offset。replid 匹配且 offset 还在 repl backlog 范围内就走增量;否则走全量,主库 bgsave 生成 RDB 传过去,同时把期间的新命令写进 buffer,传完补发。
17repl-backlog 太小会怎样?
从库断线时间稍长,offset 就落到了 backlog 之外,只能退回去做全量同步。表现为"主从频繁全量同步",第一件事就是调大 repl-backlog-size。
18主从延迟怎么解决?
没法彻底解决,复制是异步的。工程上的办法是:强一致的读走主库、写完延时再读、或者用 WAIT 命令等待至少一个从库确认(但要接受延迟增加)。
19哨兵是怎么判断主库挂了的?
单个哨兵发现无响应是主观下线;达到 quorum 个哨兵都这么认为就是客观下线;然后哨兵之间用 Raft 选一个 leader 执行故障转移。
20为什么 Redis Cluster 是 16384 个槽?
心跳包要携带槽位图,16384 位正好 2KB,65536 位就是 8KB,gossip 心跳太占带宽。而集群规模设计上不超过约 1000 个节点,16384 个槽已经够分。
21MOVED 和 ASK 有什么区别?
MOVED 表示这个槽永久归属另一个节点,客户端要更新本地槽位表;ASK 表示槽正在迁移,这次请求临时去目标节点问,但不要更新本地表。
22Cluster 下 MGET 多个 key 报错怎么办?
报 CROSSSLOT,因为多个 key 不在同一个槽。解决办法是 hash tag,用 {} 把要保证同槽的部分括起来,比如 {user:1001}:name 和 {user:1001}:age。
23缓存穿透是什么?怎么解决?
查一个数据库里也不存在的 key,每次都穿透到库。解法是缓存空值(带短 TTL)或者用布隆过滤器,在访问 Redis 之前就挡掉。
24缓存击穿和穿透有什么区别?
击穿是数据存在,只是热点 key 恰好过期,瞬间大量请求打到库上;穿透是数据根本不存在。击穿用互斥锁重建或者逻辑过期,穿透用布隆过滤器或空值缓存。
25缓存雪崩怎么防?
三层:过期时间加随机抖动,避免同时失效;多级缓存,本地缓存加 Redis,防 Redis 整体不可用;限流降级兜底,保证数据库不被压垮。
26缓存和数据库怎么保持一致?
Cache Aside,先更新数据库再删除缓存。是删不是更新,因为更新会有并发覆盖问题,而且可能算了很多没人用的值。只能做到最终一致,用延迟双删或者订阅 binlog 缩窗,TTL 兜底。
27为什么不能先删缓存再更库?
删完到更库之间有空窗期,这期间的读请求会把旧值重新加载进缓存,而且之后不会再有人删它,脏数据会留到 TTL 到期。
28延迟双删的延迟该设多久?
要大于一次"读请求查库 + 写缓存"的耗时。这个值没法精确算,所以它是经验做法。更可靠的是订阅 binlog 异步删缓存。
29分布式锁怎么加?
SET key value NX PX 30000,一条命令完成。value 必须是每个客户端唯一的标识,比如 UUID 加线程 ID。NX 保证互斥,PX 保证客户端崩溃后锁能自动释放。
30分布式锁释放要注意什么?
不能直接 DEL,要先 GET 比对 value 再删,而且这两步要用 Lua 保证原子。否则可能出现"我的锁过期了,别人拿到了锁,我执行完把它删了"。
31锁的过期时间不够怎么办?
用看门狗续期。Redisson 默认锁 30 秒,后台每 10 秒检查一次,业务还在跑就续到 30 秒。客户端真的挂了就续不了,锁自然过期,这是对的。
32Redlock 为什么不推荐?
它依赖各节点时钟不漂移这个假设,而分布式系统里时钟不可信;而且持锁期间如果客户端发生 GC 停顿,锁可能已经过期而客户端不知道。需要严格正确性应该用 ZooKeeper、etcd 或者 fencing token。
33Redis 事务支持回滚吗?
不支持。MULTI/EXEC 保证命令连续执行不被打断,但运行期错误只影响出错的那一条,其他命令照常执行。设计上认为运行期错误是程序 bug,应该在开发阶段发现。
34WATCH 是干什么的?
乐观锁。WATCH 某个 key 之后,如果它在 EXEC 之前被别人改了,整个事务不执行,EXEC 返回 nil。适合读-改-写场景,但并发高时重试率会很高。
35Lua 和事务有什么区别?
Lua 是真正原子的,还能做条件判断和循环,执行期间 Redis 不处理其他命令。事务不支持条件判断,你没法在事务里先读再决定写什么。但 Lua 必须短小,否则会阻塞整个实例。
36Pipeline 是事务吗?
不是。它只是把多条命令打包一次发出、一次收结果,减少网络往返,不保证原子性,中间可能插入其他客户端的命令。但吞吐提升非常明显。
37大 key 有什么危害?
删除时会阻塞主线程几百毫秒甚至几秒;网络传输可能打满带宽;客户端反序列化占大量内存;集群下会造成数据倾斜。所以大 key 要用 UNLINK 异步删,读用 HSCAN 分批。
38热 key 怎么发现和处理?
用 --hotkeys(需要 LFU)或监控代理统计。处理是三层:本地缓存挡住大部分请求、读写分离分散到从库、把 key 复制多份打散到不同节点。
39为什么不能用 List 当可靠队列?
BRPOP 取走消息后,消费者处理时崩溃,这条消息就永久丢失了,没有 ACK 和重投机制。要可靠投递用 Stream,它支持消费者组、ACK 和 XPENDING + XCLAIM 重新认领。
40Redis 适合放什么、不适合放什么?
适合高频、低延迟、能接受最终一致的数据:缓存、排行榜、计数器、Session、限流。不适合需要复杂查询和事务的数据、需要强一致的金额、以及单个几百 KB 的大对象。