别再等缓存自己"热"起来了!Python后端必会的预热技巧 🚀
一、引子:一次线上故障引发的思考
想象一下这个场景:
你是某电商公司的 Python 后端工程师,双十一大促前夕,为了上线新功能,你在凌晨 2 点重启了服务。重启完成后,你松了一口气,准备下班。
但是,监控大屏突然红了!
- 数据库 CPU 飙升至 90%
- 接口响应时间从平时的 50ms 暴增到 3 秒
- 用户投诉页面打开缓慢
- 更糟糕的是,有几台数据库实例因为连接数过多直接宕机了
你赶紧查日志,发现重启后的前几分钟,Redis 缓存命中率几乎为 0,所有请求都直接打到了 MySQL 数据库上。这就是典型的"冷启动"问题。
如果你提前做了缓存预热,这一切本可以避免。
二、什么是缓存预热?
2.1 定义
缓存预热(Cache Warming) 是指在系统启动、重启或特定时机,主动将热点数据提前加载到缓存系统(如 Redis、Memcached)中的过程。
简单来说:就是在用户真正访问之前,我们先把"可能会被频繁访问的数据"准备好。
2.2 形象类比
把缓存想象成一个小卖部,数据库是仓库:
- 没有预热:早上开门营业,货架是空的。第一批顾客来了,你才慌慌张张跑到仓库搬货,顾客等得不耐烦。
- 有预热:开门前,你已经把畅销商品摆好了。顾客一来,直接拿货付款,体验极佳。
2.3 核心价值
| 问题 | 没有预热 | 使用预热 |
|---|---|---|
| 缓存命中率 | 0% → 逐步上升 | 启动即 80%+ |
| 数据库压力 | 瞬时暴增 | 平稳过渡 |
| 接口响应时间 | 3000ms+ | 50ms |
| 用户体验 | 😤 卡顿 | 😊 流畅 |
三、为什么需要缓存预热?
3.1 避免缓存击穿
缓存击穿:热点数据过期或不存在时,大量请求同时穿透缓存,直接访问数据库。
用户请求 → Redis(未命中)→ MySQL(压力山大)
↓
100万个请求同时查数据库 = 💥 数据库崩溃
3.2 提升用户体验
根据统计,页面加载时间每增加 1 秒,转化率下降 7%。缓存预热能确保:
- 系统重启后立即恢复高性能
- 新功能上线无感知切换
- 大促活动零点开场不卡顿
3.3 保护后端系统
数据库的并发能力有限:
- MySQL 单机:2000-5000 QPS
- Redis:10 万+ QPS
缓存预热相当于给数据库加了一层"防弹衣"。
3.4 典型业务场景
- 电商大促:双十一零点,百万用户涌入
- 热点事件:突发新闻,流量暴增
- 系统重启:版本升级、故障恢复
- 定时任务:每日榜单、推荐列表更新
四、缓存预热的实现策略
4.1 全量预热
适用场景:数据量小(几千到几万条),全部数据都重要
python
# 示例:预热所有商品基础信息
def warmup_all_products():
products = Product.objects.all() # 假设有5万个商品
for product in products:
cache_key = f"product:{product.id}"
redis_client.setex(
cache_key,
3600, # 1小时过期
json.dumps(product.to_dict())
)
print(f"预热完成:{len(products)} 个商品")
优点 :简单直接,覆盖全面
缺点:数据量大时启动慢,占用大量内存
4.2 增量预热(热点优先)
适用场景:数据量大,遵循"二八定律"(20%的数据承担 80%的流量)
python
# 示例:只预热热销商品
def warmup_hot_products():
# 获取最近7天销量TOP 1000的商品
hot_products = Product.objects.filter(
sales_count__gte=100
).order_by('-sales_count')[:1000]
for product in hot_products:
cache_key = f"product:{product.id}"
redis_client.setex(cache_key, 3600, json.dumps(product.to_dict()))
print(f"热点预热完成:{len(hot_products)} 个商品")
优点 :启动快,性价比高
缺点:需要准确识别热点数据
4.3 定时预热
适用场景:数据有明确的时效性,如每日榜单
scss
# 示例:每天凌晨3点预热今日推荐
from apscheduler.schedulers.background import BackgroundScheduler
def warmup_daily_recommendations():
recommendations = get_daily_recommendations() # 计算推荐列表
redis_client.setex('daily_recommendations', 86400, json.dumps(recommendations))
print("每日推荐预热完成")
scheduler = BackgroundScheduler()
scheduler.add_job(warmup_daily_recommendations, 'cron', hour=3, minute=0)
scheduler.start()
4.4 懒加载 + 预热(混合策略)
适用场景:大型系统,平衡启动速度和缓存命中率
ini
def get_product(product_id):
cache_key = f"product:{product_id}"
# 先查缓存
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 缓存未命中,查数据库并回写缓存(懒加载)
product = Product.objects.get(id=product_id)
redis_client.setex(cache_key, 3600, json.dumps(product.to_dict()))
return product.to_dict()
# 启动时只预热TOP 100
def warmup():
hot_ids = [1, 2, 3, ..., 100] # 热点商品ID列表
for pid in hot_ids:
get_product(pid) # 触发懒加载,同时预热缓存
五、Python 实战:三个实用案例
5.1 案例 1:Redis 预热 - 商品信息缓存
场景:电商系统,10 万个商品,需要预热热销商品
python
import redis
import json
from datetime import timedelta
from typing import List, Dict
class CacheWarmer:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
def warmup_products(self, product_ids: List[int]) -> None:
"""预热指定商品列表"""
print(f"开始预热 {len(product_ids)} 个商品...")
# 使用pipeline提升性能
pipe = self.redis.pipeline()
for product_id in product_ids:
product_data = self._fetch_product_from_db(product_id)
if product_data:
cache_key = f"product:{product_id}"
pipe.setex(
cache_key,
timedelta(hours=2), # 2小时过期
json.dumps(product_data, ensure_ascii=False)
)
pipe.execute()
print("预热完成!")
def _fetch_product_from_db(self, product_id: int) -> Dict:
"""从数据库获取商品信息(这里简化处理)"""
# 实际项目中,这里应该是ORM查询
# product = Product.objects.get(id=product_id)
# return product.to_dict()
return {
"id": product_id,
"name": f"商品{product_id}",
"price": 99.9,
"stock": 1000
}
# 使用示例
if __name__ == "__main__":
redis_client = redis.Redis(host='localhost', port=6379, db=0)
warmer = CacheWarmer(redis_client)
# 预热热销商品TOP 500
hot_product_ids = list(range(1, 501))
warmer.warmup_products(hot_product_ids)
关键点:
- 使用
pipeline批量操作,性能提升 10 倍 - 设置合理的过期时间,避免内存溢出
- 预热过程可加入进度条(tqdm 库)
5.2 案例 2:Flask 应用启动预热
场景:Flask API 服务,启动时自动预热缓存
python
from flask import Flask, jsonify
import redis
import threading
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
def cache_warmup():
"""后台线程执行预热任务"""
print("🔥 开始缓存预热...")
# 预热配置数据
configs = {
"app:version": "1.0.0",
"app:maintenance": "false",
"app:max_upload_size": "10MB"
}
for key, value in configs.items():
redis_client.setex(key, 86400, value)
# 预热热点数据
hot_data = fetch_hot_data_from_db()
for item in hot_data:
redis_client.setex(f"hot:{item['id']}", 3600, str(item))
print("✅ 缓存预热完成!")
def fetch_hot_data_from_db():
"""模拟从数据库获取热点数据"""
return [{"id": i, "value": f"data_{i}"} for i in range(1, 101)]
@app.before_request
def before_first_request():
"""在第一次请求前执行预热(只执行一次)"""
if not hasattr(app, 'warmed_up'):
app.warmed_up = True
# 使用后台线程,不阻塞应用启动
threading.Thread(target=cache_warmup, daemon=True).start()
@app.route('/health')
def health_check():
return jsonify({"status": "ok"})
@app.route('/api/product/<int:product_id>')
def get_product(product_id):
# 先查缓存
cache_key = f"product:{product_id}"
cached = redis_client.get(cache_key)
if cached:
return jsonify({"source": "cache", "data": cached})
# 缓存未命中,查数据库
data = {"id": product_id, "name": f"Product {product_id}"}
redis_client.setex(cache_key, 3600, str(data))
return jsonify({"source": "database", "data": data})
if __name__ == '__main__':
# 启动前预热(同步方式,适合小规模预热)
# cache_warmup()
app.run(host='0.0.0.0', port=5000)
技巧:
- 使用
@app.before_request在首次请求时触发 - 后台线程避免阻塞应用启动
- 生产环境建议使用 Gunicorn 的
on_starting钩子
5.3 案例 3:Django AppConfig 实现预热
场景:Django 项目,在应用启动时自动预热缓存
Django 推荐的方式是在 AppConfig.ready() 方法中执行启动任务,这比中间件更加优雅和规范。
python
# apps.py
import os
from django.apps import AppConfig
from django.core.cache import cache
class SubnetManageConfig(AppConfig):
"""子网管理应用配置"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'subnet_manage'
def ready(self):
"""
应用就绪时执行
注意:在多进程环境(如Gunicorn)下,每个worker都会调用一次
"""
# 导入信号处理器
import subnet_manage.signals
# 执行缓存预热
# 注意:如果使用了Django的runserver命令,会启动两个进程(一个主进程用于监听文件变化)
# 可以通过检查环境变量来避免重复执行:if os.environ.get('RUN_MAIN') == 'true'
self.warm_up_cache()
def warm_up_cache(self):
"""预热缓存"""
try:
print("🔥 开始预热缓存...")
# 预热子网树形结构
self.warm_up_subnet_tree()
# 预热其他数据(可扩展)
self.warm_up_system_config()
print("✅ 缓存预热完成!")
except Exception as e:
print(f"❌ 缓存预热失败: {str(e)}")
# 预热失败不应该影响应用启动
# 可以在这里发送告警通知
def warm_up_subnet_tree(self):
"""预热子网树形结构缓存"""
try:
# 延迟导入,确保模型已加载
from subnet_manage.data_cache import generate_subnet_tree
# 生成树形数据
tree_data = generate_subnet_tree()
# 写入缓存(与视图中使用相同的cache_key和过期时间)
cache_key = 'subnet_tree'
cache.set(cache_key, tree_data, timeout=60 * 5) # 5分钟过期
print(f" ✓ 预热子网树形缓存,数据长度: {len(tree_data)}")
except Exception as e:
print(f" ⚠ 子网树预热失败: {str(e)}")
def warm_up_system_config(self):
"""预热系统配置"""
try:
# 预热常用配置
configs = {
"site_name": "子网管理系统",
"max_subnet_depth": 5,
"default_page_size": 20
}
for key, value in configs.items():
cache.set(f"config:{key}", value, timeout=86400) # 24小时
print(f" ✓ 预热系统配置:{len(configs)} 项")
except Exception as e:
print(f" ⚠ 系统配置预热失败: {str(e)}")
# ============== 扩展示例:预热博客文章 ==============
class BlogConfig(AppConfig):
"""博客应用配置"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
def ready(self):
"""应用就绪时执行"""
import blog.signals # 导入信号
self.warm_up_blog_cache()
def warm_up_blog_cache(self):
"""预热博客缓存"""
try:
print("🔥 开始预热博客缓存...")
# 延迟导入模型
from blog.models import Article, Category
# 预热热门文章(访问量TOP 50)
hot_articles = Article.objects.filter(
status='published'
).order_by('-view_count')[:50]
for article in hot_articles:
cache_key = f"article:{article.id}"
cache_data = {
"id": article.id,
"title": article.title,
"summary": article.summary,
"view_count": article.view_count,
"created_at": article.created_at.isoformat()
}
cache.set(cache_key, cache_data, timeout=3600) # 1小时
print(f" ✓ 预热热门文章:{hot_articles.count()} 篇")
# 预热分类列表(所有分类)
categories = list(Category.objects.all().values('id', 'name', 'slug'))
cache.set('categories:all', categories, timeout=86400) # 24小时
print(f" ✓ 预热文章分类:{len(categories)} 个")
print("✅ 博客缓存预热完成!")
except Exception as e:
print(f"❌ 博客缓存预热失败: {str(e)}")
# ============== settings.py 配置 ==============
# 在 INSTALLED_APPS 中指定自定义的 AppConfig
# INSTALLED_APPS = [
# 'django.contrib.admin',
# 'django.contrib.auth',
# # ...
# 'subnet_manage.apps.SubnetManageConfig', # 使用自定义配置
# 'blog.apps.BlogConfig',
# # ...
# ]
关键要点解析:
1️⃣ 为什么在 ready() 中预热?
- ✅ Django 官方推荐的应用初始化入口
- ✅ 确保所有模型、信号已注册完成
- ✅ 代码组织清晰,职责明确
2️⃣ 多进程环境注意事项
python
# 方法1:限制只在主进程执行(适用于 runserver)
def ready(self):
if os.environ.get('RUN_MAIN') == 'true':
self.warm_up_cache()
# 方法2:使用分布式锁(推荐用于生产环境)
def ready(self):
from django.core.cache import cache
lock_key = 'warmup:lock'
# 尝试获取锁,60秒超时
if cache.add(lock_key, '1', timeout=60):
try:
self.warm_up_cache()
finally:
cache.delete(lock_key)
else:
print("其他进程正在预热,跳过")
# 方法3:Gunicorn配置(只在master进程执行)
# gunicorn_config.py
def on_starting(server):
"""服务启动时执行一次(master进程)"""
from django import setup
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
setup()
from django.core.cache import cache
# 执行预热逻辑
warmup()
3️⃣ 延迟导入的重要性
python
# ❌ 错误:在模块级别导入
from blog.models import Article # 可能导致AppRegistryNotReady异常
class BlogConfig(AppConfig):
def ready(self):
# 使用Article模型
pass
# ✅ 正确:在方法内导入
class BlogConfig(AppConfig):
def ready(self):
from blog.models import Article # 确保Django已完全初始化
# 使用Article模型
pass
4️⃣ 实际项目中的完整示例
python
# apps.py
import os
import time
from django.apps import AppConfig
from django.core.cache import cache
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
"""应用启动时执行"""
# 只在生产环境预热,开发环境跳过
if os.environ.get('DJANGO_ENV') == 'production':
self.perform_warmup()
# 导入信号
import myapp.signals
def perform_warmup(self):
"""执行预热流程"""
start_time = time.time()
# 使用分布式锁,避免多进程重复预热
lock_key = 'app:warmup:lock'
if not cache.add(lock_key, '1', timeout=120): # 2分钟锁
print("⏸️ 其他进程正在预热,本进程跳过")
return
try:
print("🔥 开始缓存预热...")
# 统计信息
stats = {
'success': 0,
'failed': 0
}
# 预热各模块
warmup_tasks = [
('产品数据', self.warmup_products),
('用户数据', self.warmup_users),
('配置数据', self.warmup_configs),
]
for task_name, task_func in warmup_tasks:
try:
count = task_func()
stats['success'] += count
print(f" ✓ {task_name}:{count} 条")
except Exception as e:
stats['failed'] += 1
print(f" ✗ {task_name}失败:{str(e)}")
# 记录预热完成
duration = time.time() - start_time
cache.set('app:warmup:last_time', time.time(), timeout=None)
print(f"✅ 缓存预热完成!")
print(f" - 成功:{stats['success']} 项")
print(f" - 失败:{stats['failed']} 项")
print(f" - 耗时:{duration:.2f}秒")
except Exception as e:
print(f"❌ 预热过程异常:{str(e)}")
finally:
cache.delete(lock_key)
def warmup_products(self):
"""预热产品数据"""
from myapp.models import Product
hot_products = Product.objects.filter(
is_active=True
).order_by('-sales_count')[:100]
for product in hot_products:
cache.set(
f'product:{product.id}',
product.to_dict(),
timeout=3600
)
return hot_products.count()
def warmup_users(self):
"""预热VIP用户数据"""
from myapp.models import User
vip_users = User.objects.filter(is_vip=True)[:50]
for user in vip_users:
cache.set(
f'user:{user.id}',
user.get_profile(),
timeout=1800
)
return vip_users.count()
def warmup_configs(self):
"""预热配置数据"""
configs = {
'site_name': '我的网站',
'site_url': 'https://example.com',
'max_upload_size': 10485760, # 10MB
}
for key, value in configs.items():
cache.set(f'config:{key}', value, timeout=86400)
return len(configs)
优势总结:
- ✅ 与 Django 生命周期无缝集成
- ✅ 代码组织清晰,易于维护和扩展
- ✅ 支持多进程环境(通过分布式锁)
- ✅ 异常处理完善,不影响应用启动
- ✅ 实际生产环境验证可用
六、最佳实践与避坑指南
6.1 预热数据的选择标准
如何识别热点数据?
python
# 方法1:基于访问日志统计
def analyze_hot_keys():
"""分析Redis访问日志,找出热点Key"""
# 使用Redis的MONITOR命令(开发环境)
# 或者使用redis-cli --hotkeys(生产环境)
pass
# 方法2:基于业务指标
def get_hot_products_by_sales():
"""根据销量、点击量等业务指标"""
return Product.objects.annotate(
heat_score=F('sales_count') * 0.6 + F('view_count') * 0.4
).order_by('-heat_score')[:1000]
# 方法3:使用LRU算法
# Redis本身支持LRU淘汰策略,可以参考其统计结果
数据优先级:
- ⭐⭐⭐ 核心业务数据:商品详情、用户信息
- ⭐⭐ 配置数据:系统配置、字典表
- ⭐ 统计数据:榜单、推荐列表
6.2 预热时机的把控
| 场景 | 预热时机 | 实现方式 |
|---|---|---|
| 应用启动 | 启动后立即执行 | @app.before_first_request |
| 定时任务 | 每天固定时间 | APScheduler、Celery Beat |
| 流量低谷 | 凌晨 2-4 点 | Cron 定时任务 |
| 手动触发 | 运维操作 | 管理后台接口 |
python
# Django管理命令:python manage.py warmup_cache
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = '手动触发缓存预热'
def handle(self, *args, **options):
self.stdout.write('开始预热...')
# 执行预热逻辑
warmup_all()
self.stdout.write(self.style.SUCCESS('预热完成!'))
6.3 监控与告警
关键指标:
python
import time
from functools import wraps
def monitor_warmup(func):
"""装饰器:监控预热过程"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
duration = time.time() - start_time
# 记录成功日志
print(f"✅ {func.__name__} 预热成功,耗时: {duration:.2f}s")
# 发送监控指标到Prometheus/Grafana
# metrics.warmup_duration.labels(func.__name__).observe(duration)
return result
except Exception as e:
duration = time.time() - start_time
# 记录失败日志
print(f"❌ {func.__name__} 预热失败: {str(e)},耗时: {duration:.2f}s")
# 发送告警(邮件/钉钉/企业微信)
# alert.send(f"缓存预热失败: {func.__name__}")
raise
return wrapper
@monitor_warmup
def warmup_products():
# 预热逻辑
pass
建议监控的指标:
- ⏱️ 预热耗时
- 📊 预热数据量
- ✅ 成功/失败率
- 💾 Redis 内存占用
- 🎯 缓存命中率(预热前后对比)
6.4 常见问题及解决方案
问题 1:预热时间过长,影响启动
❌ 错误做法:
scss
# 启动时同步预热100万条数据,需要10分钟
warmup_all_data()
app.run() # 10分钟后才能提供服务
✅ 正确做法:
ini
# 方案1:异步预热
threading.Thread(target=warmup_all_data, daemon=True).start()
app.run() # 立即启动,后台预热
# 方案2:分批预热
def warmup_in_batches():
# 先预热TOP 100(1秒),再预热剩余(后台进行)
warmup_hot_data(limit=100) # 快速预热核心数据
threading.Thread(target=lambda: warmup_hot_data(limit=10000)).start()
问题 2:预热数据过期后,又出现"冷启动"
❌ 错误做法:
bash
redis_client.setex('product:1', 60, data) # 只缓存60秒
✅ 正确做法:
python
# 方案1:设置足够长的过期时间
redis_client.setex('product:1', 86400, data) # 24小时
# 方案2:定时刷新
@scheduler.scheduled_job('interval', hours=1)
def refresh_cache():
warmup_hot_data()
# 方案3:访问时自动续期
def get_product(product_id):
key = f'product:{product_id}'
data = redis_client.get(key)
if data:
redis_client.expire(key, 3600) # 续期1小时
return data
# ...
问题 3:预热占用过多内存
❌ 错误做法:
bash
# 预热所有数据(100GB),Redis内存不足
warmup_all_products() # 1000万个商品
✅ 正确做法:
python
# 只预热热点数据,控制内存占用
MAX_WARMUP_SIZE = 10000 # 最多预热1万条
def warmup_with_limit():
hot_products = get_hot_products(limit=MAX_WARMUP_SIZE)
for product in hot_products:
# 只缓存关键字段,减少内存占用
cache_data = {
"id": product.id,
"name": product.name,
"price": product.price
# 不缓存详情description、图片等大字段
}
redis_client.setex(f'product:{product.id}', 3600, json.dumps(cache_data))
问题 4:多进程/多实例重复预热
❌ 错误做法:
ini
# Gunicorn 4个worker,每个都执行预热 = 浪费资源
if __name__ == '__main__':
warmup() # 执行4次
app.run()
✅ 正确做法:
python
# 方案1:使用分布式锁
import redis
from contextlib import contextmanager
@contextmanager
def warmup_lock(redis_client, timeout=60):
lock_key = 'warmup:lock'
# 尝试获取锁
if redis_client.set(lock_key, '1', nx=True, ex=timeout):
try:
yield True
finally:
redis_client.delete(lock_key)
else:
yield False
# 使用
with warmup_lock(redis_client) as acquired:
if acquired:
print("获取到锁,开始预热")
warmup()
else:
print("其他进程正在预热,跳过")
# 方案2:只在主进程预热(Gunicorn)
def on_starting(server):
"""Gunicorn配置文件中"""
warmup() # 只在master进程执行一次
七、总结与延伸
7.1 核心要点回顾
✅ 什么时候需要缓存预热?
- 系统重启/上线
- 预期流量高峰(大促、热点事件)
- 数据更新后(榜单、推荐列表)
✅ 预热什么数据?
- 遵循"二八定律",优先预热 20%的热点数据
- 核心业务数据 > 配置数据 > 统计数据
✅ 如何实现预热?
- 全量预热:适合小数据量
- 增量预热:热点优先,性价比高
- 定时预热:周期性更新
- 懒加载+预热:混合策略
✅ 注意事项
- 异步预热,不阻塞启动
- 设置合理过期时间
- 监控预热效果
- 多实例环境使用分布式锁
7.2 进阶学习建议
📚 延伸阅读
-
缓存策略:
- Cache-Aside、Read-Through、Write-Through
- LRU、LFU 淘汰算法
-
Redis 高级特性:
- Redis Pipeline(批量操作)
- Redis Cluster(分布式)
- Redis 持久化(RDB/AOF)
-
分布式缓存:
- 一致性 Hash 算法
- 缓存雪崩、击穿、穿透解决方案
🛠️ 实战项目建议
- 搭建一个带缓存预热的 Flask/Django 博客系统
- 实现一个商品秒杀系统(Redis+预热+限流)
- 使用 Prometheus+Grafana 监控缓存命中率
📖 推荐资源
- 书籍:《Redis 设计与实现》黄健宏
- 视频:B 站搜索"Redis 缓存实战"
- 工具:RedisInsight(Redis 可视化工具)
7.3 最后的话
缓存预热是性能优化的重要手段,但它不是银弹。
记住这个原则:预热是锦上添花,而不是雪中送炭。如果你的系统架构本身就有问题(慢 SQL、N+1 查询、无索引),预热只能治标不治本。
先优化代码和 SQL,再考虑缓存,最后才是预热。
作为一名 Python 后端工程师,掌握缓存预热这项技能,能让你在面对高并发场景时更加从容。希望这篇文章能帮助你:
- 🎓 理解缓存预热的原理和价值
- 🛠️ 掌握 Python 实现缓存预热的多种方式
- 🚀 在实际项目中应用,提升系统性能
如果你是刚入门的大一新生,不用急于理解所有细节。先把 Redis 玩起来,从简单的set/get开始,慢慢你就会发现缓存的魅力。
Keep learning, keep coding! 💪
附录:完整代码示例
A. 通用缓存预热工具类
python
"""
cache_warmer.py - 通用缓存预热工具
"""
import redis
import json
import time
from typing import List, Dict, Callable, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
class CacheWarmer:
def __init__(
self,
redis_client: redis.Redis,
max_workers: int = 10,
batch_size: int = 100
):
self.redis = redis_client
self.max_workers = max_workers
self.batch_size = batch_size
def warmup(
self,
data_fetcher: Callable,
key_generator: Callable[[Dict], str],
ttl: int = 3600,
max_items: Optional[int] = None
) -> Dict[str, any]:
"""
通用预热方法
Args:
data_fetcher: 数据获取函数,返回List[Dict]
key_generator: Key生成函数,接收单条数据,返回cache_key
ttl: 过期时间(秒)
max_items: 最大预热数量
Returns:
统计信息
"""
start_time = time.time()
# 1. 获取数据
print("📥 正在获取数据...")
data_list = data_fetcher()
if max_items:
data_list = data_list[:max_items]
total = len(data_list)
print(f"📊 共需预热 {total} 条数据")
# 2. 分批预热
success_count = 0
failed_count = 0
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = []
# 按batch_size分批
for i in range(0, total, self.batch_size):
batch = data_list[i:i + self.batch_size]
future = executor.submit(self._warmup_batch, batch, key_generator, ttl)
futures.append(future)
# 收集结果
for future in as_completed(futures):
try:
batch_success, batch_failed = future.result()
success_count += batch_success
failed_count += batch_failed
# 进度条
progress = (success_count + failed_count) / total * 100
print(f"⏳ 进度: {progress:.1f}% ({success_count}/{total})")
except Exception as e:
print(f"❌ 批次预热失败: {str(e)}")
failed_count += self.batch_size
# 3. 统计结果
duration = time.time() - start_time
result = {
"total": total,
"success": success_count,
"failed": failed_count,
"duration": f"{duration:.2f}s",
"qps": int(success_count / duration) if duration > 0 else 0
}
print(f"\n✅ 预热完成!")
print(f" - 成功: {success_count}")
print(f" - 失败: {failed_count}")
print(f" - 耗时: {duration:.2f}s")
print(f" - QPS: {result['qps']}")
return result
def _warmup_batch(
self,
batch: List[Dict],
key_generator: Callable,
ttl: int
) -> tuple:
"""预热单个批次"""
success = 0
failed = 0
pipe = self.redis.pipeline()
for item in batch:
try:
cache_key = key_generator(item)
cache_value = json.dumps(item, ensure_ascii=False)
pipe.setex(cache_key, ttl, cache_value)
success += 1
except Exception as e:
print(f"⚠️ 处理失败: {str(e)}")
failed += 1
try:
pipe.execute()
except Exception as e:
print(f"❌ Pipeline执行失败: {str(e)}")
# 失败时重试单条插入
for item in batch:
try:
cache_key = key_generator(item)
cache_value = json.dumps(item, ensure_ascii=False)
self.redis.setex(cache_key, ttl, cache_value)
except:
failed += 1
return success, failed
# 使用示例
if __name__ == "__main__":
# 初始化
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
warmer = CacheWarmer(redis_client, max_workers=5, batch_size=50)
# 定义数据获取函数
def fetch_products():
# 模拟从数据库获取
# 实际项目中:return Product.objects.filter(status='active').values()
return [
{"id": i, "name": f"商品{i}", "price": 99.9}
for i in range(1, 1001)
]
# 定义Key生成函数
def generate_key(product):
return f"product:{product['id']}"
# 执行预热
stats = warmer.warmup(
data_fetcher=fetch_products,
key_generator=generate_key,
ttl=7200, # 2小时
max_items=500 # 只预热前500个
)
print(f"\n📊 最终统计: {stats}")
关于作者
我是一名 Python 后端工程师,专注于高性能 Web 应用开发。如果这篇文章对你有帮助,欢迎:
- ⭐ 点赞收藏
- 💬 留言交流
- 🔗 分享给朋友
有任何问题,欢迎在评论区讨论!
★
下期预告:《Redis 分布式锁:从原理到实战》,敬请期待!