Redis作为数据库高可用架构中的核心缓存层,缓存穿透、缓存击穿、缓存雪崩是运维中遇到频率最高的三类问题。三者表现相似但根因不同,解决策略也有本质区别。本文从故障现象出发,给出每种问题的诊断方法和工程化解决方案,包含可直接使用的代码实现。
三类缓存故障的区分与诊断
| 问题 | 触发条件 | 影响范围 | 核心特征 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 单个Key | 缓存和数据库都不命中 |
| 缓存击穿 | 热点Key过期瞬间 | 单个Key | 大量请求同时打到数据库 |
| 缓存雪崩 | 大量Key同时过期 | 多个Key | 数据库瞬时压力激增 |
诊断方法:通过Redis的INFO stats和慢查询日志定位。如果keyspace_misses突增且对应Key在数据库中不存在,判定为缓存穿透。如果某个Key的expired事件与数据库QPS激增时间吻合,判定为缓存击穿。如果大量Key的过期时间集中在同一秒,判定为缓存雪崩。
缓存穿透解决方案:布隆过滤器与空值缓存
缓存穿透的根因是请求了根本不存在的数据。攻击者可能利用此特性发起大量无效ID查询,压垮数据库。
方案一:空值缓存——实现简单,适合数据量小的场景。
import redis
import json
class CacheService:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
# 空值缓存过期时间:5分钟,比正常缓存短
self.NULL_CACHE_TTL = 300
self.NORMAL_CACHE_TTL = 3600
def get_user(self, user_id: int):
cache_key = f"user:{user_id}"
# 1. 查询缓存
cached = self.redis.get(cache_key)
if cached is not None:
if cached == "NULL":
return None # 空值缓存命中,直接返回
return json.loads(cached)
# 2. 缓存未命中,查询数据库
user = db_query("SELECT * FROM users WHERE id = %s", user_id)
if user is None:
# 3. 数据库也不存在,缓存空值
self.redis.setex(cache_key, self.NULL_CACHE_TTL, "NULL")
return None
# 4. 正常缓存
self.redis.setex(cache_key, self.NORMAL_CACHE_TTL, json.dumps(user))
return user
方案二:布隆过滤器——适合数据量大且ID范围广的场景。布隆过滤器在内存中以极小代价判断元素”可能存在”或”一定不存在”。
import redis
import mmh3 # MurmurHash3
import math
class BloomFilter:
"""
基于Redis Bitmap的布隆过滤器
"""
def __init__(self, redis_client, key, expected_items=1000000, fpr=0.001):
"""
expected_items: 预期元素数量
fpr: 误判率(false positive rate)
"""
self.redis = redis_client
self.key = key
# 计算需要的bit数和hash函数数量
self.bit_size = int(-expected_items * math.log(fpr) / (math.log(2) ** 2))
self.hash_count = int(self.bit_size / expected_items * math.log(2))
# 初始化Bitmap(确保key存在)
if not self.redis.exists(key):
self.redis.execute_command('SETBIT', key, self.bit_size - 1, 0)
def add(self, item: str):
"""添加元素"""
for i in range(self.hash_count):
# 使用不同种子生成多个hash值
hash_val = mmh3.hash(item, i) % self.bit_size
self.redis.execute_command('SETBIT', self.key, hash_val, 1)
def exists(self, item: str) -> bool:
"""检查元素是否可能存在"""
for i in range(self.hash_count):
hash_val = mmh3.hash(item, i) % self.bit_size
if self.redis.execute_command('GETBIT', self.key, hash_val) == 0:
return False # 一定不存在
return True # 可能存在(有误判率)
def batch_init(self, items):
"""批量初始化(使用pipeline提升性能)"""
pipe = self.redis.pipeline()
for item in items:
for i in range(self.hash_count):
hash_val = mmh3.hash(str(item), i) % self.bit_size
pipe.execute_command('SETBIT', self.key, hash_val, 1)
pipe.execute()
# 使用示例:启动时加载所有用户ID到布隆过滤器
class UserService:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379)
self.bloom = BloomFilter(
self.redis,
key="bloom:users",
expected_items=5000000,
fpr=0.001 # 0.1%误判率
)
def get_user(self, user_id: int):
# 第一道防线:布隆过滤器
if not self.bloom.exists(str(user_id)):
return None # 一定不存在,直接返回
# 通过布隆过滤器后走正常缓存逻辑
cache_key = f"user:{user_id}"
cached = self.redis.get(cache_key)
if cached:
return json.loads(cached)
user = db_query("SELECT * FROM users WHERE id = %s", user_id)
if user:
self.redis.setex(cache_key, 3600, json.dumps(user))
return user
缓存击穿解决方案:互斥锁与逻辑过期
热点Key过期瞬间,大量并发请求同时查询数据库。核心思路是只让一个请求查库,其余请求等待。
import redis
import time
import uuid
import json
class HotKeyCacheService:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379)
self.lock_timeout = 10 # 分布式锁超时秒数
self.cache_ttl = 3600
def get_with_mutex_lock(self, key: str, db_loader):
"""
互斥锁方案:只有一个请求查库,其余等待重试
db_loader: 数据库查询函数,返回数据或None
"""
# 1. 查缓存
data = self.redis.get(key)
if data is not None:
return json.loads(data)
# 2. 缓存未命中,获取分布式锁
lock_key = f"lock:{key}"
lock_value = str(uuid.uuid4()) # 唯一标识用于安全释放锁
acquired = self.redis.set(
lock_key, lock_value,
nx=True, # 只在key不存在时设置
ex=self.lock_timeout
)
if acquired:
try:
# 双重检查:防止队列中前一个请求已经写入缓存
data = self.redis.get(key)
if data is not None:
return json.loads(data)
# 3. 查询数据库
data = db_loader()
if data is not None:
self.redis.setex(key, self.cache_ttl, json.dumps(data))
return data
finally:
# 安全释放锁:Lua脚本保证原子性
release_script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
self.redis.eval(release_script, 1, lock_key, lock_value)
else:
# 4. 未获取到锁,短暂等待后重试
time.sleep(0.1)
return self.get_with_mutex_lock(key, db_loader)
def get_with_logical_expire(self, key: str, db_loader):
"""
逻辑过期方案:缓存永不过期,但数据中包含过期时间戳
适合高并发热点数据,不阻塞任何请求
"""
data = self.redis.get(key)
if data is None:
# 冷启动:查库并写入带逻辑过期时间的缓存
data = db_loader()
if data is not None:
self._set_logical_expire(key, data)
return data
cached = json.loads(data)
if not cached.get('_expired', True):
# 逻辑未过期,直接返回
return cached['_data']
# 逻辑已过期,尝试异步刷新
lock_key = f"lock:{key}"
lock_value = str(uuid.uuid4())
acquired = self.redis.set(lock_key, lock_value, nx=True, ex=10)
if acquired:
# 获取到锁,启动异步刷新线程
import threading
def refresh():
try:
new_data = db_loader()
if new_data is not None:
self._set_logical_expire(key, new_data)
finally:
# 释放锁
release_script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
"""
self.redis.eval(release_script, 1, lock_key, lock_value)
threading.Thread(target=refresh, daemon=True).start()
# 无论是否刷新,都返回旧数据
return cached['_data']
def _set_logical_expire(self, key: str, data):
"""设置带逻辑过期时间的缓存"""
cached = {
'_data': data,
'_expire_at': time.time() + self.cache_ttl,
'_expired': False
}
# 物理不过期,逻辑过期
self.redis.set(key, json.dumps(cached))
缓存雪崩解决方案:过期时间随机化与多级缓存
缓存雪崩的根因是大量Key设置相同过期时间,同一时刻集体失效。数据库分库分表方案中,雪崩可能导致数据库连接池耗尽。
import random
class AntiAvalancheCache:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379)
self.base_ttl = 3600 # 基础过期时间
self.random_range = 600 # 随机偏移范围(±10分钟)
def set_cache(self, key: str, value, ttl=None):
"""
过期时间随机化:基础TTL + 随机偏移
分散Key过期时间点,避免集中失效
"""
if ttl is None:
ttl = self.base_ttl + random.randint(-self.random_range, self.random_range)
self.redis.setex(key, ttl, json.dumps(value))
def batch_set_cache(self, items: dict):
"""
批量设置缓存,使用Pipeline + 随机TTL
items: {key: value, ...}
"""
pipe = self.redis.pipeline()
for key, value in items.items():
ttl = self.base_ttl + random.randint(-self.random_range, self.random_range)
pipe.setex(key, ttl, json.dumps(value))
pipe.execute()
def get_with_fallback(self, key: str, db_loader):
"""
多级缓存:Redis → 本地缓存 → 数据库
Redis不可用时降级到本地缓存
"""
# 第一级:Redis
try:
data = self.redis.get(key)
if data is not None:
return json.loads(data)
except redis.ConnectionError:
log.warning("Redis不可用,降级到本地缓存")
# 第二级:本地缓存(进程内)
if hasattr(self, '_local_cache'):
local_data = self._local_cache.get(key)
if local_data is not None:
return local_data
# 第三级:数据库
data = db_loader()
if data is not None:
self.set_cache(key, data)
# 同时写入本地缓存(TTL更短)
if not hasattr(self, '_local_cache'):
from functools import lru_cache
self._local_cache = {}
self._local_cache[key] = data
return data
数据备份恢复与缓存预热
缓存雪崩后恢复阶段,缓存预热策略决定服务恢复速度。全量预热会造成数据库瞬时压力,应该采用分批分时段预热:
def warmup_cache(batch_size=1000, sleep_interval=0.5):
"""分批预热缓存,避免压垮数据库"""
offset = 0
while True:
# 每次取1000条,按ID排序,分批加载
rows = db_query(
"SELECT id, data FROM items ORDER BY id LIMIT %s OFFSET %s",
batch_size, offset
)
if not rows:
break
pipe = redis.pipeline()
for row in rows:
key = f"item:{row['id']}"
# 随机TTL避免雪崩
ttl = 3600 + random.randint(-300, 300)
pipe.setex(key, ttl, json.dumps(row['data']))
pipe.execute()
offset += batch_size
time.sleep(sleep_interval) # 控制速率
log.info(f"预热进度: {offset} 条")
缓存策略的选择不是非此即彼。生产环境中通常组合使用:布隆过滤器挡住穿透请求,互斥锁或逻辑过期保护热点Key,随机TTL和熔断机制防范雪崩。NoSQL选型应用中,Redis不是唯一缓存方案,对于需要持久化的缓存层,可考虑混合使用Redis和本地缓存(如Caffeine),通过多级缓存架构提升系统弹性。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ce-lyue-shen-du-jie-xi-huan-cun-chuan-tou-ji/