Redis缓存架构设计原则
Redis作为内存数据库,读写性能在10万QPS以上,是缓解数据库压力的核心组件。缓存架构设计的核心是在命中率、一致性和可用性之间取得平衡。命中率低则缓存形同虚设,一致性差则业务数据出错,可用性差则缓存一旦宕机数据库立刻被打垮。以下从缓存穿透、缓存雪崩、持久化方案三个维度展开实战方案。
缓存穿透防护实战
缓存穿透指大量请求查询一个数据库中也不存在的数据,每次请求都穿透缓存直接打到数据库。典型场景:爬虫遍历ID、恶意攻击、业务删除数据后未清理缓存。
方案一:布隆过滤器
在Redis前面加一层布隆过滤器,所有可能存在的数据哈希映射到一个足够大的bitmap中。请求进来先查布隆过滤器,判断不存在的直接拒绝,判断存在的再查Redis和数据库。布隆过滤器有误判率(不存在可能判为存在),但不会漏判(存在一定判为存在):
import mmh3
import math
class BloomFilter:
def __init__(self, capacity: int, error_rate: float = 0.001):
"""capacity: 预计元素数量, error_rate: 期望误判率"""
self.size = int(-capacity * math.log(error_rate) / (math.log(2) ** 2))
self.hash_count = int(self.size / capacity * math.log(2))
self.bit_array = bytearray(math.ceil(self.size / 8))
def _get_offsets(self, key: str) -> list[int]:
offsets = []
for i in range(self.hash_count):
h = mmh3.hash(str(key), i) % self.size
offsets.append(h)
return offsets
def add(self, key: str):
for offset in self._get_offsets(key):
byte_idx = offset // 8
bit_idx = offset % 8
self.bit_array[byte_idx] |= (1 << bit_idx)
def exists(self, key: str) -> bool:
for offset in self._get_offsets(key):
byte_idx = offset // 8
bit_idx = offset % 8
if not (self.bit_array[byte_idx] & (1 << bit_idx)):
return False
return True
# 使用:加载所有合法ID到布隆过滤器
bf = BloomFilter(capacity=10_000_000, error_rate=0.001)
for item_id in db_query_all_ids():
bf.add(str(item_id))
def get_item(item_id: str):
if not bf.exists(item_id):
return None # 布隆过滤器判定不存在,直接返回
return cache_or_db_get(item_id)
方案二:缓存空值
当数据库查询结果为空时,将空结果缓存起来,设置较短的过期时间(60-300秒)。这样重复请求不会穿透到数据库:
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def get_user(user_id: str) -> dict | None:
cache_key = f"user:{user_id}"
# 查缓存
cached = r.get(cache_key)
if cached is not None:
if cached == "NULL":
return None # 缓存的空值
return json.loads(cached)
# 查数据库
user = db_query_user(user_id)
if user is None:
# 缓存空值,TTL设短(120秒),避免占用过多内存
r.setex(cache_key, 120, "NULL")
return None
# 缓存真实数据,TTL较长
r.setex(cache_key, 3600, json.dumps(user))
return user
缓存雪崩预防方案
缓存雪崩指大量缓存key在同一时刻过期,或Redis节点宕机,瞬间大量请求涌入数据库。与穿透不同,雪崩的请求查询的是合法存在的数据,只是缓存恰好失效了。
方案一:过期时间加随机抖动
设置缓存TTL时加上随机偏移量,避免大量key在同一秒过期:
import random
import redis
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def set_cache_with_jitter(key: str, value: str, base_ttl: int, jitter_range: int = 300):
"""base_ttl: 基础过期时间(秒), jitter_range: 随机偏移范围(秒)"""
ttl = base_ttl + random.randint(0, jitter_range)
r.setex(key, ttl, value)
# 示例:基础TTL 1小时,加0-5分钟随机抖动
set_cache_with_jitter("user:1001", user_data, 3600, 300)
set_cache_with_jitter("user:1002", user_data, 3600, 300)
# 两个key的过期时间分别为3723秒和3891秒,不会同时失效
方案二:缓存永不过期 + 异步更新
缓存不设TTL,通过后台任务定期刷新。即使刷新任务失败,旧数据仍在缓存中可用,不会击穿到数据库:
import threading
import time
import redis
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def async_refresh(key: str, ttl_key: str, refresh_func, refresh_interval: int = 1800):
"""异步刷新缓存:检查逻辑过期时间,过期则触发后台刷新"""
# 检查逻辑过期标记
expire_at = r.get(ttl_key)
if expire_at and int(expire_at) > time.time():
return # 未过期,直接返回
# 标记为正在刷新(防并发刷新)
lock_key = f"refreshing:{key}"
if r.set(lock_key, "1", nx=True, ex=30): # 30秒分布式锁
try:
# 后台线程刷新数据
def refresh():
try:
new_data = refresh_func()
r.set(key, json.dumps(new_data))
r.set(ttl_key, int(time.time() + refresh_interval))
finally:
r.delete(lock_key)
threading.Thread(target=refresh, daemon=True).start()
except Exception:
r.delete(lock_key)
方案三:多级缓存 + 熔断降级
本地缓存(如Python的cachetools、Go的bigcache)作为L1,Redis作为L2,数据库作为L3。Redis不可用时降级到本地缓存:
from cachetools import TTLCache
import redis
import json
# L1本地缓存:容量1000,TTL 60秒
local_cache = TTLCache(maxsize=1000, ttl=60)
# L2 Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def get_data(key: str, db_query_func) -> dict | None:
# L1: 本地缓存
if key in local_cache:
return local_cache[key]
# L2: Redis
try:
cached = redis_client.get(key)
if cached is not None:
data = json.loads(cached)
local_cache[key] = data # 回填L1
return data
except redis.ConnectionError:
pass # Redis不可用,降级继续
# L3: 数据库
data = db_query_func(key)
if data is not None:
try:
redis_client.setex(key, 3600, json.dumps(data))
except redis.ConnectionError:
pass # Redis不可用,只回填L1
local_cache[key] = data
return data
Redis持久化方案对比与选型
Redis数据存在内存中,进程退出后数据丢失。持久化方案决定了数据恢复能力和性能开销:
RDB(快照):定期将内存数据全量写入磁盘dump.rdb文件。优点是文件紧凑、恢复速度快(直接加载二进制文件)、对性能影响小(fork子进程写)。缺点是两次快照之间的数据可能丢失、fork大内存实例时可能卡顿。
AOF(Append Only File):将每个写命令追加到日志文件。优点是数据安全性高(最多丢1秒数据)、文件可读性好。缺点是文件体积大、恢复速度慢(重放所有命令)、对性能有一定影响。
混合持久化(推荐):RDB + AOF结合。Redis 4.0+支持在AOF重写时将当前数据以RDB格式写入AOF文件头部,后续增量命令追加在后面:
# redis.conf 配置
# 开启AOF
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec # 每秒刷盘,兼顾安全和性能
# 开启混合持久化
aof-use-rdb-preamble yes
# RDB配置(作为备份补充)
save 900 1 # 900秒内有1次修改则快照
save 300 10 # 300秒内有10次修改则快照
save 60 10000 # 60秒内有10000次修改则快照
恢复时先加载AOF文件头部的RDB数据(快速加载大部分数据),再重放尾部增量命令(补齐最新数据),恢复速度比纯AOF快数倍。
Redis持久化方案选型决策
根据业务场景选择持久化策略:
纯缓存场景:关闭持久化(save "",appendonly no),数据全可从数据库重新加载,追求极致性能。
会话存储/计数器:开启AOF,appendfsync everysec,最多丢1秒数据可接受。
排行榜/社交关系:混合持久化,RDB做定时全量快照便于快速恢复,AOF保证增量不丢。
消息队列:只开RDB,消息有消费确认机制,丢失可重发,不需要AOF的高开销。
主从复制场景:主节点关闭持久化(减少fork开销),从节点开启持久化(负责数据备份)。主节点宕机由从节点提升为主,数据从从节点的持久化文件恢复。主节点不做持久化是为了避免fork大内存实例导致的延迟毛刺——一个32GB内存的Redis实例fork时需要复制页表,可能产生数百毫秒的停顿。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-yu-xue-beng-fang-hu-shi-zhan-bu/