缓存异常问题
约 2105 字大约 7 分钟
2025-09-01
缓存击穿
缓存击穿是指在高并发场景下,某个热点数据在缓存中恰好过期失效的瞬间,大量针对该热点数据的请求会直接打到数据库,导致数据库瞬时压力过大甚至崩溃。
解决方案
互斥锁(Mutex Lock)
- 思路: 仅允许一个线程去查询数据库并重建缓存,其他线程则阻塞等待该线程完成。这样可以避免大量请求同时重建缓存,从而减轻数据库压力。
- 具体实现:
- 当一个线程从 Redis 中获取数据为空时,首先尝试获取一个分布式锁。
- 如果获取锁成功,该线程从数据库加载数据,将数据写入 Redis 缓存,然后释放锁。
- 如果其他线程在此时也从 Redis 获取数据为空,尝试获取锁时会失败。这些线程会阻塞等待锁的释放。
- 待锁释放后,这些线程会进行二次检查(double check),此时通常能成功从 Redis 中获取到数据。
逻辑过期(Logical Expiration)
- 思路: 缓存数据不设置物理过期时间(即不设置 Redis
TTL),而是在缓存的Value中额外存储一个逻辑过期时间字段。当数据逻辑过期时,异步地进行缓存重建,同时主线程可以先返回旧数据。 - 具体实现:
- 缓存数据时,除了实际数据外,额外存储一个逻辑过期时间字段(例如
expireTime)。 - 当请求命中缓存时,检查
Value中的expireTime字段。 - 如果
expireTime未过期,直接返回缓存中的旧数据。 - 如果
expireTime已过期,则启动一个异步线程去获取互斥锁并重建缓存(从数据库加载新数据,更新缓存及expireTime)。同时,当前请求线程可以立即返回缓存中的旧数据。这种方式保证了高并发下热点数据访问的低延迟。
- 缓存数据时,除了实际数据外,额外存储一个逻辑过期时间字段(例如
- 思路: 缓存数据不设置物理过期时间(即不设置 Redis
缓存穿透
缓存穿透是指查询一个不存在的数据。由于数据在 Redis 缓存和数据库中都不存在,导致缓存永远不会命中,所有针对该数据的请求都会直接打到数据库,从而给数据库带来不必要的压力。
解决方案
缓存空值(Cache Empty Values)
- 思路: 当从数据库查询结果为空时,也将这个空结果缓存到 Redis,并设置一个较短的
TTL。这样,下次查询相同的不存在数据时,可以直接从缓存中返回空,避免再次访问数据库。 - 注意: 空值的
TTL不宜过长,以避免缓存过多无用数据,同时允许数据库中新增数据后能够及时被查询到。
- 思路: 当从数据库查询结果为空时,也将这个空结果缓存到 Redis,并设置一个较短的
布隆过滤器(Bloom Filter)
- 思路: 在数据写入数据库时,将数据的 Key 通过多个哈希函数映射到布隆过滤器中的一个位数组,并将对应的位设置为 1。在查询数据时,首先通过布隆过滤器判断 Key 是否可能存在。如果布隆过滤器判断 Key 不存在,则直接返回空,避免访问缓存和数据库。
- 具体实现:
- 数据插入: 当数据写入数据库时,使用
n个不同的哈希函数对 Key 进行计算,得到n个哈希值(即位数组中的索引)。将布隆过滤器位数组中对应索引的位设置为 1。 - 数据查询: 当查询一个 Key 时,同样使用这
n个哈希函数对 Key 进行计算,得到n个哈希值。 - 检查布隆过滤器位数组中对应这
n个索引的位。- 如果所有对应的位都是 1,则表示该 Key 可能存在(存在一定的误判率)。此时可以继续查询缓存和数据库。
- 如果其中任何一个对应的位是 0,则表示该 Key 一定不存在。此时直接返回空,无需查询缓存和数据库。
- 数据插入: 当数据写入数据库时,使用
缓存雪崩
缓存雪崩是指在短时间内,大量缓存 Key 同时过期失效,或者缓存服务整体宕机,导致所有原本应该由缓存处理的请求瞬间涌向数据库,使得数据库瞬时压力过大甚至崩溃。
解决方案
均匀设置过期时间
- 思路: 避免大量 Key 在同一时间点过期。通过给 Key 的
TTL加上一个随机值,使得不同 Key 的过期时间错开。 - 具体实现: 在设置缓存的
TTL时,在基础过期时间上增加一个小的随机数。
- 思路: 避免大量 Key 在同一时间点过期。通过给 Key 的
构建高可用的缓存集群
- 思路: 通过使用 Redis 集群(如主从复制、哨兵模式、集群模式)来提高缓存服务的可用性和稳定性,避免单点故障导致整个缓存服务宕机。
- 具体实现:
- 主从复制: 读写分离,提高读取性能,主节点宕机可手动切换从节点。
- 哨兵模式: 自动监控主从节点,实现故障自动切换,提高可用性。
- 集群模式(Cluster): 数据分片存储,高可用,可扩展,更彻底地避免单点故障。
熔断、限流、降级
- 思路: 在缓存失效或服务宕机时,通过限制进入数据库的请求数量,保护数据库不被冲垮。
- 具体实现:
- 限流: 控制并发访问数据库的请求数量,例如使用漏桶算法或令牌桶算法。
- 熔断: 当数据库压力过大或响应异常时,暂时切断对数据库的访问,快速返回错误或默认值。
- 降级: 在服务压力过大时,关闭一些非核心功能,或者提供简化服务,确保核心功能的可用性。
多级缓存
- 思路: 引入多级缓存机制(如本地缓存 L1、分布式缓存 L2),即使 L2 缓存宕机,L1 缓存仍能承担一部分流量,减轻数据库压力。
- 具体实现: 将一部分热点数据缓存在应用服务器的本地内存中,作为第一级缓存。当分布式缓存失效时,请求可以先尝试访问本地缓存。
总结
什么是热 Key?会带来什么问题?你是怎么解决的?
热 Key 指在短时间内被大量访问的某个 Key,它会导致 Redis 单节点压力过大,引发性能瓶颈,严重时可能出现请求阻塞,甚至在 Key 过期时触发缓存击穿。
它的影响主要有三方面:
- Redis 实例的 QPS 瞬间激增,延迟明显上升;
- 集群分片失衡,热点集中在单台节点;
- 应用层可能出现级联故障,比如请求堆积或雪崩。
针对热 Key,我的思路是分层解决:
- 在应用层,可以加本地缓存(如 Caffeine),减少对 Redis 的依赖;同时预加载热点数据,避免过期击穿。
- 在 Redis 层,可以通过 Key 分片,把热点 Key 拆成多个子 Key,或者用读写分离,把读流量分散到从节点。
- 此外,要做好监控和预防,利用工具及时发现热 Key,在业务上提前识别高并发场景,比如秒杀商品或热榜数据。
什么是大 Key?会带来什么问题?你是怎么解决的?
大 Key 是指单个 Value 体积过大,比如 String 类型超过 2MB,或者 Hash/List 等集合中元素数超过 5000。
它的影响主要有两个方面:
- 操作大 Key 会占用 Redis 主线程时间,导致阻塞和性能下降;
- 在使用 RDB 持久化时,大 Key 会引发内存和 CPU 峰值,严重时甚至导致 OOM。
常见的解决方案有:
数据拆分:把大 Key 拆成多个小 Key,例如一个超大 Hash 拆成多个 Hash 存储,避免单 Key 过大。
异步删除:删除时使用
UNLINK代替DEL,由后台线程异步释放内存,避免阻塞主线程。监控和预防:利用工具(如
redis-cli --bigkeys)提前发现大 Key,在系统设计阶段就避免过大数据聚合。