综合实战:电商首页缓存设计、高并发秒杀系统缓存策略、缓存一致性解决方案(先更新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脚本 + 预扣库存。
- 预热:活动开始前,把商品库存从DB同步到Redis。
- 扣减:用Lua脚本保证原子性,先判断库存是否足够,再扣减。
- 异步落库:扣减成功后,发送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 | 可视化展示,告警 | 仪表盘、阈值告警 |
搭建步骤其实很简单:
- 启动Redis Exporter,连接你的Redis实例。
- 在Prometheus配置里添加Exporter的地址。
- 在Grafana导入Redis的官方仪表盘模板(ID: 763)。
个人经验:我习惯在Grafana里设置三个告警:内存使用率超过80%告警、命中率低于90%告警、慢查询超过10个/分钟告警。这三个指标能覆盖90%的Redis问题。
五、面试高频考点与话术
最后,咱们聊聊面试。这部分我直接给你话术,你背下来,面试时直接甩出去。
- 问:缓存穿透怎么解决?
答:「我个人习惯用布隆过滤器+缓存空对象。布隆过滤器能挡住大部分非法key,空对象缓存能防止恶意攻击。我在项目中遇到过,一个爬虫疯狂请求不存在的商品ID,布隆过滤器直接拦截了99%的请求,剩下的用空对象缓存兜底,数据库完全没压力。」 - 问:缓存击穿怎么解决?
答:「说白了就是热点key过期。我建议用互斥锁,或者让热点key永不过期,后台异步更新。我记得有一次双十一,一个爆款商品的缓存过期了,瞬间几千个请求打到数据库。还好我提前加了分布式锁,只有一个线程去查DB,其他线程等待,数据库扛住了。」 - 问:缓存雪崩怎么解决?
答:「核心思路是避免大量key同时过期。我一般会给过期时间加随机值,比如300秒加一个0-60秒的随机数。另外,多级缓存也能有效缓解雪崩。嗯,这里要注意,如果Redis集群整体挂了,那就得靠本地缓存和限流来保命了。」
面试加分项:面试官问你缓存一致性时,你可以主动提「我了解过Canal + MQ的方案,通过监听binlog来异步同步缓存,能实现最终一致性」。这一句话,就能让面试官觉得你不仅会用,还懂原理。
好了,这一章的内容就到这里。缓存这东西,说白了就是空间换时间。但怎么换、换多少、换完之后怎么保证不出问题,这才是真功夫。希望今天的内容能帮你少踩几个坑。