Redis 内存缓存,面试补充

缓存不一致问题:并发窗口的"定时炸弹"

一、问题场景

并发窗口的"定时炸弹"(价:10 → 20):写请求将价格从 10 改为 20,采用 先删缓存、再更新数据库 的策略。

步骤 请求 操作 说明
1️⃣ 写请求(改 20) 删缓存 此时数据库仍是 10
2️⃣ 读请求(极速响应) 缓存空,查库拿到 10 恰好落在删除后的空窗期
3️⃣ 读请求 旧值 10 回填进缓存 污染发生
4️⃣ 写请求 更新数据库 = 20 但缓存里已是脏数据

⚠️ 1️⃣ 和 4️⃣ 之间存在 致命微秒级缝隙,读请求恰好插入其中。

结论

💥 脏数据 10 会一直赖在缓存里,产生主动的持续污染!

  • 后续读请求会一直命中缓存中的旧值 10
  • 直到缓存过期或再次写操作才可能修复

二、解决方案

方案一:先更新数据库,再删缓存(Cache Aside)✅ 推荐

适合普通场景下、并发不高的业务。

把顺序反过来:

步骤 操作 说明
1️⃣ 写请求 更新数据库 = 20
2️⃣ 写请求 删除缓存
3️⃣ 读请求 缓存未命中 → 查库 → 回填 20

为什么有效:即使删缓存前有读请求拿到旧值,它的回填发生在删缓存之后(读+回填比"更新库+删库"慢的概率极低),脏数据几乎不会留存。

这是业界最通用的方案(Facebook、各大厂默认),配合下面的兜底措施即可。

残余风险:读请求"查库(10) → 回填"若极端卡顿,晚于写请求"更新库(20) → 删缓存",仍可能回填旧值。概率极低,可用兜底方案覆盖。


方案二:延迟双删

针对"先删缓存"场景的补救:

text 复制代码
1. 删除缓存
2. 更新数据库
3. 睡眠 N 毫秒(覆盖一次读请求的耗时,如 500ms)
4. 再次删除缓存   ← 把并发窗口期被回填的脏数据清掉
  • 缺点:延迟时间靠估算,且降低吞吐(睡眠)
  • 适合:无法改造为 Cache Aside 的存量代码

方案三:订阅 Binlog 异步删缓存(最终一致性)✅ 生产常用

text 复制代码
更新数据库 → Canal/Debezium 订阅 binlog → 发送 MQ → 消费者删除缓存

实现逻辑:业务代码只负责更新 MySQL。通过中间件(如 Canal、Debezium)伪装成 MySQL 从库,监听 Binlog 日志。当解析到数据变更时,异步触发 Redis 的更新或删除操作。

优势:与业务逻辑完全解耦,保证了严格的顺序性,支持全量/增量同步,架构扩展性强。

劣势:架构复杂度较高,同步存在一定延迟,且运维成本较高。

  • 业务代码与缓存删除解耦
  • 可通过 MQ 重试保证删除成功
  • 天然具备"再次删除"的兜底能力

方案四:加分布式锁(强一致场景)

写请求对同一 key 加锁,读请求发现锁存在时:

  • 等待,或
  • 直接读库、跳过回填缓存

代价是性能下降,仅在强一致要求(如库存扣减)时使用。


方案五:兜底措施(无论选哪种都建议加上)

措施 说明
设置过期时间 TTL 即使脏了也能自愈,比如 30s~5min
删除失败重试 删缓存失败时发 MQ / 记录重试表
写入带版本号 回填时校验版本,旧版本不回填

三、总结选型

text 复制代码
一般业务        →  先更新库,再删缓存 + TTL          (简单够用)
高一致性要求    →  Cache Aside + Binlog 异步删 + 重试
强一致(库存等)→  分布式锁 / 串行化
不能改代码      →  延迟双删

核心原则:让"删缓存"这个动作尽可能晚发生、且有机会重试,再靠 TTL 兜底,脏数据的窗口就能被压缩到几乎为零。


Redis 内存淘汰策略

Redis 内存淘汰策略是指当 Redis 的内存使用达到上限时,如何决定清理哪些数据以保证新数据的存入。Redis 提供了多种内存淘汰策略,每种策略适用于不同的场景和需求。


模拟面试:Redis 淘汰策略的拷打

以下是一个模拟的面试场景,面试官逐步深入提问,答案详细且结构化,涵盖技术细节和实际应用。

面试官:Redis 的内存淘汰策略有哪些?分别适用于什么场景?

候选人: Redis 提供了八种淘汰策略,分为两类:针对所有键(allkeys)和仅针对设置了过期时间的键(volatile)。具体如下:

  • noeviction:不淘汰,内存满时拒绝写入,适合数据不可丢失的场景,如金融数据存储。
  • allkeys-lru:对所有键使用 LRU 算法,适合热点数据频繁访问的缓存系统。
  • allkeys-lfu:对所有键使用 LFU 算法,适合访问频率差异大的场景,如推荐系统。
  • allkeys-random:随机淘汰所有键,适合无明显访问模式的测试环境。
  • volatile-lru:对设置了 TTL 的键使用 LRU,适合混合数据场景,如临时缓存。
  • volatile-lfu:对设置了 TTL 的键使用 LFU,适合关注频率的临时数据。
  • volatile-random:随机淘汰设置了 TTL 的键,适合简单临时数据场景。
  • volatile-ttl:优先淘汰 TTL 最短的键,适合短生命周期数据。

