Cache Aside 模式
什么是 Cache Aside 模式
Cache Aside(旁路缓存)是目前最常见的缓存策略,也是 Redis 作为缓存层时最广泛使用的模式。它的核心思想是:应用程序直接管理缓存和数据库的读写,缓存层不主动同步数据。
读操作流程
Cache Aside 的读操作非常简单直接:
应用程序读取数据:
1. 先查缓存(Redis)
├── 命中 → 直接返回缓存数据(快路径)
└── 未命中 → 继续
2. 查询数据库
3. 将查询结果写入缓存(设置过期时间)
4. 返回数据
def read_data(key):
# 1. 查缓存
data = redis.get(key)
if data is not None:
return data
# 2. 缓存未命中,查数据库
data = db.query("SELECT * FROM data WHERE id = ?", key)
if data is not None:
# 3. 写入缓存(设置过期时间)
redis.setex(key, 3600, data)
else:
# 可选:缓存空对象防止穿透
redis.setex(key, 30, "NULL")
# 4. 返回数据
return data
写操作流程
Cache Aside 的写操作采用先更新数据库,后删除缓存的策略:
应用程序更新数据:
1. 更新数据库
2. 删除缓存(而非更新缓存)
def write_data(key, new_value):
# 1. 更新数据库
db.execute("UPDATE data SET value = ? WHERE id = ?", new_value, key)
# 2. 删除缓存
redis.delete(key)
# 注意:不是 redis.set(key, new_value)!
}
为什么写操作是”删除缓存”而非”更新缓存”
问题一:写操作并非总是”写最终值”
考虑一个”++”或”–“操作(如点赞数、库存量):
# 错误做法:更新缓存
def increment_likes(post_id):
# 更新数据库
db.execute("UPDATE posts SET likes = likes + 1 WHERE id = ?", post_id)
# 更新缓存 —— 缓存中是"当前值",但可能是"中间值"
new_likes = redis.incr(f"post:{post_id}:likes")
# 问题:缓存的值总是落后或超前于数据库
更常见的情况是更新了复杂对象:
# 更新对象中的一部分字段
def update_user_avatar(user_id, new_avatar_url):
db.execute("UPDATE users SET avatar = ? WHERE id = ?", new_avatar_url, user_id)
# 如果"更新缓存":需要先查询完整对象再合并字段 → 两次操作
# 如果"删除缓存":直接删除,下次读请求重建 → 一次操作
redis.delete(f"user:{user_id}") # 简单高效
问题二:避免写操作的并发覆盖
之前已经讨论过:
写线程 A:更新 DB → 更新缓存(V1 → V2)
写线程 B:更新 DB → 更新缓存(V2 → V3)
写线程 A:更新 DB 完成,但...
(如果 A 的缓存更新比 B 慢)
结果:缓存中是 V2(A 的旧值),数据库中是 V3(B 的新值)
删除缓存比更新缓存更安全,因为下次读操作一定会获取最新值。
Cache Aside 的优缺点
优点
| 特性 | 说明 |
|---|---|
| 实现简单 | 读写的逻辑清晰,易于理解和维护 |
| 数据安全 | 删除缓存而非更新,避免了并发覆盖 |
| 最终一致 | 配合过期时间,自动恢复一致性 |
| 性能好 | 读操作先查缓存,写操作直接写 DB |
缺点
| 特性 | 说明 |
|---|---|
| 一致性弱 | 写 DB 到删缓存之间有短暂不一致窗口 |
| 缓存命中率 | 写操作频繁会频繁删除缓存,降低命中率 |
| 缓存击穿 | 缓存失效瞬间大量并发打到 DB |
| 无自动同步 | 数据变化需要应用层手动操作 |
Cache Aside 的防护措施
防止缓存击穿
def read_data_with_lock(key):
data = redis.get(key)
if data is not None:
return data
# 互斥锁防止击穿
if redis.set(f"lock:{key}", "1", nx=True, ex=5):
try:
# 双重检查
data = redis.get(key)
if data is not None:
return data
data = db.query(key)
redis.setex(key, 3600, data)
return data
finally:
redis.delete(f"lock:{key}")
else:
time.sleep(0.05)
return redis.get(key) # 等待后重试
防止缓存穿透
def read_data_with_bloom(key):
# 布隆过滤器检查
if not bloom_filter.contains(key):
return None # 一定不存在
# 正常 Cache Aside 流程
data = redis.get(key)
if data is not None:
return data
data = db.query(key)
if data is not None:
redis.setex(key, 3600, data)
else:
redis.setex(key, 30, "NULL") # 缓存空对象
return data
防止缓存雪崩
def read_data_with_random_ttl(key):
data = redis.get(key)
if data is not None:
return data
data = db.query(key)
if data is not None:
# 随机过期时间,防止雪崩
ttl = 3600 + random.randint(-300, 300)
redis.setex(key, ttl, data)
return data
Cache Aside 的变体
Read-Through
和 Cache Aside 相比,Read-Through 将”缓存读取失败后查数据库”的逻辑封装到缓存库中:
Cache Aside:应用层先查缓存,未命中时应用层查数据库
Read-Through:应用层只调用缓存库,缓存库内部处理未命中的逻辑
Write-Through
写操作时,缓存库自动将数据同时写入缓存和数据库:
Cache Aside:应用层主动更新 DB + 主动删除缓存
Write-Through:应用层只写缓存,缓存库同步写入 DB
Write-Behind
写操作先写入缓存,缓存库异步批量写入数据库(延迟写):
Cache Aside:实时写入 DB
Write-Behind:先写缓存,后台异步写 DB
实际应用中的 Cache Aside
class CacheAside(object):
"""Cache Aside 模式封装"""
def __init__(self, redis_client, db_client, ttl=3600):
self.redis = redis_client
self.db = db_client
self.ttl = ttl
def get(self, key, loader_func):
"""
Cache Aside 读取
loader_func: 从数据库加载数据的函数
"""
# 1. 查缓存
data = self.redis.get(key)
if data is not None:
return data
# 2. 缓存未命中,查数据库
data = loader_func()
if data is not None:
# 3. 写入缓存
ttl = self.ttl + random.randint(-300, 300)
self.redis.setex(key, ttl, data)
else:
# 4. 缓存空对象(防止穿透)
self.redis.setex(key, 30, "NULL")
return data
def set(self, key, value, db_func):
"""
Cache Aside 写入
db_func: 更新数据库的函数
"""
# 1. 更新数据库
db_func(value)
# 2. 删除缓存
self.redis.delete(key)
面试话术
当面试官问”你是如何保证缓存和数据库一致性的”,可以这样回答:
“我们使用的是标准的 Cache Aside 模式。读取时先查缓存,未命中再查数据库并回写缓存;写入时先更新数据库,然后删除缓存。这样做的原因是:删除缓存比更新缓存更安全,能避免并发写操作的覆盖问题。每次缓存被删除后,下一次读操作会自动加载最新数据,保证了最终一致性。在这个基础上,我们还设置了随机的缓存过期时间来防止缓存雪崩,并在高并发场景下使用互斥锁来防止缓存击穿。”
总结
Cache Aside 是 Redis 缓存的最标准、最推荐的使用模式。它的核心设计理念是:让应用层显式管理缓存的生命周期,通过”先写 DB 后删缓存”的机制保证数据最终一致。虽然它不能提供强一致性,但在绝大多数业务场景中,Cache Aside 提供的最终一致性 + 高性能组合是最佳选择。
理解 Cache Aside 模式,是掌握 Redis 缓存应用的基础,也是面试中回答”缓存和数据库一致性”问题的标准答案框架。


暂无评论内容