高并发情况下数据库如 MySQL 等通常会成为系统瓶颈,所以通常会引入 Redis 作为数据库的缓存层。可以缓解数据库的压力,同时提升访问速度。流程如下:
- 查询请求先到 Redis,如果没有数据则到底层数据库查找。
- 查完后再缓存到 Redis 中以便下次访问命中。
但在使用过程中会发生缓存穿透、缓存击穿、缓存雪崩等问题,下面来详细解析这些问题及应对方案。
1. 缓存穿透
1.1. 缓存穿透概念
缓存穿透 是指 查询一个数据库中不存在的数据,导致请求 绕过缓存 直接访问数据库。低频缓存穿透不可避免,但恶意的、高频的缓存穿透则容易引发数据库压力过大甚至宕机。
1.2. 解决方案
1.2.1. 缓存空对象
当查询请求到达 Redis 且没有命中时,请求会到达底层数据库,如果数据库中也不存在该数据,则缓存一个空对象到 Redis 中。这样就可以解决缓存穿透问题。
该方案存在的问题:
- 后续可能数据库中真的写入了该数据,但 Redis 中并未更新,Redis 里缓存的依然的是空对象。
- 解决办法:Redis 中缓存的空数据需要设置一个较短的过期时间,只能实现弱一致性。
- 黑客随机访问大量不存在的数据,会导致 Redis 中缓存大量无用的值为空的key,导致大量的内存占用。同时 Redis 有 LRU 或 LFU 的内存淘汰策略,可能会将缓存中有价值的数据淘汰掉,导致真正的用户请求会打到数据库上,造成数据库压力过大。
1.2.2. 布隆过滤器
将数据库中所有存在的数据的 id 经过 hash 放入一个 list 中,访问前先判断 list 中是否存在该数据。如果布隆过滤器返回 不存在,则直接拒绝请求。如果返回 存在,再去查缓存或数据库。
该方案优缺点:
- 优点:内存占用极小,适合海量数据场景。
- 缺点:有一定的误判率(可能误判存在,但不会误判不存在)。
示例(Redis + Bloom Filter):
from pybloom_live import ScalableBloomFilter
# 初始化布隆过滤器
bloom_filter = ScalableBloomFilter(initial_capacity=100000, error_rate=0.001)
# 预热数据(如所有合法 user_id)
for user_id in db.query("SELECT id FROM users"):
bloom_filter.add(user_id)
def get_user(user_id):
# 1. 先用布隆过滤器判断是否存在
if user_id not in bloom_filter:
return None # 直接返回,不查缓存和数据库
# 2. 查缓存
user = redis.get(f"user:{user_id}")
if user:
return json.loads(user)
# 3. 查数据库
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
if user:
redis.setex(f"user:{user_id}", 3600, json.dumps(user))
return user
Python2. 缓存击穿
2.1. 缓存击穿概念
缓存击穿是缓存穿透的一种特殊表现,某个热点 key 突然失效,大量请求直接打到数据库(通常发生在高并发场景)。例如:热搜数据缓存过期,瞬间大量请求涌入。
2.2. 解决方案
2.2.1. 热点数据不过期
最简单的方法就是缓存这些热点数据的时候不设置过期时间,但这么做也会引发新问题,数据库数据更新了但 Redis 缓存的数据没有更新会导致数据不一致。所以需要一种方式来保证数据库和 Redis 中的数据的一致性。但是强一致性很难做到,只能保证弱一致性,例如:
- 使用中间件监听 MySQL 的 binlog,有变动则同步到 Redis 中,可以达到弱一致性。
2.2.2. 分布式锁
只允许 一个请求 去查询数据库并重建缓存,其他请求等待或直接返回旧数据。
实现方式(Redis + SETNX 分布式锁):
import redis
import time
import threading
redis_client = redis.StrictRedis()
def get_data(key, expire=3600):
# 1. 先查缓存
data = redis_client.get(key)
if data is not None:
return data
# 2. 缓存不存在,尝试获取锁
lock_key = f"lock:{key}"
lock_acquired = redis_client.setnx(lock_key, 1) # SETNX 实现锁
if lock_acquired:
try:
# 3. 获取锁成功,查询数据库
data = query_db(key)
# 4. 写入缓存
redis_client.setex(key, expire, data)
# 5. 释放锁(防止死锁)
redis_client.delete(lock_key)
return data
except Exception as e:
redis_client.delete(lock_key) # 确保异常时释放锁
raise e
else:
# 6. 未获取锁,短暂等待后重试或返回默认值
time.sleep(0.1) # 避免频繁重试
return get_data(key) # 递归重试(或返回旧数据/错误提示)
Python优缺点:
- 优点:
- 完全避免缓存击穿,确保只有一个请求访问数据库。
- 缺点:
- 如果获取锁的请求 处理时间过长,可能导致其他请求长时间等待。
- 需要处理 死锁问题(如设置锁超时
redis_client.expire(lock_key, 10)
)。
3. 缓存雪崩
缓存雪崩也是缓存穿透的一种特殊表现,上面介绍的缓存击穿是一个热点 key 失效,而缓存雪崩是多个热点 key 同时失效。一般出现缓存雪崩的原因如下:
- 缓存过期的时间比较一致某一时刻 key 大面积失效。
- 解决办法:将缓存时间设置成一个随机数。
- Redis 服务崩溃。
- 解决办法:通过 Redis 集群 或 哨兵模式 避免单点故障。
Redis 集群和哨兵模式在下一篇文章详细介绍。