综合实战:电商首页缓存设计、高并发秒杀系统缓存策略、缓存一致性解决方案(先更新DB再删缓存)、缓存监控平台搭建(Redis Exporter + Prometheus + Grafana)、面试高频考点与话术

好,终于到了咱们这个课程最硬核的章节了。前面讲了那么多理论,什么穿透、击穿、雪崩,说白了都是纸上谈兵。今天咱们直接上战场,把电商首页、秒杀系统、缓存一致性、监控平台这些真刀真枪的东西串起来。我保证,你学完这一章,面试官问你缓存问题,你不仅能答上来,还能跟他聊出花来。

一、电商首页缓存设计:别让数据库扛下所有

电商首页,那是流量的入口。双十一的时候,首页的QPS能冲到几十万甚至上百万。你想想看,如果每个请求都去查数据库,那数据库不就直接跪了?

我个人习惯的做法是:多级缓存 + 分层过期

  • 一级缓存(本地缓存):用Caffeine或者Guava Cache,放在应用进程内。热点数据比如导航栏、分类列表,TTL设个30秒。为什么?因为首页数据变化没那么快,30秒内用户看到一样的也没问题。
  • 二级缓存(Redis):存全量的首页数据,比如轮播图、推荐商品、楼层数据。TTL设个5分钟。这里要注意,不要用统一的过期时间,否则雪崩等着你。
  • 三级兜底(数据库):如果Redis也挂了,直接查DB。但这种情况极少发生。

避坑指南:我曾经在一个项目中,首页所有缓存都设了相同的过期时间,结果凌晨3点缓存集体失效,数据库瞬间被打爆。嗯,从那以后,我养成了给每个缓存key加随机偏移量的习惯。

// 伪代码示例:分层缓存读取
public HomePageData getHomePage() {
    // 1. 查本地缓存
    HomePageData data = localCache.get("home_page");
    if (data != null) return data;
    
    // 2. 查Redis
    data = redis.get("home_page");
    if (data != null) {
        localCache.put("home_page", data, 30, TimeUnit.SECONDS);
        return data;
    }
    
    // 3. 查数据库
    data = db.queryHomePage();
    redis.set("home_page", data, 300 + random.nextInt(60), TimeUnit.SECONDS);
    localCache.put("home_page", data, 30, TimeUnit.SECONDS);
    return data;
}

二、高并发秒杀系统缓存策略:库存就是命

秒杀系统,说白了就是抢库存。我参与过几个秒杀项目,最核心的一点就是:库存数据必须提前预热到Redis,而且操作要原子化。

为什么?因为秒杀场景下,数据库的行锁根本扛不住。你想想看,10万人抢100件商品,如果用数据库扣库存,那行锁竞争能把数据库拖死。

我的方案是:Redis Lua脚本 + 预扣库存

  1. 预热:活动开始前,把商品库存从DB同步到Redis。
  2. 扣减:用Lua脚本保证原子性,先判断库存是否足够,再扣减。
  3. 异步落库:扣减成功后,发送MQ消息,异步更新数据库。

小技巧:Lua脚本里别忘了加一个「防超卖」的判断。我见过有人直接用DECR命令,结果库存扣成负数了,那画面太美不敢看。

// Lua脚本:原子扣库存
local key = KEYS[1]  // 库存key
local stock = redis.call('GET', key)
if not stock or tonumber(stock) <= 0 then
    return 0  // 库存不足
end
redis.call('DECR', key)
return 1  // 扣减成功

三、缓存一致性解决方案:先更新DB再删缓存

缓存一致性,这是面试必问的题。我面试别人的时候,十个人里有八个会说「先更新DB再删缓存」,但能说清楚为什么的,不超过两个。

为什么选「先更新DB再删缓存」?说白了,就是容忍短暂的不一致,但保证最终一致

  • 先删缓存再更新DB:并发场景下,A线程删了缓存,B线程读到了旧数据并写回缓存,然后A才更新DB。结果缓存里永远是旧数据。这问题无解。
  • 先更新DB再删缓存:A更新DB,然后删缓存。这时候如果有B线程读缓存,可能读到旧数据(因为缓存还没删),但等A删完缓存后,B的下一次读取就会从DB拉新数据。不一致窗口期很短。

注意:这个方案有个坑——如果删缓存失败了怎么办?我建议加一个重试机制,比如用MQ异步重试,或者用Canal监听binlog来补偿。我曾经在一个项目中,就因为删缓存失败,导致用户看到的数据和数据库不一致了整整10分钟,被老板骂惨了。

// 伪代码:先更新DB,再删缓存
public void updateProduct(Product product) {
    // 1. 更新数据库
    db.update(product);
    
    // 2. 删除缓存(带重试)
    boolean success = redis.del("product:" + product.getId());
    if (!success) {
        // 发送MQ,异步重试
        mq.send("cache_del", product.getId());
    }
}

四、缓存监控平台搭建:Redis Exporter + Prometheus + Grafana

监控这事,很多人觉得不重要。但我告诉你,没有监控的缓存系统,就像闭着眼睛开车。你根本不知道Redis什么时候内存爆了,什么时候慢查询多了。

我的标配是:Redis Exporter + Prometheus + Grafana。这套组合拳打下来,Redis的健康状况一目了然。

组件 作用 关键指标
Redis Exporter 采集Redis指标,暴露给Prometheus 内存使用率、命中率、慢查询数
Prometheus 存储时序数据,提供查询 每秒请求数、延迟分布
Grafana 可视化展示,告警 仪表盘、阈值告警

搭建步骤其实很简单:

  1. 启动Redis Exporter,连接你的Redis实例。
  2. 在Prometheus配置里添加Exporter的地址。
  3. 在Grafana导入Redis的官方仪表盘模板(ID: 763)。

个人经验:我习惯在Grafana里设置三个告警:内存使用率超过80%告警、命中率低于90%告警、慢查询超过10个/分钟告警。这三个指标能覆盖90%的Redis问题。

五、面试高频考点与话术

最后,咱们聊聊面试。这部分我直接给你话术,你背下来,面试时直接甩出去。

  • 问:缓存穿透怎么解决?
    答:「我个人习惯用布隆过滤器+缓存空对象。布隆过滤器能挡住大部分非法key,空对象缓存能防止恶意攻击。我在项目中遇到过,一个爬虫疯狂请求不存在的商品ID,布隆过滤器直接拦截了99%的请求,剩下的用空对象缓存兜底,数据库完全没压力。」
  • 问:缓存击穿怎么解决?
    答:「说白了就是热点key过期。我建议用互斥锁,或者让热点key永不过期,后台异步更新。我记得有一次双十一,一个爆款商品的缓存过期了,瞬间几千个请求打到数据库。还好我提前加了分布式锁,只有一个线程去查DB,其他线程等待,数据库扛住了。」
  • 问:缓存雪崩怎么解决?
    答:「核心思路是避免大量key同时过期。我一般会给过期时间加随机值,比如300秒加一个0-60秒的随机数。另外,多级缓存也能有效缓解雪崩。嗯,这里要注意,如果Redis集群整体挂了,那就得靠本地缓存和限流来保命了。」

面试加分项:面试官问你缓存一致性时,你可以主动提「我了解过Canal + MQ的方案,通过监听binlog来异步同步缓存,能实现最终一致性」。这一句话,就能让面试官觉得你不仅会用,还懂原理。

好了,这一章的内容就到这里。缓存这东西,说白了就是空间换时间。但怎么换、换多少、换完之后怎么保证不出问题,这才是真功夫。希望今天的内容能帮你少踩几个坑。