📚 文章概述
缓存是Redis最重要的应用场景之一。合理的缓存设计可以大幅提升系统性能,但不当的缓存使用也会带来各种问题。本文将深入讲解缓存设计模式、缓存穿透、缓存击穿、缓存雪崩等经典问题及其解决方案,帮助读者设计出高性能、高可用的缓存架构。
一、理论部分
1.1 缓存设计模式
1.1.1 Cache-Aside模式(旁路缓存)
工作流程:
Client Cache Database 读取数据 返回数据 未找到 查询数据库 返回数据 写入缓存 alt [缓存命中] [缓存未命中] 写操作 更新数据 成功 删除缓存 Client Cache Database
特点:
- 应用负责缓存和数据库的读写
- 缓存和数据库独立
- 实现简单,灵活性高
1.1.2 Read-Through模式(读穿透)
工作流程:
Client Cache Database 读取数据 返回数据 查询数据库 返回数据 写入缓存 返回数据 alt [缓存命中] [缓存未命中] Client Cache Database
特点:
- 缓存服务负责从数据库加载数据
- 对应用透明
- 需要缓存服务支持
1.1.3 Write-Through模式(写穿透)
工作流程:
Client Cache Database 写入数据 写入数据库 成功 更新缓存 成功 Client Cache Database
特点:
- 同时更新缓存和数据库
- 数据一致性高
- 写性能较低
1.1.4 Write-Behind模式(写回)
工作流程:
Client Cache Database 写入数据 更新缓存 成功(立即返回) 异步写入 异步写入数据库 成功 Client Cache Database
特点:
- 先更新缓存,异步写入数据库
- 写性能高
- 可能丢失数据
1.1.5 模式对比
| 模式 | 读性能 | 写性能 | 一致性 | 复杂度 |
|---|---|---|---|---|
| Cache-Aside | 高 | 高 | 中 | 低 |
| Read-Through | 高 | 中 | 高 | 中 |
| Write-Through | 高 | 低 | 高 | 中 |
| Write-Behind | 高 | 很高 | 低 | 高 |
1.2 缓存穿透
1.2.1 问题描述
缓存穿透场景:
未命中 不存在 客户端请求 查询缓存 查询数据库 返回空 不写入缓存 恶意请求 大量不存在的数据 缓存穿透 数据库压力大
问题特征:
- 查询不存在的数据
- 缓存和数据库都不命中
- 大量请求直接打到数据库
1.2.2 解决方案
方案1:缓存空值
查询不存在数据 缓存空值 设置短过期时间 后续请求命中缓存
实现:
python
def get_user(user_id):
# 先查缓存
user = cache.get(f'user:{user_id}')
if user is not None:
return user if user != '' else None
# 查数据库
user = db.get_user(user_id)
if user:
cache.set(f'user:{user_id}', user, 3600)
else:
# 缓存空值,短过期时间
cache.set(f'user:{user_id}', '', 60)
return user
方案2:布隆过滤器
不存在 可能存在 未命中 请求 布隆过滤器 直接返回 查询缓存 查询数据库
实现:
python
from redisbloom import Client
rb = Client(host='localhost', port=6379)
# 初始化布隆过滤器
rb.bfCreate('users', 0.01, 1000000)
# 添加数据
rb.bfAdd('users', user_id)
# 检查存在性
if rb.bfExists('users', user_id):
# 可能存在,继续查询
user = get_user(user_id)
else:
# 一定不存在
return None
1.3 缓存击穿
1.3.1 问题描述
缓存击穿场景:
Client1 Client2 Client3 Cache Database 热点数据过期 查询热点数据 未命中 查询数据库 查询热点数据 未命中 查询数据库 查询热点数据 未命中 查询数据库 大量并发请求 Client1 Client2 Client3 Cache Database
问题特征:
- 热点数据过期
- 大量并发请求
- 数据库压力瞬间增大
1.3.2 解决方案
方案1:互斥锁
Client1 Client2 Cache Database 查询数据 未命中 获取锁 获取成功 查询数据 未命中 获取锁 获取失败 查询数据库 返回数据 写入缓存 释放锁 等待后重试 返回数据 Client1 Client2 Cache Database
实现:
python
import redis
import time
def get_user_with_lock(user_id):
# 先查缓存
user = cache.get(f'user:{user_id}')
if user:
return user
# 尝试获取锁
lock_key = f'lock:user:{user_id}'
if cache.set(lock_key, '1', nx=True, ex=10):
try:
# 查数据库
user = db.get_user(user_id)
if user:
cache.set(f'user:{user_id}', user, 3600)
return user
finally:
cache.delete(lock_key)
else:
# 等待后重试
time.sleep(0.1)
return get_user_with_lock(user_id)
方案2:永不过期 + 异步更新
python
def get_user_never_expire(user_id):
# 缓存永不过期
user = cache.get(f'user:{user_id}')
if user:
# 异步检查是否需要更新
if time.time() - user['update_time'] > 3600:
# 异步更新
async_update_user(user_id)
return user
# 首次加载
user = db.get_user(user_id)
if user:
cache.set(f'user:{user_id}', {
'data': user,
'update_time': time.time()
})
return user
1.4 缓存雪崩
1.4.1 问题描述
缓存雪崩场景:
大量缓存同时过期 大量请求 缓存未命中 请求数据库 数据库压力激增 数据库崩溃 服务不可用
问题特征:
- 大量缓存同时过期
- 大量请求打到数据库
- 可能导致数据库崩溃
1.4.2 解决方案
方案1:过期时间随机化
python
import random
def set_cache(key, value, base_ttl=3600):
# 基础过期时间 + 随机时间(0-600秒)
ttl = base_ttl + random.randint(0, 600)
cache.set(key, value, ttl)
方案2:多级缓存
未命中 未命中 请求 本地缓存 L1 Redis缓存 L2 数据库
方案3:缓存预热
python
def cache_warmup():
# 系统启动时预热热点数据
hot_keys = get_hot_keys()
for key in hot_keys:
data = db.get_data(key)
cache.set(key, data, 3600)
方案4:限流降级
python
from redis import Redis
def get_user_with_limit(user_id):
# 限流:每秒最多100个请求
if not rate_limit('user_query', 100, 1):
# 降级:返回默认值或错误
return get_default_user()
return get_user(user_id)
1.5 缓存更新策略
1.5.1 更新策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 先更新数据库,再删除缓存 | 简单 | 可能短暂不一致 | 一般场景 |
| 先删除缓存,再更新数据库 | 简单 | 可能短暂不一致 | 一般场景 |
| 先更新数据库,再更新缓存 | 一致性高 | 可能浪费更新 | 一致性要求高 |
| 延迟双删 | 一致性较好 | 实现复杂 | 一致性要求高 |
1.5.2 延迟双删策略
流程:
Client Cache Database 删除缓存 更新数据库 成功 延迟一段时间 再次删除缓存 确保一致性 Client Cache Database
实现:
python
import threading
import time
def update_user(user_id, data):
# 第一次删除缓存
cache.delete(f'user:{user_id}')
# 更新数据库
db.update_user(user_id, data)
# 延迟删除缓存
def delayed_delete():
time.sleep(1)
cache.delete(f'user:{user_id}')
threading.Thread(target=delayed_delete).start()
1.6 缓存预热与降级
1.6.1 缓存预热
预热策略:
系统启动 加载热点数据 写入缓存 系统就绪 定时任务 更新热点数据
1.6.2 缓存降级
降级策略:
直接查数据库 返回默认值 返回错误 缓存异常 降级策略 性能下降 功能降级 服务降级
二、实践指南
2.1 缓存设计实践
2.1.1 缓存键设计
键命名规范:
业务:模块:功能:标识
示例:
python
# 用户信息
user:info:{user_id}
# 商品详情
product:detail:{product_id}
# 订单列表
order:list:{user_id}:{page}
2.1.2 缓存过期时间设计
过期时间策略:
- 热点数据:较长过期时间(1-24小时)
- 普通数据:中等过期时间(10-60分钟)
- 实时性要求高:短过期时间(1-10分钟)
2.2 问题解决方案实践
2.2.1 防止缓存穿透
python
def get_user_safe(user_id):
# 使用布隆过滤器
if not bloom_filter.exists('users', user_id):
return None
# 查询缓存
user = cache.get(f'user:{user_id}')
if user is not None:
return user if user != '' else None
# 查询数据库
user = db.get_user(user_id)
if user:
cache.set(f'user:{user_id}', user, 3600)
else:
# 缓存空值
cache.set(f'user:{user_id}', '', 60)
return user
2.2.2 防止缓存击穿
python
def get_hot_data(key):
# 使用互斥锁
lock_key = f'lock:{key}'
if cache.set(lock_key, '1', nx=True, ex=10):
try:
data = cache.get(key)
if not data:
data = db.get_data(key)
cache.set(key, data, 3600)
return data
finally:
cache.delete(lock_key)
else:
# 等待后重试
time.sleep(0.1)
return cache.get(key)
2.2.3 防止缓存雪崩
python
import random
def set_cache_safe(key, value, base_ttl=3600):
# 随机过期时间
ttl = base_ttl + random.randint(0, 600)
cache.set(key, value, ttl)
三、总结
3.1 关键知识点回顾
-
缓存设计模式
- Cache-Aside:最常用
- Read-Through/Write-Through:一致性高
- Write-Behind:性能高
-
缓存问题
- 缓存穿透:查询不存在数据
- 缓存击穿:热点数据过期
- 缓存雪崩:大量缓存同时过期
-
解决方案
- 穿透:布隆过滤器、缓存空值
- 击穿:互斥锁、永不过期
- 雪崩:随机过期、多级缓存
3.2 最佳实践
- 合理设计缓存键
- 设置合适的过期时间
- 使用布隆过滤器防止穿透
- 使用互斥锁防止击穿
- 随机化过期时间防止雪崩
下一篇预告: 第9篇将深入讲解Redis分布式锁与分布式ID,包括分布式锁实现、Redlock算法和分布式ID生成策略。