Skip to content

10. 实战:缓存架构设计与踩坑 ​


❓ 面试实战:你们系统的商品详情页是怎么设计的?缓存是怎么用的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 我们在商品详情页采用了多级缓存架构(Nginx 代理层本地缓存 + 应用层 JVM 本地缓存 + Redis 分布式缓存),并结合了异步刷新机制,最大程度抗住了高并发读取,保护了底层 MySQL。

📝 详细落地实战解析(面试高分答题模板): 面试官其实不想听你背诵“我用了 String 类型 set/get”,他想听的是你的架构演进过程。

  1. 第一阶段(裸奔期):直接查 MySQL

    • 痛点:商品详情页 QPS 稍微一高,数据库连接池就打满,响应时间从 10ms 飙升到 1000ms。
    • 演进:引入 Redis 分布式缓存。
  2. 第二阶段(Redis 集中缓存期):读写分离,Redis 挡在前面

    • 架构:客户端 -> 应用服务器 -> Redis -> MySQL。
    • 实现:先查 Redis(把商品详情整个转成 JSON 存进去,或者用 Hash 存字段),命中直接返回;不命中去查 MySQL,查出来放入 Redis 并设置 24 小时随机过期时间(防雪崩)。
    • 痛点:双十一大促,某几个爆款商品(热点 Key)的请求量极大,导致某一台 Redis 节点的网卡带宽瞬间被打满(Redis 单节点并发再高,也扛不住千兆网卡的物理上限)。
  3. 第三阶段(多级缓存终极架构期):JVM 本地缓存兜底

    • 架构:客户端 -> Nginx -> 应用服务器 (Caffeine/Guava 本地缓存) -> Redis 集群 -> MySQL。
    • 演进策略:
      • 在应用服务器内部引入 Caffeine(号称最快本地缓存)。
      • 请求打到应用服务器,先查本地 Caffeine 缓存(设置 10 秒极短过期时间,甚至只存几十个顶级爆款的 Key)。
      • 本地缓存没有命中,再去查 Redis 集群。
      • Redis 集群没命中,再去 MySQL(结合分布式锁防击穿)。
    • 核心优势:本地缓存直接在内存中读取,不需要跨网络通信(没有 Socket 序列化开销和网络延迟),彻底解决了 Redis 单点热点网卡被打满的瓶颈。

❓ 面试实战:既然用了多级缓存,那缓存和数据库的数据一致性你们是怎么保证的?【中高级】 ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 在并发不是极端的场景下,我们采用业界标准的 “Cache Aside 旁路缓存模式”:先更新数据库,再删除缓存。如果要求极高的最终一致性,我们会引入 Canal 监听 MySQL Binlog 异步删除缓存。

📝 详细踩坑分析与方案对比: 这也是面试必考连环大坑,回答一定要逻辑严密。

  1. 方案一:先更新缓存,再更新数据库(绝对不用❌)

    • 坑点:如果更新缓存成功,更新数据库失败抛了异常。那么缓存里是新值,数据库里是旧值,这就叫永久脏数据,绝对不行。
  2. 方案二:先更新数据库,再更新缓存(依然不用❌)

    • 坑点 1:并发写的时候,线程 A 先更新库为 100,线程 B 更新库为 200;然后线程 B 执行得快,先把缓存更新为 200,随后线程 A 才慢吞吞地把缓存更新成了 100。结果:库里是 200,缓存是 100(脏数据)。
    • 坑点 2:每次改库都要同步去计算/拼装复杂的 JSON 写入缓存,非常耗费性能。如果是“写多读少”的场景,很多写入的缓存根本没人读就被过期淘汰了,纯属浪费 CPU。
  3. 方案三:先删除缓存,再更新数据库(有可能出大坑⚠️)

    • 坑点(经典高并发漏洞):线程 A 删除缓存,正准备去更新库;就在这零点几秒的空隙,线程 B 跑来查询。B 一看没缓存,马上跑去查库(此时库里还是旧值),把旧值查出来塞进了缓存。然后线程 A 才终于把库更新成了新值。结果:库里是新值,缓存里被 B 塞入了永久的旧值。
    • 补救方案:延时双删。A 更新完库后,休眠 500 毫秒,再删一次缓存(确保把 B 刚才塞入的旧脏数据给干掉)。但这个休眠时间很难估算,且影响接口响应速度。
  4. 方案四(终极推荐):先更新数据库,再删除缓存(业界标配✅)

    • 原理:不管那么多,先把权威数据的数据库改对。然后再把旧缓存干掉。下一个来查询的读请求,自然会去数据库拉最新数据并重新载入缓存。
    • 理论上唯一的极小概率漏洞:读线程刚好在缓存失效瞬间去读库(拿到了旧值),此时写线程飞速更新了库并删除了缓存;然后读线程才慢吞吞地把旧值写回缓存。但实际上数据库的写操作远比读操作慢,这种情况在生产环境中极其罕见。

🌟 面试加分项(架构拔高): "面试官您好,其实哪怕是『先更新数据库,再删除缓存』,也有一个小缺陷:如果在删除缓存那一步,网络突然抖动,删除失败了怎么办? 缓存里不就一直残留旧数据了吗? 我们在生产环境的终极解法是:引入消息队列(MQ)或中间件(Canal)。

  • 我们根本不在业务代码里写任何操作 Redis 的代码。只管安心更新 MySQL。
  • 额外部署一个 Canal 伪装成从节点去监听 MySQL 的 Binlog 日志。
  • 一旦发现有写操作日志,Canal 就把这条变更消息扔进 MQ 里。
  • 后台起一个消费者微服务,专门从 MQ 里消费消息,去 Redis 执行彻底删除(或精准更新)对应 Key 的操作。如果删除失败,MQ 强大的重试机制能保证最后一定能删成功,实现最终一致性。"