Redis 缓存机制原理:从数据结构到淘汰策略

作者:杨帅      发布日期:2026.07.10

DATABASE · REDIS · 原理

Redis 缓存机制原理:从数据结构到淘汰策略

REDIS 内存模型 db0 主哈希表 dict key → value 过期字典 expires key → 过期时间戳 两张表通过相同的 key 关联

很多人对 Redis 缓存的理解停留在"设个过期时间的键值对",但真正决定线上系统稳定性的,是它背后一整套过期删除、内存淘汰、读写更新和一致性保障的机制。这篇文章从原理层面梳理 Redis 缓存是怎么运转的。

1. 数据是怎么"过期"的:惰性删除 + 定期删除

Redis 并不会在 EXPIRE 时间一到就立刻把 key 删掉,而是维护一张独立的 expires 字典,专门记录哪些 key 设置了过期时间以及具体的过期时间戳。真正的删除动作由两种机制配合完成。

惰性删除 客户端 GET key 检查 expires 字典 已过期 → 删除并返回 nil 优点:不用扫全库,省 CPU 定期删除 后台定时任务 (默认 10Hz) 随机抽取一批带过期时间的 key 删除其中已过期的,比例过高则重复 优点:避免大量冷 key 常驻内存
惰性删除负责"访问时兜底",定期删除负责"主动清扫"

只靠惰性删除会导致一批设了过期时间但再也不会被访问的 key 一直占着内存不释放;只靠定期删除又可能因为抽样和 CPU 时间片限制而不够及时。两者搭配,再加上内存淘汰机制兜底,才构成 Redis 完整的过期回收体系。

如果开启了主从复制,从节点自身不会主动删除过期 key,而是等待主节点删除后,通过 DEL 命令同步过来——这是为了保证主从数据的一致性。

2. 内存不够用时怎么办:淘汰策略

当 Redis 内存占用达到 maxmemory 上限时,新的写入会触发淘汰机制,具体行为由 maxmemory-policy 参数决定,一共有 8 种策略可选。

策略作用范围淘汰依据
noeviction不淘汰内存满后写入直接报错
allkeys-lru全体 key最近最少使用
volatile-lru设置了过期时间的 key最近最少使用
allkeys-lfu全体 key访问频率最低
volatile-lfu设置了过期时间的 key访问频率最低
allkeys-random全体 key随机
volatile-random设置了过期时间的 key随机
volatile-ttl设置了过期时间的 key剩余存活时间最短
近似 LRU:Redis 不维护全局链表,而是随机采样打分 全部 key(内存池) 随机采样 N 个 (默认5) 比较空闲时间/访问频率 淘汰其中最久未用的一个 样本数越大越接近真实 LRU 但更耗 CPU,需要权衡
Redis 的 LRU/LFU 都是近似算法,用采样换取性能

生产环境最常用的是 volatile-lruallkeys-lru:前者只淘汰设置了过期时间的 key,保证那些永久性的重要数据不会被误删;后者则把 Redis 完全当缓存用,所有 key 都参与淘汰竞争。选择哪种取决于 Redis 实例里是否混用了缓存数据和持久化业务数据。

3. 缓存更新的四种模式

业务代码里到底该先更新数据库还是先操作缓存、该主动写缓存还是被动加载,这几种常见模式各有取舍。

Cache-Aside(旁路缓存,最常见) 应用代码 1.写库 数据库 2.删缓存 Redis Read/Write-Through(缓存代理读写,应用只对缓存说话) 应用代码 Redis 缓存代理 数据库 Write-Behind(异步落盘,先写缓存,批量/延迟刷库) 应用代码 立即返回 Redis 异步批量 数据库
三种典型的缓存读写模式,应用侧承担的职责各不相同

Cache-Aside 是绝大多数业务系统的默认选择:应用代码自己负责查缓存、查数据库、写缓存,逻辑简单,出问题排查也直观。Read/Write-Through 把缓存和存储的读写都封装在缓存层背后,应用只跟缓存打交道,一致性由缓存组件保证,但需要额外的中间层支持。Write-Behind 追求极致的写入性能,先写缓存立即返回,再异步批量刷库,吞吐量最高但有丢数据的风险,通常用在日志、计数这类可以容忍少量丢失的场景。

4. 缓存更新顺序:为什么"先删缓存"更安全

Cache-Aside 模式下,更新数据时是"先更新数据库、再删除缓存"还是"先删除缓存、再更新数据库"?这两种顺序都存在并发问题,业界的共识是先更新数据库,再删除缓存,并在必要时配合延迟双删来兜底。

并发场景:写请求与读请求交叉执行时可能产生的脏数据 写请求 T 更新数据库 删除缓存 读请求 R 缓存未命中 查到旧值 写回旧值⚠ 这种极端窗口期概率很低,但高并发下仍可能出现。 兜底方案:删除缓存后延迟几百毫秒再删一次(延迟双删)。
先更新库再删缓存,配合延迟双删应对极端并发窗口

如果反过来"先删缓存、再更新数据库",在删除缓存和更新数据库之间的窗口期,一旦有读请求进来,就会把数据库里的旧值重新加载回缓存,而且这个旧值会一直留在缓存里直到下一次过期或更新,影响时间比前一种方案更长。所以工程实践中更推荐"先库后缓存"的顺序。对一致性要求更高的场景,还可以通过订阅数据库的 binlog(如借助 Canal)来异步同步缓存,把"删缓存"这个动作从业务代码里解耦出来。

5. 三大经典问题:穿透、击穿、雪崩

这三个问题的本质都是"缓存没能挡住请求",但触发原因和应对方式不同。

缓存穿透 查询根本不存在的数据 缓存和数据库都没有 请求每次都打到数据库 ✓ 空值也缓存(短TTL) ✓ 布隆过滤器提前拦截 ✓ 参数合法性校验 缓存击穿 单个热点 key 突然过期 大量并发请求同时涌入 瞬间压垮数据库 ✓ 互斥锁重建缓存 ✓ 逻辑过期,不物理删除 ✓ 热点key预先续期 缓存雪崩 大批 key 同时到期 或 Redis 实例宕机 请求集体压向数据库 ✓ 过期时间加随机值 ✓ 集群高可用部署 ✓ 限流+熔断+降级
三种问题触发条件不同,但都需要在缓存层之外再加一道防线

6. 热点 key:单个 key 也能压垮整个集群

即使做了分片,某个爆款商品或热搜话题对应的 key 也可能被极高频访问,请求全部落在同一个分片节点上,造成该节点 CPU 或带宽打满,而其他节点却很空闲。常见应对思路是识别出热点 key 后,把它复制成多份(如 hotkey:1hotkey:2 ... 加随机后缀),读请求随机打到某一份上分摊压力;也可以在应用层加一层进程内本地缓存(如 Caffeine、ristretto),对访问频率极高的数据做二级兜底,减少打到 Redis 的次数。

小结

Redis 缓存机制的核心可以归纳为四层:用主哈希表和过期字典配合惰性删除、定期删除来管理数据的生命周期;用近似 LRU/LFU 算法在内存达到上限时决定淘汰谁;用 Cache-Aside 等模式约定应用代码和缓存、数据库之间的读写顺序;再用穿透、击穿、雪崩的专项应对方案和热点 key 的分摊手段,补齐缓存机制在极端场景下的短板。理解这套原理,才能在设计缓存方案时提前预判风险,而不是等线上出问题了再去救火。