Redis:数据结构、持久化与缓存一致性

从底层编码、单线程模型到 RDB/AOF、淘汰策略和分布式锁,系统梳理 Redis 面试题与线上故障的应对方式

Redis 题要避免停留在「有什么数据类型」。真正有区分度的是:为什么这个场景选这个结构、数据丢了最多丢多少、缓存和数据库不一致时怎么办。

1. Redis 有哪些数据类型?底层编码是什么?

基础类型有 String、List、Hash、Set、ZSet,扩展类型有 Bitmap、HyperLogLog、GEO 和 Stream。

底层编码会随数据量自动切换:String 有整数编码、短字符串编码和 SDS;Redis 7 起,小 List 直接用 listpack,元素变多后转为 quicklist(由多个 listpack 组成的双向链表);Hash 和 ZSet 在小数据量下用 listpack,超过阈值后分别转为哈希表和跳表加哈希表;Set 在纯整数时用 intset,否则用 listpack 或哈希表。

选型要贴合访问模式:计数器用 String 的 INCR;排行榜用 ZSet;去重计数用 HyperLogLog(有误差);签到用 Bitmap;消息队列可以用 Stream,但要评估消费组和持久化语义。

记住一个原则:结构选错,后面靠加机器补不回来。例如用 String 存整个大 JSON 再整体更新,会放大网络和序列化开销。

2. Redis 为什么快?单线程模型是怎么回事?

Redis 快在几点:数据在内存;数据结构针对场景优化;用 epoll 做 IO 多路复用;命令执行单线程,避免了锁竞争和上下文切换;协议简单。

「单线程」指命令执行是单线程的。Redis 6 起引入了多线程 IO,用来处理网络读写和协议解析,命令仍在主线程串行执行,所以不需要为数据结构加锁。

代价是任何慢命令都会阻塞所有请求。生产上要避免 KEYS *、大 key 的 HGETALL、对大集合做 SORT、长时间运行的 Lua 脚本,以及直接 DEL 超大 key(用 UNLINK 异步释放)。

排查慢请求可以用 SLOWLOG GET 和 LATENCY DOCTOR,并在监控里关注命令耗时和阻塞型命令的出现频率。

3. RDB 和 AOF 怎么选?会丢数据吗?

RDB 是某一时刻的全量快照,BGSAVE 通过 fork 子进程写盘,借助写时复制减少阻塞。文件紧凑、恢复快,但两次快照之间的数据会丢。

AOF 记录写命令,appendfsync 有三个档位:always 最安全但性能最差;everysec 是默认值,最多丢约一秒;no 交给操作系统刷盘,丢得更多。AOF 文件会持续增长,需要重写压缩。

Redis 4 起支持混合持久化:重写时先生成 RDB 格式的基线,再追加增量命令,兼顾恢复速度和数据完整度。

实际选择通常是两者都开:RDB 用于备份和快速恢复,AOF 用于缩小丢失窗口。但无论怎么配,Redis 都不是强一致存储,关键数据必须有数据库或消息队列兜底。

4. 过期键是怎么删除的?内存满了怎么办?

过期删除是「惰性 + 定期」组合:访问键时检查是否过期;同时后台周期性随机抽样,把过期比例降到阈值以下。这样避免为每个键建定时器,但也意味着过期键可能短暂占内存。

从节点通常不独立删除过期键,而是等待主节点的删除命令同步,保证主从数据一致。

内存达到 maxmemory 后按淘汰策略处理:noeviction 直接报错;allkeys-lru / allkeys-lfu 在所有键里淘汰;volatile-* 只淘汰设置了过期时间的键;volatile-ttl 优先淘汰剩余时间短的。

LRU 是近似实现,靠采样若干键选最久未用的,maxmemory-samples 越大越精确。LFU 更适合「曾经热、现在冷」的访问分布,但计数器需要衰减。

5. 缓存穿透、击穿、雪崩分别怎么解决?

