缓存常见问题
一、缓存穿透 (Cache Penetration)
是什么
缓存穿透是指客户端持续请求一个缓存和数据库中都根本不存在的数据。这导致每次请求都会先查缓存(未命中),然后穿透到数据库查询(也未命中)。如果这类请求量很大(比如恶意攻击,或者程序逻辑缺陷导致查询不存在的ID),就会给数据库带来巨大压力,因为每次查询都是无效的。
解决方法
- 缓存空值 (Cache Null Values) :
- 当数据库查询未找到数据时,仍然在缓存中存储一个特殊标记(如null或特定字符串),并为其设置一个较短的过期时间(如1-5分钟)。这样后续对同一不存在数据的请求会命中缓存中的这个“空标记”,直接返回,避免了再次查询数据库。
- 布隆过滤器 (Bloom Filter) фильтр:
- 在访问缓存和数据库之前,使用布隆过滤器预先判断请求的数据是否存在。布隆过滤器可以高效地判断一个元素“一定不存在”或“可能存在”。
- 如果布隆过滤器判断数据不存在,则直接返回,不查询缓存和数据库,能拦截大部分对不存在数据的无效请求。
- 需要注意布隆过滤器的误判率,以及数据写入时需要同步更新布隆过滤器。
- 接口层增加校验:
- 对请求参数进行合法性校验,如用户鉴权、数据格式校验、ID范围校验等,对于不合法的请求直接拦截。
二、缓存雪崩 (Cache Avalanche)
是什么
缓存雪崩是指在短时间内,缓存中的大量Key几乎在同一时刻集中过期失效,或者缓存服务本身(如Redis集群)发生故障宕机。这两种情况都会导致原本由缓存处理的大量并发请求,在缓存失效后,瞬间直接涌向数据库,可能导致数据库压力剧增甚至崩溃。
解决方法
- 设置随机过期时间 (Randomized Expiration Times) :
- 在基础过期时间上增加一个随机的偏移量(例如,过期时间 = 固定时间 + random(0, 300)秒)。这样可以避免大量Key在同一精确时刻集中失效,将过期时间点打散。
- 多级缓存 (Multi-level Cache) :
- 使用例如Nginx/OpenResty的本地缓存 + Redis分布式缓存 + 数据库的架构。即使Redis层发生雪崩,前置的本地缓存也能挡住一部分流量。
- 服务熔断与限流 (Circuit Breaking & Rate Limiting) :
- 当检测到数据库访问压力过大或响应缓慢时,通过熔断机制(如Sentinel, Hystrix)暂时阻止对数据库的进一步请求,直接返回降级响应(如预设的默认值或提示信息),保护数据库。
限流则控制单位时间内访问数据库的请求数量。
- 当检测到数据库访问压力过大或响应缓慢时,通过熔断机制(如Sentinel, Hystrix)暂时阻止对数据库的进一步请求,直接返回降级响应(如预设的默认值或提示信息),保护数据库。
- 保证缓存服务高可用 (High Availability for Cache Service) :
- 对于Redis等缓存服务,搭建集群(如Redis Sentinel、Redis Cluster)或使用云服务商提供的高可用缓存服务,确保缓存服务不会轻易整体宕机。
- 数据预热 (Data Preheating) :
- 在系统启动或低峰期,提前将热点数据加载到缓存中,并设置合理的过期策略。
三、缓存击穿 (Cache Breakdown / Hotspot Key Problem)
是什么
缓存击穿,也常被称为热点Key问题。它指的是某一个访问非常频繁的热点数据Key,在其对应的缓存失效的瞬间,大量针对该特定Key的并发请求同时涌入。由于缓存未命中,这些并发请求会全部直接打到数据库上查询这同一个数据,对数据库单点造成巨大压力,就像把缓存“击穿”了一个洞。
解决方法
- 互斥锁/分布式锁 (Mutex Lock / Distributed Lock) :
- 当缓存未命中时,只允许一个请求线程获取锁去查询数据库并将结果写入缓存。其他线程则等待(可以设置自旋、休眠后重试,或直接返回特定提示)。一旦持有锁的线程更新了缓存并释放锁,后续请求就能从缓存获取数据。
- 在分布式环境下,需要使用分布式锁(如基于Redis的Redisson,或基于ZooKeeper)。
- 这是最常用的解决方案,能有效防止对数据库的并发冲击。
- 热点数据永不过期 (或逻辑过期) (Never Expire Hot Data / Logical Expiration) :
- 物理永不过期:对于访问量极大且数据相对稳定的热点Key,可以考虑不设置物理过期时间,数据的更新通过后台任务主动刷新或消息通知机制来更新缓存。
- 逻辑过期:缓存数据本身不设置TTL(或设置很长)。在缓存值中额外存储一个逻辑过期时间字段。当查询缓存命中后,判断逻辑过期时间:
- 未过期:直接返回。
- 已过期:启动一个异步线程去更新缓存(此过程可加锁防止并发更新),当前请求可以先返回旧数据(如果业务允许),或者等待异步更新完成。
- 二级缓存/本地缓存:
- 对于极热点的数据,除了分布式缓存,还可以在应用服务器内部署本地缓存(如Caffeine, Guava Cache),降低对分布式缓存的压力,进一步减少击穿到数据库的可能性。