Loading...

【面试真题拆解】十万用户并发下,Caffeine+Redis+MySQL多级缓存怎么设计?

面试官问了这么一道场景题:

“平台用了Caffeine本地缓存、Redis分布式缓存、MySQL数据库,现在有十万个用户同时访问,相当于每个用户对应一个请求,这多级缓存该怎么设计?”

多级缓存就是“分层扛压”,把十万用户的请求,从快到慢、从近到远,一层层分流,别让所有请求都直接打MySQL。

设计方案

定好每一层的职责

缓存层级 职责 放什么数据 容量/速度
Caffeine本地缓存 扛超级热点中的超级热点,大幅减少 Redis 压力 访问频率 Top10 的极致热门数据(比如首页轮播爆款、正在进行的秒杀活动) 容量小(几百MB)、速度极快(纳秒级)
Redis分布式缓存 全局通用数据,挡MySQL 全平台的业务数据(比如商品详情、用户信息、订单数据) 容量大(几十GB)、速度快(毫秒级)
MySQL数据库 数据最终兜底,保证数据不丢 所有的原始业务数据 容量无限、速度慢(秒级)

缓存预热

平台刚启动的时候,Caffeine和Redis里都是空的,如果这时候十万用户同时进来,所有请求都会直接打MySQL,直接把数据库打垮。

为了解决这个问题,考虑采用缓存预热:

平台启动前,先做两级缓存预热:

  1. 把访问频率最高的Top1000 个全量热门 Key(比如全平台热门商品、活动规则),提前从 MySQL 里查出来,全量加载到 Redis;
  2. 把其中访问频率最高的Top10 个超级热点 Key(比如首页轮播商品、正在进行的爆款活动),额外加载到每台服务器的 Caffeine 本地缓存,作为最外层的扛压层。

缓存雪崩、击穿、穿透

缓存雪崩:十万个Key同时过期

比如你给所有Key都设置了“1小时过期”,结果十万个Key在同一时间全部过期,所有请求直接打MySQL。

解决这个问题的话,就是别给所有Key都设一样的过期时间,可以设置“1小时 + 0~30分钟的随机值”,让Key的过期时间分散开,避免同时过期;

对于首页Top10商品这种超级热点数据,还可以设置成“永不过期”,只在数据更新的时候主动删除缓存。

缓存击穿:超级热门Key突然过期

比如“双11爆款商品详情”这个Key,每秒有十万个请求,结果它突然过期了,十万个请求同时打Redis,发现没有,又同时打MySQL,直接把两者都打垮。

这个问题也可以对超级热点Key不设过期时间,只在更新时主动删;

或者加Redis 分布式互斥锁

如果 Key 真的过期了,只让第一个抢到锁的请求去查 MySQL,查出来后更新缓存并释放锁;

其他没抢到锁的请求,每隔 100ms 重试一次查缓存,直到缓存更新完成,彻底避免十万请求同时打 MySQL。

缓存穿透:查根本不存在的Key

比如有人恶意攻击,每秒发十万个请求查“id=999999999”的商品(这个商品根本不存在),缓存里没有,直接打MySQL,把数据库打垮。

这个问题优先采用布隆过滤器拦截:

  1. 平台启动时,提前把 MySQL 里所有商品的 id 全量加载到布隆过滤器里完成预热;
  2. 用户请求进来,先过布隆过滤器,如果布隆过滤器判定这个商品 id 不存在,直接返回【商品不存在】,完全不会打到缓存和 MySQL;
  3. 只有布隆过滤器判定 id 存在,才会继续走【Caffeine→Redis→MySQL】的查询流程。

兜底方案

缓存空值。如果查 MySQL 发现这个 Key 真的不存在,就把【空值】缓存到 Redis 里(设置 5 分钟短过期时间),下次再查这个 Key,直接从 Redis 里拿空值返回,避免重复打 MySQL。

小贴士

空值缓存必须设置短过期时间(3-5 分钟),同时限制单 IP 的请求频率,避免恶意攻击生成大量空值缓存撑爆 Redis 内存。

十万并发下的 Redis 压力分摊方案

十万个请求同时进来,如果都打到单实例 Redis 上,大概率会直接把 Redis 打宕机,必须做以下 3 件事分摊压力:

  1. 热点隔离,本地优先扛压:把访问频率 Top10 的极致超级热点 Key(比如首页轮播爆款、正在进行的秒杀活动),全量放在每台服务器的 Caffeine 本地缓存里。十万请求里 90% 都能直接在本地命中,Redis 的压力直接降 90%。
  2. 用 Redis Cluster 集群,把数据分片存储在多个节点上,每个节点只扛一部分请求,避免单节点压力过载;
  3. Redis 主节点只负责数据更新的写请求,多个从节点负责处理十万级的读请求,把读压力分摊到多个从节点上。

多实例下的本地缓存一致性

服务基本都是多实例集群部署的(比如 10 台服务器),每台服务器都有自己独立的 Caffeine 本地缓存,很容易出现数据不一致的问题:

比如在 A 实例更新了商品价格,删了 A 实例的 Caffeine 和 Redis 缓存,但 B 实例的 Caffeine 里还存着旧价格,用户访问 B 实例就会看到脏数据。

考虑用延迟双删 + Redis 发布订阅的组合方案解决这个问题:

  1. 延迟双删:保证 Redis 和 MySQL 的数据一致

① 先删除 Redis 缓存,让缓存失效;

② 再更新 MySQL 数据库,写入新数据;

③ 等待 1 秒(延迟时间必须略大于你的业务接口平均耗时,根据实际压测结果调整,不能盲目设置),再次删除 Redis 缓存。

延迟的目的是等其他实例已经从 MySQL 读到旧数据、更新到缓存后,再删一次缓存,避免脏数据回写。

  1. Redis 发布订阅:保证多实例 Caffeine 的数据一致

①A实例更新完数据、完成延迟双删后,通过 Redis 的发布订阅功能,给集群里所有服务实例广播一条消息:商品 id=123 的缓存已失效,请删除本地缓存;

②所有实例收到消息后,立刻删除自己本地Caffeine里对应的Key;

③下次用户访问任何实例,都会去 Redis 里查询新数据,再更新到本地 Caffeine,保证全集群所有实例的数据一致。

完整的请求+数据更新全流程

Code Road Record