Redis 缓存穿透、雪崩和击穿问题及解决方案
Redis 缓存穿透、雪崩和击穿问题及解决方案
在使用 Redis 作为缓存时,可能会遇到 缓存穿透、缓存雪崩 和 缓存击穿 这三种常见问题,它们都会影响系统的稳定性和性能。下面详细讲解它们的定义、成因以及相应的解决方案。
1. 缓存穿透(Cache Penetration)
1.1 定义
缓存穿透指的是查询的数据在缓存和数据库中都不存在,导致每次请求都会直接访问数据库,进而对数据库造成很大压力。
1.2 产生原因
- 恶意攻击:攻击者有意构造大量不存在的 key 来请求,绕过缓存直接访问数据库。
- 程序错误:业务代码逻辑有漏洞,导致频繁访问数据库中没有的数据。
1.3 解决方案
-
缓存空值(推荐)
- 当数据库查询结果为空时,将该 key 对应的值设置为 NULL 或 默认值 并存入缓存,设置一个短暂的过期时间(如 1 分钟)。
- 例如:
value = redis.get(key) if value is None: value = db.query(key) if value is None: redis.set(key, "NULL", ex=60) # 设置 1 分钟过期 else: redis.set(key, value, ex=3600) # 设置 1 小时过期
-
布隆过滤器(Bloom Filter)
- 使用布隆过滤器存储所有可能存在的 key,在查询缓存和数据库之前,先检查 key 是否可能存在。
- 例如:
from bloom_filter import BloomFilter bf = BloomFilter(capacity=100000, error_rate=0.01) bf.add("existing_key") # 添加已有 key if key not in bf: return None # 直接返回,避免访问 Redis 和数据库
-
参数校验与拦截
- 在业务逻辑层对 key 进行基本的格式校验,避免恶意构造的请求直接进入数据库查询。
2. 缓存雪崩(Cache Avalanche)
2.1 定义
缓存雪崩指的是大量缓存数据同时失效,导致瞬间大量请求涌向数据库,造成数据库负载过高甚至宕机。
2.2 产生原因
- 大量数据设置了相同的过期时间,导致缓存同时过期。
- Redis 故障或宕机,导致所有缓存数据失效。
2.3 解决方案
-
缓存过期时间加随机值(推荐)
- 例如:
import random redis.set(key, value, ex=3600 + random.randint(1, 600)) # 过期时间 3600~4200 秒
- 例如:
-
双层缓存策略
- 设立两级缓存:
- 一级缓存(Redis)
- 二级缓存(本地缓存,如 Guava Cache 或 EhCache)
- 当 Redis 失效时,查询本地缓存,减少数据库压力。
- 设立两级缓存:
-
热点数据提前预热
- 在大规模活动(如秒杀、促销)前,提前加载热点数据到缓存,防止瞬时大规模请求击穿缓存。
-
降级与限流
- 在缓存失效导致数据库压力激增时:
- 限流:限制请求速率,如 令牌桶 或 漏桶算法。
- 降级:返回默认值、缓存旧数据、提示用户稍后重试。
- 在缓存失效导致数据库压力激增时:
3. 缓存击穿(Cache Breakdown)
3.1 定义
缓存击穿指的是某个热点 key 失效后,大量请求同时访问数据库,造成数据库瞬时压力过大。
3.2 产生原因
- 某些热点数据访问量极高,一旦该 key 过期,瞬时大量请求直接访问数据库。
- 并发查询同一 key,导致大量请求同时击穿缓存。
3.3 解决方案
-
设置热点数据永不过期(推荐)
- 例如:
redis.set("hot_key", value, ex=None) # 不设置过期时间
- 例如:
-
互斥锁(分布式锁)
- 例如:
import time import redis client = redis.StrictRedis() def get_value_with_lock(key): value = client.get(key) if value is None: # 缓存失效 lock = client.setnx(f"lock:{key}", 1) # 尝试加锁 if lock: client.expire(f"lock:{key}", 10) # 设置锁超时时间 value = db.query(key) # 查询数据库 client.set(key, value, ex=3600) # 写入缓存 client.delete(f"lock:{key}") # 释放锁 else: time.sleep(0.1) # 等待一段时间后再查缓存 return get_value_with_lock(key) return value
- 例如:
-
异步更新缓存
- 采用异步线程或消息队列来更新缓存,避免数据库查询被并发请求阻塞。
总结
问题 | 定义 | 解决方案 |
---|---|---|
缓存穿透 | 查询数据在缓存和数据库中都不存在,导致所有请求都打到数据库 | 缓存空值、布隆过滤器、参数校验 |
缓存雪崩 | 大量 key 同时过期或 Redis 故障,导致数据库瞬时压力过大 | 过期时间加随机值、双层缓存、预热数据、限流降级 |
缓存击穿 | 热点 key 过期后,大量并发请求直接访问数据库 | 热点数据永不过期、互斥锁、异步更新缓存 |
如果你的系统涉及大规模并发访问,那么就要考虑这些缓存问题,并采用合适的策略来防止性能瓶颈! 🚀