缓存击穿、缓存穿透和缓存雪崩是高并发场景下缓存系统设计中的三类典型问题,它们的核心都与缓存失效导致数据库压力激增相关,但触发条件、表现形式和解决方案各有不同。
情况 | 缓存是否存在 | 数据库是否存在 | 严重等级 | 发生原因 | 影响范围 | 常见解决方案 |
---|---|---|---|---|---|---|
缓存穿透 | 不存在 | 不存在 | 高 | 恶意构造不存在的Key请求、业务数据校验不严格等 | 可能影响所有依赖该数据的业务功能 | 使用布隆过滤器、缓存空值、加强参数校验等 |
缓存击穿 | 不存在(热点数据缓存过期等) | 存在 | 较高 | 热点数据缓存过期、系统初始化时未加载热点数据等 | 主要影响与该热点数据相关的业务功能 | 设置热点数据永不过期、加互斥锁、使用布隆过滤器等 |
缓存雪崩 | 大量数据缓存失效(缓存服务器故障等) | 存在 | 高 | 缓存数据集中过期、缓存服务器故障等 | 可能影响整个系统的正常运行 | 设置不同的过期时间、数据预热、使用多级缓存、保证缓存高可用等 |
缓存击穿
缓存击穿是指在缓存中没有某个 key 的数据,但同时有大量针对该 key 的请求并发访问,这些请求会直接穿透缓存,访问后端数据库(数据库有该数据),可能导致数据库压力过大甚至崩溃。
产生原因
- 缓存过期:当缓存中的数据过期后,还没有来得及更新,此时大量请求同时到达,就会出现缓存击穿的情况。
- 缓存未命中:如果某些数据在缓存中一直没有被命中过,或者由于某些原因(如缓存服务器重启、数据丢失等)导致数据不在缓存中,而此时有大量针对该数据的请求到来,也会引发缓存击穿。
解决方案
-
热点数据设置永不过期
对于一些经常被访问的热点数据,可以设置为永不过期,这样就不会因为缓存过期而导致缓存击穿。但需要注意的是,这种方法可能会导致缓存数据占用过多内存,因此需要根据实际情况合理使用。 -
加互斥锁
在缓存数据过期或不存在时,通过加互斥锁来保证只有一个请求去访问后端数据库加载数据,其他请求则等待该请求将数据加载到缓存后再从缓存中获取数据。这样可以避免大量请求同时穿透缓存访问数据库。具体实现可以使用 Redis 的 SETNX 命令来设置一个互斥锁,当获取锁成功时,执行数据库查询并将结果存入缓存,然后释放锁;当获取锁失败时,等待一段时间后再次尝试从缓存中获取数据。 -
使用布隆过滤器
布隆过滤器可以用来快速判断一个 key 是否存在于缓存中。在请求到达时,先通过布隆过滤器判断该 key 是否可能存在,如果不存在则直接返回,避免无效的数据库查询。布隆过滤器可以显著减少缓存击穿的概率,但它存在一定的误判率,即可能会误判某些不存在的 key 存在于缓存中,但这种误判通常不会对系统造成严重影响。
缓存穿透
缓存穿透是指在系统中,客户端发送的请求所对应的 key 在缓存和数据库中均不存在,导致请求直接绕过缓存,穿透到后端数据库进行查询的现象。
产生原因
-
恶意攻击
攻击者故意构造大量不存在的 key 发起请求,目的是让请求大量访问数据库,可能导致数据库因承受过高压力而崩溃,进而影响整个系统的正常运行。 -
数据异常
业务系统中可能存在数据不一致或错误的情况,导致某些本应存在的数据在缓存和数据库中都缺失,从而引发缓存穿透。比如数据在迁移、同步过程中出现问题。
解决方案
-
缓存空值
当查询数据库发现某个 key 对应的 value 为空时,也将这个空值缓存起来,并设置一个较短的过期时间。这样,下次再有相同的请求时,直接从缓存中返回空值,而不会穿透到数据库。 -
参数校验
对客户端传入的参数进行严格校验,确保参数的合法性和合理性。例如,检查参数的范围、格式等,防止恶意构造不存在的 key 进行请求。 -
使用布隆过滤器
布隆过滤器可以用来快速判断一个 key 是否存在于缓存中。在请求到达时,先通过布隆过滤器判断该 key 是否可能存在,如果不存在则直接返回,避免无效的数据库查询。布隆过滤器可以显著减少缓存击穿的概率,但它存在一定的误判率,即可能会误判某些不存在的 key 存在于缓存中,但这种误判通常不会对系统造成严重影响。
可以说穿透和击穿的根本区别在于数据库中是否存在该数据
缓存雪崩
缓存雪崩是指在某一时刻,缓存中大量的数据同时过期或缓存服务器出现故障,导致大量请求直接涌向数据库,从而使数据库承受巨大压力,甚至可能导致数据库崩溃,进而影响整个系统的正常运行。
产生原因
-
缓存数据同时过期
如果缓存中的大量数据是在同一时间点设置的过期时间,当这些数据同时过期后,后续的请求就无法从缓存中获取数据,会同时去数据库查询,导致数据库瞬间接收大量请求。例如,电商系统中对商品信息的缓存设置了相同的过期时间,促销活动开始时大量商品信息缓存同时失效,就会引发缓存雪崩。 -
缓存服务器故障
当缓存服务器出现硬件故障、软件故障或网络故障等情况时,缓存中的数据无法被正常访问,所有请求都会被转发到数据库,这也会导致数据库面临巨大的压力。比如,缓存服务器所在的机房发生网络中断,导致缓存服务不可用,请求只能去访问数据库。
解决方案
-
设置不同的过期时间
避免缓存中的数据集中在同一时间过期,可以为不同的数据设置随机的过期时间,让缓存失效的时间分散开来,减少同时请求数据库的可能性。例如,将商品信息的缓存过期时间设置在一个时间段内随机取值,如 1 - 2 小时之间。 -
数据预热
在系统启动或缓存失效后,提前将一些热点数据加载到缓存中,避免在缓存失效后大量请求直接访问数据库。可以通过定时任务或在系统初始化阶段进行数据预热,确保缓存中有足够的数据来处理请求。 -
设置多级缓存
采用多级缓存架构,如本地缓存和分布式缓存相结合。当分布式缓存出现问题时,本地缓存可以作为兜底,继续提供数据服务,减少对数据库的直接访问。例如,在应用服务器上使用本地缓存 Ehcache,同时结合分布式缓存 Redis,当 Redis 出现故障时,Ehcache 可以暂时处理部分请求。 -
加互斥锁
在缓存数据过期或不存在时,通过加互斥锁来保证只有一个请求去访问后端数据库加载数据,其他请求则等待该请求将数据加载到缓存后再从缓存中获取数据。这样可以避免大量请求同时穿透缓存访问数据库。具体实现见上文。