穿透是查不存在的数据,请求每次都打到数据库。可以用布隆过滤器提前拦截,或缓存空值并设置较短过期时间;接口层还要做参数校验和限流。

击穿是某个热点 key 过期瞬间,大量请求同时回源。常见做法是互斥重建(只放一个请求查库,其他等待)或逻辑过期(值不过期,异步刷新),前者简单但会阻塞,后者体验好但会读到旧值。

雪崩是大量 key 同时失效,或 Redis 整体不可用。解决思路包括给过期时间加随机抖动、做多级缓存、集群化部署、加熔断降级和限流,以及准备本地兜底数据。

三者容易混:穿透是「数据不存在」,击穿是「单个热点失效」,雪崩是「大面积同时失效或整体故障」。

6. 如何保证缓存和数据库的一致性?

常用的是旁路缓存:读时先查缓存,未命中查库并回填;写时先更新数据库,再删除缓存。

为什么是删除而不是更新缓存:并发更新时,两个写请求的缓存更新顺序可能与数据库提交顺序相反,留下脏数据;删除让下一次读自然回填。

即使先更新库再删缓存,仍有极小概率不一致:读请求在删除前拿到旧值,删除后又把旧值写回。可以用延迟双删、给缓存加版本号,或订阅 binlog 异步删除来收敛。

要接受「最终一致」而不是「强一致」。对一致性要求极高的场景,应让缓存只承载可容忍短暂陈旧的数据,或在读路径上做版本校验。

7. Redis 分布式锁怎么做?有哪些坑?

基本写法是 SET lock_key unique_value NX PX 30000:NX 保证互斥,PX 设置过期防止死锁,unique_value 标识持有者。

释放时必须用 Lua 先比较 value 再删除,避免误删别人的锁;业务执行时间可能超过锁租期,需要看门狗续期,Redisson 就封装了这套逻辑。

仍有两个本质问题:主从异步复制下,主节点加锁成功后宕机,锁可能丢失;锁过期与业务完成之间存在时间竞态。前者需要接受「Redis 锁不是强一致」或改用共识系统,后者可以用 fencing token 让下游拒绝旧持有者的写。

所以 Redis 锁适合「防重复、降并发」这类场景,不适合转账扣款这种必须绝对互斥的核心链路。Redlock 也有广泛争议,不要把「多数节点加锁成功」等同于强一致。

8. 主从、哨兵、Cluster 有什么区别?热 key 和大 key 怎么处理?

主从解决读扩展和备份,复制是异步的,主节点故障需要人工切换。哨兵在主从之上做监控和自动故障转移,靠多数派投票选主,仍是一个主节点写。Cluster 做分片,16384 个槽分布在多个主节点上,客户端按 CRC16(key) mod 16384 路由,遇到 MOVED / ASK 重定向;多 key 操作要求落在同一槽,可用 hash tag 强制同槽。

热 key 指单个 key 的访问量过大,会打满某个节点。可以做本地缓存、把 key 拆成多个副本分散读、增加只读副本,或在代理层做请求合并。

大 key 指 value 过大或集合元素过多,会造成网络阻塞、删除卡顿和迁移困难。应拆分结构、分页读取、用 UNLINK 删除,并通过 redis-cli --bigkeys 和慢日志定期发现。

易错点与自测

  • 单线程指的是命令执行,不是整个进程只有一个线程。
  • AOF everysec 仍可能丢约一秒,Redis 不能替代持久化数据库。
  • 缓存一致性只能做到最终一致,方案要配套业务容忍度和监控。

自测:① 说明 ZSet 在小数据量和大数据量下的编码差异;② 比较互斥重建与逻辑过期在击穿场景的优劣;③ 写出分布式锁的加锁与释放命令,并解释 value 为什么必须唯一。

参考资料

评论

为保障内容质量,评论需站长审核通过后才会展示;提交后没立刻看到属正常,请勿重复提交。

还没有评论,来抢沙发~