缓存击穿和缓存雪崩是缓存体系中常见的问题,,它们都可能导致系统性能下降,甚至瘫痪,了解它们的机制和预防措施对于提高系统性能和稳定性非常重要。
一、缓存击穿
1. 产生机制:
缓存击穿发生在缓存中不存在某个请求的结果,且这个请求的结果在数据库中也是不存在的情况下。当大量并发请求同一key,而该key在缓存中为空时,由于缓存中没有该结果,所有请求都会直接查询数据库,这种情况会导致数据库瞬时承受巨大的压力。
2. 后果:
-
数据库负载剧增,可能导致数据库崩溃或者响应缓慢。
-
影响用户体验,可能会导致服务不可用。
-
造成资源浪费,重复查询数据库增加了不必要的开销。
3. 预防措施:
-
缓存空值:对于查询结果为空的key,也进行缓存,设置较短的失效时间,这样后续相同请求就能直接从缓存中获取。
-
使用布隆过滤器:在请求数据库之前,先通过布隆过滤器检查请求的key是否存在,存在才会去数据库查询,可以有效减少无效请求直接访问数据库。
-
加锁机制:对于热点数据,可以在查询之前加锁,直到数据被加载完成,这样可以避免大量请求同时打到数据库。
二、缓存雪崩
1. 产生机制:
缓存雪崩是指在某一时刻大量缓存同时失效,导致大量请求直接访问后端数据库。尤其是在高峰期,如果多个缓存项在同一时间过期,会集中对数据库发起请求。
2. 后果:
-
数据库承受巨大的压力,可能导致瞬时崩溃或长时间无法响应。
-
影响系统的稳定性和可用性。
-
增加后端处理时间,导致响应延迟。
3. 预防措施:
-
设置随机过期时间:在设置缓存的过期时间时,可以引入一定的随机性,避免所有缓存同时失效。
-
多级缓存:使用不同层次的缓存,如本地缓存、分布式缓存等,以降低单个缓存层的压力。
-
限流:对访问数据库的请求进行限流,保护数据库的承载能力。
-
提前预热缓存:在预计的高并发时段,提前将热点数据加载到缓存中。
三、布隆过滤器
**布隆过滤器(Bloom Filter)**是一种空间效率高的概率型数据结构,用于测试某个元素是否在一个集合中。它的特点是:
-
快速查询:可以快速判断某个元素是否在集合中。
-
可能产生误判:如果查询结果为“在集合中”,可能是正确的,也可能是误判(即元素实际上不在集合中)。如果查询结果为“不在集合中”,则一定不在集合中。
-
不支持删除:一旦元素被添加到布隆过滤器中,无法删除。
使用场景:布隆过滤器常用于防止缓存击穿,尤其是在高并发情况下,能够有效减少对数据库的无效请求。
四、热点数据查询之前加锁
在并发环境中,为了避免多个请求同时查询数据库并导致压力过大,可以对热点数据加锁。具体步骤如下:
-
请求到达时,先检查缓存:
-
如果命中,直接返回缓存结果。
-
如果未命中,尝试获取锁。
-
-
获取锁:
-
使用分布式锁(如Redis的SETNX命令)或本地锁(如Java的synchronized)来对该key加锁。
-
-
查询数据库:
-
如果成功获得锁,查询数据库并更新缓存。
-
-
释放锁:
-
查询完成后,释放锁。
-
五、对于查询结果为空的key进行缓存
对于查询结果为空的key进行缓存的操作可以通过以下方式实现:
-
设置缓存时,检查结果是否为空:
-
如果结果为空,则将该key的值设置为一个特殊的标记(如
null
或"EMPTY"
),并设置较短的过期时间。
-
-
后续请求:
-
当后续请求相同的key时,如果缓存中存在该标记,则直接返回空值,而不再查询数据库
-
示例代码:
-
import redis
-
import time
-
import threading
-
# 初始化Redis连接
-
r = redis.StrictRedis(host='localhost', port=6379, db=0)
-
# 锁的前缀
-
LOCK_PREFIX = 'lock:'
-
def get_data_from_db(key):
-
# 模拟从数据库查询数据
-
print(f"Querying database for key: {key}")
-
time.sleep(2) # 模拟延迟
-
return None # 假设数据库中没有该key
-
def get_data(key):
-
# 先检查缓存
-
cached_data = r.get(key)
-
if cached_data is not None:
-
if cached_data == b'EMPTY':
-
# 如果缓存中是空值标记,直接返回None
-
return None
-
return cached_data # 返回缓存中的数据
-
# 尝试获取锁
-
lock_key = LOCK_PREFIX + key
-
if r.set(lock_key, 'LOCKED', nx=True, ex=5): # 设置锁,5秒后过期
-
try:
-
# 锁成功,查询数据库
-
data = get_data_from_db(key)
-
if data is None:
-
# 如果结果为空,缓存一个空值标记
-
r.set(key, 'EMPTY', ex=60) # 设置过期时间为60秒
-
else:
-
r.set(key, data, ex=60) # 正常缓存数据
-
return data
-
finally:
-
# 释放锁
-
r.delete(lock_key)
-
else:
-
# 如果锁被其他请求持有,等待一段时间后重试
-
time.sleep(0.1)
-
return get_data(key)
-
# 示例调用
-
result = get_data('some_key')
-
print(f"Result: {result}")
六、提前将热点数据加载到缓存中
热点数据: 通常是访问频率极高、易于产生缓存穿透或击穿的数据。例如,用户信息、商品列表、统计数据等。
1. 如何选择热点数据:
-
历史访问日志分析:通过分析历史日志,确定哪些数据被频繁访问。
-
业务逻辑:识别业务中哪些数据可能是热点,例如高频交易、直播时的评论等。
-
A/B 测试:基于用户行为标记进行测试以及反馈分析,识别访问量高的特定数据。
2. 提前加载的操作:
可以在系统启动时或定期进行后台任务,将热点数据加载到缓存中。
示例代码:
-
def preload_hot_data(hot_keys):
-
for key in hot_keys:
-
# 从数据库中查询数据
-
data = get_data_from_db(key)
-
if data is not None:
-
# 将数据缓存
-
r.set(key, data, ex=3600) # 设定过期时间为1小时
-
# 定义待加载的热点数据key
-
hot_keys = ['user:1', 'product:42', 'stats:monthly_sales']
-
# 启动时预加载数据
-
preload_hot_data(hot_keys)
总结
要有效预防缓存击穿和雪崩,可以综合使用上述多种策略,充分考虑系统的架构设计和业务逻辑,在高并发场景下保障系统的稳定性与良好的用户体验。同时,监测系统的运行状态,及时发现和调整潜在问题也是关键。
感谢每一个认真阅读我文章的人!!!
作为一位过来人也是希望大家少走一些弯路,如果你不想再体验一次学习时找不到资料,没人解答问题,坚持几天便放弃的感受的话,在这里我给大家分享一些自动化测试的学习资源,希望能给你前进的路上带来帮助。
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
视频文档获取方式:
这份文档和视频资料,对于想从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!以上均可以分享,点下方小卡片即可自行领取。