Redis 八股文 —— 缓存

· 10 min read ·

内存淘汰

  • 先进先出 FIFO (First In First Out):淘汰最早进入缓存的数据。实现简单,但不考虑访问频率,可能淘汰热点数据。
  • 最近最少使用 LRU (Least Recently Used):淘汰最长时间未使用的数据。经典实现是「哈希表 + 双向链表」,但 Redis 采用了近似 LRU 算法以节省内存。
  • 最不经常使用 LFU (Least Frequently Used):淘汰访问频率最低的数据 (Redis 4.0+ 支持)。能更好地保留长期热点数据,避免突发流量导致的热点被淘汰。

过期删除

  • 惰性删除:每次访问键时检查是否过期,若过期则立即删除。
    • 优点:CPU 开销极小,只在访问时才检查。
    • 缺点:如果过期键不再被访问,会永久占用内存(“僵尸键”)。
  • 定期删除:每隔固定时间(默认 100ms)随机抽取部分过期键进行清理。
    • 优点:主动清理内存。
    • 缺点:两次扫描之间,过期键仍可能被访问 → 可能返回已过期但尚未被删除的数据

概念区分:过期删除 vs 内存淘汰

特性过期键删除策略内存淘汰策略
核心目标清理已到过期时间的键内存达到上限时主动淘汰键
触发条件键设置了过期时间且已到期已用内存 ≥ maxmemory 配置值
处理范围仅处理设置了过期时间的键可配置为全量键或仅过期键

一句话总结:过期删除是“时间到了就删”,内存淘汰是“内存满了必须删”。


缓存一致性

缓存一致性是指:数据库中的数据与缓存中的数据保持一致。当数据发生变更时,如何保证缓存不会返回过期的旧数据。

为什么会有缓存一致性问题?

产生原因

  1. 写数据库和写缓存是两个独立的操作,不是原子的。
  2. 高并发下,操作顺序可能被打乱。

更新策略

Cache-Aside(旁路缓存)- 最常用

  • :缓存未命中则读DB并回填。
  • :先更新DB,再删除缓存。

为什么是删除缓存,而不是更新缓存?

方式问题
更新缓存可能更新一个复杂计算后的值;可能多次写无用缓存;并发更新可能覆盖
删除缓存简单、安全;下次读时再重新加载最新值

Read-Through / Write-Through

应用程序将缓存作为主要数据源,由缓存代理所有读写操作。逻辑简单,但缓存层需要实现完整代理。

Write-Behind

只更新缓存,然后异步写DB(例如将更新步骤放到消息队列中)。写入吞吐量极高,但缓存故障可能导致数据丢失。

并发问题与解决方案

经典问题:先删缓存还是先写DB?

顺序问题
先删缓存,再写DB写DB期间,读请求发现缓存未命中,读到旧数据并回填缓存 → 缓存变成脏数据
先写DB,再删缓存写DB后、删缓存前,读请求读到旧缓存 → 短暂不一致

结论:先写DB再删缓存相对更安全,因为“读到旧缓存”的时间窗口更短。

并发写问题(延迟双删策略)

在高并发 + MySQL 主从读写分离的场景下,即使“先写DB,再删缓存”,也可能出现问题:

请求A(写主库)                请求B(读从库)
   │                            │
   ├─ 写主库成功                 │
   ├─ 删除缓存(成功)            │
   │                            ├─ 读缓存(未命中)
   │                            ├─ 读从库(主从延迟,读到旧数据) ❌
   │                            └─ 回填缓存(旧数据) ❌ 

问题根因

  1. 主从复制存在延迟(毫秒到秒级)。
  2. 请求A删除缓存后,请求B因为主从延迟读到从库的旧数据。
  3. 请求B将旧数据回填到缓存,导致缓存变成脏数据。

解决方案:延迟双删

核心思想:删除两次缓存,第二次删除延迟执行,等待主从同步完成。

  1. 第一次删除:更新DB后立即删除缓存。
  2. 延迟等待:等待一段时间(略大于主从复制的延迟时间)。
  3. 第二次删除:再次删除缓存,以清除在此期间可能被回填的脏数据。

缓存击穿

定义:查询一个在数据库中存在,但在缓存中已过期的数据。

带来的问题某个热点数据过期的一瞬间,大量并发请求同时穿透缓存,直接打到数据库,造成数据库压力骤增。

与其他概念的区分

  • 缓存穿透:查询一个不存在的数据。
  • 缓存雪崩:大量缓存同时过期。

解决方案

  • 互斥锁:只让一个线程去加载数据,其他线程等待。
  • 设置热点数据永不过期:由后台异步线程主动更新缓存。
  • 逻辑过期:缓存中不设置物理过期时间,而是存储一个逻辑过期字段,当发现逻辑过期时,返回旧数据并异步更新缓存。

缓存穿透

定义:查询一个数据库和缓存中都不存在的数据。

解决方案

  1. 在业务层提前拦截非法请求:如ID格式校验、ID范围校验。
  2. 缓存空对象:即使查询结果为空,也把这个“空结果”缓存起来(设置较短的过期时间)。
  3. 布隆过滤器:用一个极省内存的结构,快速判断一个 key 一定不存在

布隆过滤器

布隆过滤器是一种概率型数据结构,它用一种非常省内存的方式,告诉你“一个元素一定不存在”或者“一个元素可能存在”。

核心原理:位数组 + 多个哈希函数

  • 底层是一个位数组(bit array),每个位置初始为0。
  • 添加:用 k个 独立的哈希函数计算k个位置,把这些位置都设为1。
  • 查询:用同样的k个哈希函数计算位置。
    • 所有位置都是1 → 元素可能存在(可能因哈希冲突被其他元素设置)。
    • 有任何位置是0 → 元素一定不存在

特点:不会漏掉存在的元素,但可能误判不存在的元素为“可能存在”。


缓存雪崩

定义大量缓存集中在同一时间段过期,或者 Redis 服务宕机,导致所有请求直接打到数据库,造成数据库压力暴增甚至崩溃。

与其他概念的区分

  • 缓存击穿:单个热点数据过期(点问题)。
  • 缓存雪崩:大量数据同时过期(面问题)。

解决方案

  • 过期时间打散:给缓存过期时间加上随机偏移量,避免同时过期。
  • 设置热点数据永不过期:由后台异步线程主动更新缓存。
  • 缓存预热:在系统启动或大促前,提前把热点数据加载到缓存。
  • Redis 高可用:使用主从+哨兵、Redis Cluster 等方案,防止 Redis 宕机。
  • 多级缓存:本地缓存(如 Caffeine)+ Redis 分布式缓存,构建更稳固的防线。

总结对比

概念问题描述数据状态解决方案核心
缓存穿透查询不存在的数据缓存和DB都没有拦截不存在请求(缓存空值/布隆过滤器)
缓存击穿单个热点数据过期瞬间缓存无,DB有控制并发重建(互斥锁/永不过期)
缓存雪崩大量缓存同时过期/Redis宕机批量数据同时失效分散失效时间(随机偏移/预热/高可用)