知识卡片

缓存雪崩的连锁反应机制及更新锁与后台更新两种方案

结构图卡

内容

缓存雪崩指缓存失效(过期)后引发系统性能急剧下降的连锁反应:缓存一过期被清除,业务系统就要重新访问存储系统、重新做一遍耗时的运算才能生成新缓存(这个过程通常要几十到上百毫秒),而对高并发系统来说,这几百毫秒内可能同时涌入成百上千个请求——由于旧缓存已经没了、新缓存还没生成,而且每个处理请求的线程互相并不知道”已经有另一个线程正在生成缓存”,于是所有这些请求都会各自重新访问存储系统、各自重新做一遍运算,瞬间把原本该由缓存挡住的压力,成倍地砸向存储系统,严重时会直接压垮数据库,进而拖垮整个系统,这才是”雪崩”这个名字的由来——单点的缓存失效被并发请求放大成了系统性的连锁崩溃。常见的解决方法有两种。第一种是更新锁机制:给缓存更新操作加锁,保证同一时间只有一个线程能真正去更新缓存,没抢到锁的线程要么等锁释放后重新读缓存,要么先返回空值或默认值兜底;需要注意的是,如果业务系统本身是分布式集群(几十上百台服务器),即使每台机器内部只有一个线程负责更新,几十上百台机器加起来同一时刻依然可能有几十上百个线程在抢着更新缓存,本质上还是雪崩,所以分布式场景下要实现真正有效的更新锁,必须借助分布式锁(如ZooKeeper)而不能只在单机内加锁。第二种是后台更新机制:把缓存更新的职责从业务线程转移到专门的后台线程,缓存本身设为永不过期,由后台线程按固定周期主动刷新;这个方案要特别处理一种边界情况——缓存系统内存不够时可能会把某些数据”踢”出去,从被踢掉到后台线程下一次定时刷新之间这段空窗期,业务线程读到的就是空值,表现为”数据丢了”,应对方式有两种:一是后台线程除了定时更新,还高频(比如每秒或每100毫秒)轮询检查缓存是否被踢掉,一旦发现立刻补更新,实现简单但轮询间隔不能设太长;二是业务线程发现缓存失效时通过消息队列主动通知后台线程去更新(即使多个业务线程同时发消息也没关系,后台线程收到消息后先判断缓存是否已存在,存在就跳过),这种方式依赖消息队列、复杂度更高,但更新更及时、用户体验更好。后台更新机制天然适配单机多线程和分布式集群两种场景,实现上比更新锁机制简单,还可以顺带用于业务上线时的”缓存预热”——上线时直接把相关数据提前加载进缓存,而不是被动等第一个用户访问时才触发生成。

结构图

flowchart TB
  A["缓存雪崩:缓存过期→重新生成耗时<br/>并发请求全部同时穿透到存储系统"]
  A --> B["方案一:更新锁机制<br/>只有一个线程能更新缓存"]
  B --> B1["分布式集群陷阱:单机内加锁不够<br/>多机加起来仍会雪崩<br/>需用ZooKeeper等分布式锁"]
  A --> C["方案二:后台更新机制<br/>缓存永不过期,后台线程定时刷新"]
  C --> C1["边界情况:缓存被内存不足踢掉<br/>到下次定时刷新前出现空窗期'数据丢了'"]
  C1 --> C2["应对①后台线程高频轮询检查<br/>被踢立刻补更新"]
  C1 --> C3["应对②业务线程发现失效<br/>用消息队列通知后台线程更新"]
  C --> D["额外收益:适合做业务上线的<br/>缓存预热(主动预加载而非被动触发)"]

参考来源

- 位置:《从零开始学架构》第17讲《高性能缓存架构》"缓存雪崩"(源文件:_epub-src/OEBPS/text00001.html) - 结论依据:原文说明"由于旧的缓存已经被清除,新的缓存还未生成……所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力……造成整个系统崩溃",并展开更新锁机制"对于采用分布式集群的业务系统……需要用到分布式锁,如 ZooKeeper"和后台更新机制"缓存本身的有效期设置为永久,后台线程定时更新缓存"及其内存踢出边界情况的两种应对,直接支撑本卡片结论与结构图。 - 原始内容:缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况……所有的请求都会去重新生成缓存,都会去访问存储系统……对于采用分布式集群的业务系统……需要用到分布式锁,如 ZooKeeper……由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久。