缓存是提升系统性能的利器,但在使用过程中,我们常常会遇到三种经典问题:缓存雪崩、击穿和穿透。这些问题在高并发场景下可能导致系统崩溃,理解它们的原理并掌握解决方案是每个后端开发者必备的技能。
1. 缓存雪崩(Cache Avalanche)
1.1 什么是缓存雪崩?
缓存雪崩是指大量缓存数据在同一时间过期,导致所有请求直接访问数据库,造成数据库瞬时压力过大而崩溃的现象。
场景示例:
假设电商平台首页有1000个商品缓存,这些缓存都设置了相同的过期时间(比如2小时)。当缓存同时失效时,大量用户请求会直接涌向数据库,导致数据库不堪重负。
1.2 解决方案
1.2.1 设置随机过期时间
import random
import time
def set_cache_with_random_expire(key, value, base_expire=3600):
# 基础过期时间 + 随机时间(0-300秒)
expire = base_expire + random.randint(0, 300)
redis_client.setex(key, expire, value)
1.2.2 热点数据永不过期
对极其重要的热点数据,可以设置永不过期,通过后台任务定期更新:
def update_hot_data():
while True:
# 后台异步更新缓存
data = get_data_from_db()
redis_client.set("hot_data", data)
time.sleep(1800) # 每30分钟更新一次
1.2.3 多级缓存架构
# 本地缓存 + Redis缓存
class MultiLevelCache:
def __init__(self):
self.local_cache = {}
self.redis_client = redis.Redis()
def get(self, key):
# 先查本地缓存
if key in self.local_cache:
return self.local_cache[key]
# 再查Redis
value = self.redis_client.get(key)
if value:
self.local_cache[key] = value
return value
# 最后查数据库
value = self.get_from_db(key)
if value:
self.redis_client.setex(key, 3600, value)
self.local_cache[key] = value
return value
1.2.4 熔断机制
当数据库压力过大时,自动熔断,返回默认数据:
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN
self.last_failure_time = None
def call(self, func, fallback_func):
if self.state == "OPEN":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "HALF_OPEN"
else:
return fallback_func()
try:
result = func()
if self.state == "HALF_OPEN":
self.state = "CLOSED"
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"
return fallback_func()
2. 缓存击穿(Cache Breakdown)
2.1 什么是缓存击穿?
缓存击穿是指某个热点key过期时,大量并发请求直接访问数据库,导致数据库压力激增的现象。
与雪崩的区别:击穿是针对单个热点key,雪崩是大量key同时失效。
场景示例:
秒杀活动中,某个热门商品缓存过期,瞬间有数万个请求同时查询数据库。
2.2 解决方案
2.2.1 互斥锁(Mutex Lock)
import threading
from contextlib import contextmanager
class CacheMutex:
def __init__(self):
self.locks = {}
self.global_lock = threading.Lock()
@contextmanager
def acquire(self, key, timeout=3):
with self.global_lock:
if key not in self.locks:
self.locks[key] = threading.Lock()
lock = self.locks[key]
acquired = lock.acquire(timeout=timeout)
try:
yield acquired
finally:
if acquired:
lock.release()
def get_data_with_mutex(key):
# 先尝试从缓存获取
data = redis_client.get(key)
if data:
return data
# 缓存未命中,使用互斥锁
with cache_mutex.acquire(key) as acquired:
if not acquired:
# 获取锁失败,稍后重试或返回默认值
time.sleep(0.1)
return get_data_with_mutex(key)
# 再次检查缓存(双重检查)
data = redis_client.get(key)
if data:
return data
# 查询数据库
data = get_from_db(key)
if data:
redis_client.setex(key, 3600, data)
return data
2.2.2 逻辑过期时间
def set_cache_with_logic_expire(key, value, expire_seconds=3600):
# 在value中存储逻辑过期时间
cache_data = {
'data': value,
'expire_time': time.time() + expire_seconds
}
redis_client.set(key, json.dumps(cache_data))
def get_data_with_logic_expire(key):
cache_data = redis_client.get(key)
if not cache_data:
# 缓存不存在,直接查数据库
return get_from_db(key)
cache_obj = json.loads(cache_data)
if time.time() > cache_obj['expire_time']:
# 缓存逻辑过期,异步更新
threading.Thread(target=async_update_cache, args=(key,)).start()
return cache_obj['data']
3. 缓存穿透(Cache Penetration)
3.1 什么是缓存穿透?
缓存穿透是指查询不存在的数据,导致请求直接访问数据库的现象。可能是恶意攻击或业务逻辑bug导致。
场景示例:
攻击者随机生成不存在的用户ID发起请求,由于缓存中不存在这些数据,所有请求都直接访问数据库。
3.2 解决方案
3.2.1 布隆过滤器(Bloom Filter)
import mmh3
from bitarray import bitarray
class BloomFilter:
def __init__(self, size, hash_count):
self.size = size
self.hash_count = hash_count
self.bit_array = bitarray(size)
self.bit_array.setall(0)
def add(self, item):
for seed in range(self.hash_count):
index = mmh3.hash(item, seed) % self.size
self.bit_array[index] = 1
def exists(self, item):
for seed in range(self.hash_count):
index = mmh3.hash(item, seed) % self.size
if not self.bit_array[index]:
return False
return True
# 使用布隆过滤器
bloom_filter = BloomFilter(1000000, 3)
def get_data_with_bloom(key):
# 先检查布隆过滤器
if not bloom_filter.exists(key):
return None
# 再查缓存
data = redis_client.get(key)
if data:
return data
# 最后查数据库
data = get_from_db(key)
if data:
redis_client.setex(key, 3600, data)
return data
else:
# 数据库也不存在,缓存空值
redis_client.setex(key, 300, "") # 短期缓存空值
return None
3.2.2 缓存空对象
def get_data_with_null_cache(key):
data = redis_client.get(key)
if data is not None:
if data == "": # 空值标记
return None
return data
data = get_from_db(key)
if data:
redis_client.setex(key, 3600, data)
else:
# 缓存空值,防止穿透
redis_client.setex(key, 300, "") # 5分钟过期
return data
3.2.3 接口层校验
def validate_request(key):
# 校验ID格式
if not key.isdigit():
return False
# 校验ID范围
id_num = int(key)
if id_num <= 0 or id_num > 1000000:
return False
return True
4. 综合防护策略
在实际项目中,通常需要组合使用多种方案:
class ComprehensiveCache:
def __init__(self):
self.bloom_filter = BloomFilter(1000000, 3)
self.mutex = CacheMutex()
def get_data(self, key):
# 1. 参数校验
if not validate_request(key):
return None
# 2. 布隆过滤器检查
if not self.bloom_filter.exists(key):
return None
# 3. 查询缓存
data = redis_client.get(key)
if data:
if data == "": # 空值处理
return None
return data
# 4. 使用互斥锁防止击穿
with self.mutex.acquire(key) as acquired:
if not acquired:
time.sleep(0.1)
return self.get_data(key)
# 双重检查
data = redis_client.get(key)
if data:
return data
# 5. 查询数据库
data = get_from_db(key)
if data:
# 设置随机过期时间防止雪崩
expire = 3600 + random.randint(0, 300)
redis_client.setex(key, expire, data)
else:
# 缓存空值防止穿透
redis_client.setex(key, 300, "")
return data
5. 监控与预警
建立完善的监控体系:
-
缓存命中率监控
-
数据库QPS监控
-
慢查询监控
-
缓存key过期监控
总结
缓存问题是高并发系统必须面对的挑战,通过合理的策略组合可以有效应对:
-
缓存雪崩:通过随机过期时间、多级缓存、熔断机制解决
-
缓存击穿:通过互斥锁、逻辑过期时间解决
-
缓存穿透:通过布隆过滤器、缓存空值、参数校验解决
在实际应用中,需要根据业务特点选择合适的方案,并建立完善的监控预警机制,确保系统的稳定性和高性能。
653

被折叠的 条评论
为什么被折叠?



