缓存设计:这次怎么落地的
热点接口的 QPS 从几百涨到几千,最先顶不住的是数据库。我在应用里加了 Guava 本地缓存,命中率不错,但多实例部署后各节点各存一份,穿透和击穿的问题立刻冒出来。后面才逐步引入 Bloom Filter、Redis 和更完整的缓存架构。
本地缓存
为什么先用本地缓存
本地缓存最简单,不需要额外部署。
// 使用 Guava Cache
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
// 使用缓存
String value = cache.getIfPresent("key");
if (value == null) {
value = loadFromDatabase("key");
cache.put("key", value);
}
优势:
- 速度快,无网络开销
- 实现简单
- 成本低
劣势:
- 每个实例都有独立缓存,数据不一致
- 内存有限
- 无法共享缓存
本地缓存适用场景
适合:
- 配置数据
- 静态数据
- 热点数据(且只读)
- 单实例部署
不适合:
- 多实例部署
- 需要数据一致性
- 缓存数据量大
Redis 分布式缓存
为什么需要 Redis
多实例部署后,本地缓存数据不一致,必须用分布式缓存。
# 部署 Redis
docker run -d --name redis -p 6379:6379 redis:latest
基础操作
import redis
# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 设置缓存
r.set('user:123', json.dumps({'name': 'Alice', 'age': 30}))
r.expire('user:123', 3600) # 1 小时过期
# 获取缓存
data = r.get('user:123')
if data:
user = json.loads(data)
else:
user = load_from_database(123)
r.set('user:123', json.dumps(user))
r.expire('user:123', 3600)
缓存穿透
大量请求查询不存在的数据,缓存没命中,直接打到数据库。
解决:布隆过滤器
from pybloom_live import BloomFilter
# 初始化布隆过滤器
bf = BloomFilter(capacity=1000000, error_rate=0.001)
# 预加所有存在的 key
for key in all_keys:
bf.add(key)
# 查询时先检查布隆过滤器
if key not in bf:
return None # 一定不存在,不需要查数据库
缓存击穿
热点 key 过期,大量请求同时打到数据库。
解决:互斥锁
import time
from threading import Lock
lock = Lock()
def get_with_lock(key):
# 尝试获取锁
if lock.acquire(blocking=False):
try:
value = r.get(key)
if value is None:
value = load_from_database(key)
r.set(key, value)
r.expire(key, 3600)
return value
finally:
lock.release()
else:
# 没拿到锁,等待后重试
time.sleep(0.1)
return get_with_lock(key)
缓存雪崩
大量 key 同时过期,数据库压力骤增。
解决:随机过期时间
import random
def set_cache_with_random_ttl(key, value, base_ttl=3600):
# 在基础时间上加随机偏移
ttl = base_ttl + random.randint(0, 300) # ±5 分钟
r.set(key, value)
r.expire(key, ttl)
缓存更新策略
Cache Aside Pattern
应用负责维护缓存一致性。
def update_user(user_id, user_data):
# 1. 更新数据库
db.update('users', user_id, user_data)
# 2. 删除缓存
r.delete(f'user:{user_id}')
Write Through Pattern
应用写入时同步更新缓存。
def update_user(user_id, user_data):
# 1. 更新数据库
db.update('users', user_id, user_data)
# 2. 更新缓存
r.set(f'user:{user_id}', json.dumps(user_data))
r.expire(f'user:{user_id}', 3600)
Write Behind Pattern
应用写入后异步更新缓存。
def update_user(user_id, user_data):
# 1. 更新数据库
db.update('users', user_id, user_data)
# 2. 异步更新缓存
threading.Thread(
target=lambda: (
r.set(f'user:{user_id}', json.dumps(user_data)),
r.expire(f'user:{user_id}', 3600)
)
).start()
多级缓存
架构设计
实现
class MultiLevelCache:
def __init__(self):
self.local_cache = {}
self.redis = redis.Redis()
def get(self, key):
# L1: 本地缓存
if key in self.local_cache:
return self.local_cache[key]
# L2: Redis 缓存
value = self.redis.get(key)
if value:
self.local_cache[key] = value
return value
# L3: 数据库
value = self.load_from_database(key)
if value:
self.redis.set(key, value)
self.redis.expire(key, 3600)
self.local_cache[key] = value
return value
def delete(self, key):
self.local_cache.pop(key, None)
self.redis.delete(key)
缓存一致性
最终一致性
缓存和数据库不需要强一致,最终一致即可。
def update_data(key, value):
# 1. 更新数据库
db.update(key, value)
# 2. 发送消息到消息队列
mq.send('cache_invalidate', {'key': key})
# 消费者
def process_message(message):
key = message['key']
cache.delete(key)
强一致性
需要使用分布式锁,确保缓存和数据库原子更新。
import redis.lock
def update_with_consistency(key, value):
lock = redis.lock.RedisLock(redis, f'lock:{key}')
with lock:
# 1. 更新数据库
db.update(key, value)
# 2. 更新缓存
cache.set(key, value)
踩过的坑
坑一:缓存过期时间没设好
一开始没设过期时间,缓存数据越来越多,Redis 内存满了。
解决:合理设置过期时间,定期清理。
# 设置合理的过期时间
r.set('user:123', json.dumps(user))
r.expire('user:123', 3600) # 热点数据 1 小时
r.expire('config:system', 86400) # 配置数据 1 天
坑二:大 Value 问题
缓存了很大的对象,导致 Redis 性能下降。
解决:
- 拆分大对象
- 只缓存必要字段
- 使用压缩
# 只缓存必要字段
def get_user_summary(user_id):
user = db.get_user(user_id)
return {
'id': user['id'],
'name': user['name'],
'avatar': user['avatar']
}
# 使用压缩
import gzip
def set_compressed(key, value):
compressed = gzip.compress(json.dumps(value).encode())
r.set(key, compressed)
def get_compressed(key):
compressed = r.get(key)
return json.loads(gzip.decompress(compressed).decode())
坑三:缓存冷启动
系统重启后,缓存是空的,大量请求打到数据库。
解决:
- 预热缓存
- 限流保护
- 灰度启动
# 预热缓存
def warm_up_cache():
for user_id in all_user_ids:
user = db.get_user(user_id)
cache.set(f'user:{user_id}', json.dumps(user))
cache.expire(f'user:{user_id}', 3600)
# 限流保护
from ratelimit import limits
@limits(calls=100, period=60) # 每分钟最多 100 次调用
def get_user(user_id):
return cache.get_or_load(f'user:{user_id}', lambda: db.get_user(user_id))
写在最后
缓存这东西,不是万能药,用不好反而会带来问题。
解决了:
- 数据库压力
- 响应速度
- 吞吐量
带来了:
- 一致性问题
- 复杂度增加
- 运维成本
实施之前先评估:
- 数据读写比例
- 数据一致性要求
- 缓存命中率
- 运维能力
不是所有场景都需要缓存,有时候优化数据库查询就够用。
这次缓存优化花了两周,从本地缓存到多级缓存,再到缓存一致性处理。优化完成后,数据库 QPS 从 1000 降到 200,响应时间从 500ms 降到 50ms。
版权声明: 本文首发于 指尖魔法屋-缓存设计:这次怎么落地的(https://blog.thinkmoon.cn/post/63-cache-design-local-distributed-architecture/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。