这些策略通过 maxmemory-policy 配置,默认是 noeviction。


面试官:LRU 和 LFU 的区别是什么?Redis 是如何实现 LRU 的?

候选人:

  • LRU(Least Recently Used):根据最近访问时间淘汰,优先删除最久未访问的键。适合热点数据场景。
  • LFU(Least Frequently Used):根据访问频率淘汰,优先删除访问次数最少的键。适合长期运行、频率差异大的场景。

Redis 的 LRU 实现:Redis 并未实现严格的 LRU,而是采用近似 LRU 算法,具体步骤:

  1. 采样:从键空间中随机抽取一小部分键(默认 5 个,可通过 maxmemory-samples 配置)。
  2. 时间戳记录:每个键维护一个访问时间戳(基于 LRU_CLOCK)。
  3. 比较淘汰:比较采样键的访问时间,淘汰最旧的键。
  4. 优化:Redis 使用一个全局 LRU 时钟(精度为 1 秒),减少维护开销。

这种近似 LRU 算法在性能和精度间取得平衡,采样越多,精度越高,但开销也越大。


面试官:Fast 模式和 Slow 模式是什么?它们会影响淘汰效果吗?

候选人: Fast 模式和 Slow 模式不是淘汰策略,而是 Redis 执行淘汰算法时的运行模式,影响淘汰的效率和精度:

  • Fast 模式:采样少量数据,快速执行,适合高并发场景,但精度较低。
  • Slow 模式:采样更多数据,执行更精确的算法(如完整的 LRU/LFU 计算),适合数据价值高的场景。

影响:

  • Fast 模式可能导致次优淘汰(误删高价值数据),但响应快。
  • Slow 模式淘汰更精准,但性能开销大。

通过 maxmemory-eviction-tenacity(0 到 100)控制,0 为纯 Fast,100 为纯 Slow,中间值动态调整。

实际应用:

  • 高并发场景(如电商秒杀)用 Fast 模式。
  • 高精度场景(如推荐系统缓存)用 Slow 模式。

面试官:如果我要实现一个自定义淘汰策略,应该怎么做?

候选人: Redis 本身不支持直接自定义淘汰策略,但可以通过以下方式实现:

  1. 修改源码 :Redis 是开源的,可以修改 evict.c 中的淘汰逻辑,添加自定义算法。例如,基于键的大小、业务优先级等设计新策略。缺点:需要重新编译,维护成本高。
  2. 外部控制:使用 Redis 的 INFO 命令监控内存使用情况。通过客户端脚本定期扫描键空间,结合业务逻辑(如优先级、访问频率)执行 DEL 命令删除键。优点:无需改源码,灵活性高。缺点:需要额外开发,可能影响性能。
  3. 结合 Lua 脚本:使用 Redis 的 Lua 脚本实现简单的淘汰逻辑,通过 EVAL 命令定期运行。例如,扫描特定模式的键,基于自定义规则删除。局限性:复杂逻辑可能受 Lua 脚本性能限制。

推荐方案:

  • 小规模场景:使用外部控制 + Lua 脚本。
  • 大规模场景:修改源码并充分测试。

面试官:实际生产环境中,你会如何选择淘汰策略?

候选人: 选择淘汰策略需要综合考虑业务场景、数据特性和服务要求:

  1. 分析数据特性

    • 数据是否有明确的生命周期?如果有,优先考虑 volatile-* 策略。
    • 数据访问是否有热点?如果有,LRU 或 LFU 更适合。
  2. 评估业务需求

    • 数据丢失是否可接受?如果不可接受,选择 noeviction。
    • 性能敏感度高?选择 Fast 模式或 allkeys-random。
  3. 典型场景参考

    • 电商缓存:热点数据多,选 allkeys-lru + Fast 模式。
    • 推荐系统:频率差异大,选 allkeys-lfu + Slow 模式。
    • 临时数据:有 TTL,选 volatile-ttl 或 volatile-lru。
  4. 监控与调优

    • 使用 INFO MEMORY 监控 evicted_keysmem_used
    • 调整 maxmemory-samplesmaxmemory-eviction-tenacity 优化淘汰效果。

我的经验: 在某电商项目中,我们使用 allkeys-lru 结合 Fast 模式,配合 maxmemory-samples=10,在高并发场景下保持了低延迟,同时通过监控调整 maxmemory 避免频繁淘汰。


作者:Asthenian 链接:https://juejin.cn/post/7503454000494280742 来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐
小程故事多_801 小时前
企业AI对话记忆架构实战,告别单一Redis存储,搭建长短效协同的记忆体系
人工智能·redis·架构
蒸蒸yyyyzwd2 小时前
cpp 选手秋招学习笔记 day27
服务器·c++·面试·八股
棣廷2 小时前
初识MySQL——DQL数据查询语言详解
数据库·mysql
大模型码小白2 小时前
ChatGPT 的回答也能被结构化抓取?AI Bot Scraper 对比测评
服务器·开发语言·数据库·人工智能·spring·chatgpt
天涯柳絮2 小时前
SQL注入
数据库·sql·网络安全
API快乐传递者2 小时前
淘宝海外商品详情接口实战指南:从全球开放平台到跨境铺货的全链路方案
java·前端·数据库
tachibana22 小时前
复杂的 RAG 范式
数据库·人工智能·ai·大模型·agent
程序员清风3 小时前
聊聊怎么缓解找工作的焦虑感?
java·后端·面试
杰克尼3 小时前
同城拼车项目面试问题_01
spring·面试·职场和发展