缓存设计模式(Cache Design Pattern)是一种用于存储和管理频繁访问数据的技术,旨在提高系统性能、降低数据库或后端服务的负载,并减少数据访问延迟。以下是几种常见的缓存设计模式,并用 Python + Redis 进行示例代码实现:
适用场景:
逻辑流程:
import redis
import time
redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)
def get_data_cache_aside(key):
cache_key = f"cache:{key}"
# Step 1: 先查询缓存
data = redis_client.get(cache_key)
if data:
return data # 直接返回缓存数据
# Step 2: 缓存未命中,查询数据库
data = query_database(key)
# Step 3: 回填缓存
if data:
redis_client.setex(cache_key, 60, data) # 缓存 60s
return data
def query_database(key):
"""模拟数据库查询"""
time.sleep(1) # 模拟查询时间
return f"data_for_{key}"
# 测试
print(get_data_cache_aside("hot_item"))
存在的问题:旁路缓存(Cache Aside)如果数据库中的数据为空(例如,数据确实不存在或者被删除了),那么每次查询都会缓存未命中,导致系统一直查数据库,这就是缓存穿透(Cache Penetration)问题。
null
或特殊占位符),并设置较短的 TTL(如 5~10 秒)。import redis
import time
redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)
def get_data_cache_aside(key):
cache_key = f"cache:{key}"
# Step 1: 查询缓存
data = redis_client.get(cache_key)
if data is not None: # 即使数据为空(缓存的空值),也不会去数据库查询
return None if data == "NULL" else data
# Step 2: 查询数据库
data = query_database(key)
# Step 3: 数据为空,缓存“空值”并设置较短过期时间
if data is None:
redis_client.setex(cache_key, 10, "NULL") # 10秒缓存空值,避免短时间重复查库
return None
# Step 4: 数据有效,回填缓存
redis_client.setex(cache_key, 60, data) # 60秒缓存数据
return data
def query_database(key):
"""模拟数据库查询"""
time.sleep(1) # 模拟数据库查询时间
return None # 假设数据不存在
# 测试
print(get_data_cache_aside("hot_item")) # 第一次查数据库
print(get_data_cache_aside("hot_item")) # 直接命中缓存(返回 None,不查数据库)
✅ 效果:
None
,缓存 "NULL"
并设置 10 秒过期。None
,不会重复访问数据库。None
,不查询数据库。from bloom_filter import BloomFilter # 需要安装 pip install bloom-filter2
# 初始化布隆过滤器(假设有 10 万个 Key,误判率 0.01)
bloom = BloomFilter(max_elements=100000, error_rate=0.01)
# 预先加入一些存在的 Key
bloom.add("valid_key_1")
bloom.add("valid_key_2")
def get_data_bloom_filter(key):
cache_key = f"cache:{key}"
# Step 1: 先检查布隆过滤器,数据可能不存在
if key not in bloom:
return None # 直接返回,不查数据库
# Step 2: 查询缓存
data = redis_client.get(cache_key)
if data is not None:
return None if data == "NULL" else data
# Step 3: 查询数据库
data = query_database(key)
# Step 4: 缓存数据或空值
if data is None:
redis_client.setex(cache_key, 10, "NULL")
return None
redis_client.setex(cache_key, 60, data)
return data
# 测试
print(get_data_bloom_filter("invalid_key")) # 布隆过滤器拦截,直接返回 None
print(get_data_bloom_filter("valid_key_1")) # 查询缓存或数据库
✅ 效果:
from collections import defaultdict
request_count = defaultdict(int)
def get_data_with_rate_limit(key):
cache_key = f"cache:{key}"
# Step 1: 统计 Key 查询次数
request_count[key] += 1
if request_count[key] > 100: # 限制单个 Key 短时间内查询次数
return "Too Many Requests" # 直接拒绝请求
# Step 2: 查询缓存
data = redis_client.get(cache_key)
if data is not None:
return None if data == "NULL" else data
# Step 3: 查询数据库
data = query_database(key)
# Step 4: 缓存数据或空值
if data is None:
redis_client.setex(cache_key, 10, "NULL")
return None
redis_client.setex(cache_key, 60, data)
return data
✅ 效果:
方案 | 适用场景 | 优势 | 缺点 |
---|---|---|---|
缓存空值 | 任何缓存穿透情况 | 简单高效,防止短时间重复查询 | 额外占用 Redis 空间 |
布隆过滤器 | 大量 Key 查询,如用户 ID、商品 ID | 高效过滤无效 Key,减少数据库压力 | 需要额外存储布隆过滤器 |
限流 & 黑名单 | 恶意攻击、异常高频请求 | 防止恶意攻击,减少数据库压力 | 需要额外维护限流规则 |
推荐方案组合:
适用场景:
逻辑流程:
class Cache:
def __init__(self):
self.redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)
def get(self, key):
"""从缓存读取数据"""
data = self.redis_client.get(key)
if not data:
data = self.load_from_db(key) # 由缓存层自动加载
self.redis_client.setex(key, 60, data) # 缓存 60s
return data
def load_from_db(self, key):
"""模拟数据库查询"""
time.sleep(1) # 模拟查询时间
return f"data_for_{key}"
# 测试
cache = Cache()
print(cache.get("hot_item"))
存在的问题:读穿透(Read-Through)同样可能会遇到类似的缓存穿透问题。如果缓存未命中且查询结果为空(例如,数据库中没有该数据),那么每次查询都会去数据库查询,造成重复查询数据库的情况。
与旁路缓存的解决方案一致
适用场景:
逻辑流程:
class Cache:
def __init__(self):
self.redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)
def set(self, key, value):
"""写入缓存 + 数据库"""
self.redis_client.setex(key, 60, value) # 先写入缓存
self.write_to_db(key, value) # 再写入数据库
def get(self, key):
"""读取缓存"""
return self.redis_client.get(key)
def write_to_db(self, key, value):
"""模拟数据库写入"""
print(f"数据 {key} 已写入数据库: {value}")
# 测试
cache = Cache()
cache.set("user:123", "UserData")
print(cache.get("user:123"))
存在问题:
一致性问题:缓存和数据库可能在短时间内处于不同步的状态。例如,如果缓存成功写入,但数据库写入失败,数据就不一致了。为了避免这种情况,你可能需要引入一些机制来确保最终一致性,如 异步写入 或 消息队列 来处理数据库更新。
错误处理和重试:如果数据库写入失败,如何处理这个问题是一个关键点。可以考虑将数据库写入操作异步化,或者通过定期的任务检查数据一致性并做修复。
适用场景:
逻辑流程:
import threading
class Cache:
def __init__(self):
self.redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)
self.batch_data = {} # 临时存储待写入的数据
def set(self, key, value):
"""写入缓存"""
self.redis_client.setex(key, 60, value) # 先写入缓存
self.batch_data[key] = value # 添加到待写入数据库的批量任务
def get(self, key):
"""读取缓存"""
return self.redis_client.get(key)
def batch_write_to_db(self):
"""批量写入数据库(定期执行)"""
while True:
if self.batch_data:
for key, value in self.batch_data.items():
print(f"批量写入数据库: {key} -> {value}")
self.batch_data.clear()
time.sleep(5) # 每 5 秒写入一次
# 启动异步写线程
cache = Cache()
threading.Thread(target=cache.batch_write_to_db, daemon=True).start()
# 测试
cache.set("user:123", "UserData")
cache.set("user:124", "UserData2")
print(cache.get("user:123"))
time.sleep(6) # 等待异步写入数据库
适用场景:
逻辑流程:
import uuid
def get_data_with_mutex(key):
cache_key = f"cache:{key}"
lock_key = f"lock:{key}"
lock_value = str(uuid.uuid4())
# 先查询缓存
data = redis_client.get(cache_key)
if data:
return data
# 尝试获取锁
if redis_client.set(lock_key, lock_value, nx=True, ex=5):
try:
# 再次检查缓存
data = redis_client.get(cache_key)
if data:
return data
# 查询数据库
data = query_database(key)
# 回填缓存
redis_client.setex(cache_key, 60, data)
return data
finally:
if redis_client.get(lock_key) == lock_value:
redis_client.delete(lock_key)
else:
# 其他线程等待后重试
time.sleep(0.2)
return get_data_with_mutex(key)
# 测试
print(get_data_with_mutex("hot_item"))
设计模式 | 适用场景 | 主要特点 |
---|---|---|
Cache Aside | 读多写少 | 业务代码控制缓存逻辑 |
Read-Through | 读写频繁 | 读请求只访问缓存,自动回填 |
Write-Through | 写多读少 | 写请求先更新缓存,再写数据库 |
Write-Behind | 高并发写 | 先写缓存,异步批量写数据库 |
分布式锁 | 缓存击穿 | 互斥锁防止多个线程并发查询数据库 |
不同的缓存模式可以根据实际的业务需求和系统架构进行组合使用。组合的依据主要取决于以下几个因素:
不同缓存模式的选择和组合通常取决于对数据一致性的要求:
缓存雪崩(Cache Avalanche):当大量缓存同时失效时,可能导致大量请求直接打到数据库上,造成压力。通过 设置不同的过期时间 或 分布式缓存策略,可以避免缓存雪崩现象。组合使用 缓存分片(Sharded Cache) 和 缓存预热 等方式,有助于平衡缓存更新带来的负担。
缓存穿透(Cache Penetration):如果某些数据根本不在缓存中(例如查询不存在的用户信息),则每次都会访问数据库。可以通过 布隆过滤器(Bloom Filter) 或其他方式来过滤这些请求,减少数据库压力。
def read_through_with_ttl(key):
value = cache.get(key)
if value is None:
value = db.get(key)
cache.set(key, value, ex=3600) # 1 小时后过期
return value
def cache_aside_lru(key):
value = cache.get(key)
if value is None:
value = db.get(key)
cache.set(key, value)
return value
def write_back_with_ttl(key, value):
cache.set(key, value) # 先写缓存
write_queue.append((key, value)) # 异步写数据库
cache.expire(key, 3600) # 设置过期时间
缓存模式组合的依据主要取决于你的应用场景,特别是数据一致性要求、性能需求、读取/写入频率、以及缓存过期策略等因素。在实际开发中,可以根据具体的需求灵活选择或组合这些模式,以达到最佳的系统性能和数据一致性。