IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我也会在其它其它平台发布最新文章,助你少走弯路。
上一篇我们用 Redis 做消息代理,配合 Celery 实现了异步任务队列。今天,Redis 的另一个核心用途------缓存,终于要登场了。
你是否有这样的感受:电商首页、商品列表页、商品详情页,每次访问都要查数据库,高峰期数据库压力山大,页面加载越来越慢。尤其是商品详情页,图片多、规格多、还有浏览量实时更新,SQL 查询次数居高不下。这时候就需要 缓存 来拯救性能------把频繁访问又不常变的数据放进 Redis,下次访问直接走内存,快到飞起。
今天我就带大家用 Django 内置的缓存框架 + Redis,给商品列表、详情页加上多级缓存,同时处理好缓存的更新和失效策略,让项目具备生产级的性能基础。
一、缓存策略分析
1.1 哪些数据该缓存?
电商项目中,典型的可缓存数据:
原则:读多写少才缓存,强一致性数据不缓存。
1.2 缓存更新策略
常见的三种策略:
本系列采用 Cache Aside 模式,这也是 Django 缓存框架的默认行为。
二、配置 Django 缓存
2.1 安装依赖
上一篇我们已经装了 Redis 和 Celery,redis Python 客户端已有。如果没有,执行:
2.2 配置 settings.py
在 django_ecommerce/settings.py 中添加缓存配置:
bash
# ==================== Django 缓存配置(Redis) ====================
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2', # 使用 2 号数据库,与 Celery 隔离
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'ecommerce', # 键前缀,防止与其他项目冲突
'TIMEOUT': 300, # 默认过期时间 300 秒 = 5 分钟
}
}
注意:Redis 有 16 个逻辑数据库(编号 0-15)。我们规划:
db 0:Celery Broker
db 1:Celery Result Backend
db 2:Django 缓存各司其职,互不干扰。
三、缓存商品分类树
商品分类变化极低,是最适合缓存的。我们在第 11 篇的分类视图中加入缓存逻辑。
编辑 apps/products/views.py 中的 category_tree 视图:
bash
from django.core.cache import cache
from django.views.decorators.cache import cache_page
def category_tree(request):
# 先从缓存取
cache_key = 'category_tree'
categories = cache.get(cache_key)
if categories is None:
# 缓存未命中,查数据库
categories = list(
Category.objects.filter(
parent__isnull=True, is_active=True
).prefetch_related('children__children').order_by('sort')
)
# 写入缓存,设置超时时间为 1 小时
cache.set(cache_key, categories, timeout=3600)
return render(request, 'products/category_tree.html', {
'categories': categories
})
说明:
-
cache.get()从 Redis 读取,如果 key 不存在返回None。 -
命中缓存时,完全不走数据库,响应时间从 20ms 降到 2ms。
-
timeout=3600意味着 1 小时后缓存过期,下次访问会重新查库。
更新策略:当管理员在 Admin 中修改分类时,需要清除分类缓存。我们可以通过 Django 信号来实现。
四、使用信号自动清除分类缓存
在 apps/products/models.py 末尾添加信号处理:
bash
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
@receiver(post_save, sender=Category)
@receiver(post_delete, sender=Category)
def clear_category_cache(sender, **kwargs):
"""当分类数据变更时,自动清除缓存"""
cache.delete('category_tree')
这样,只要在 Admin 中新增、修改或删除分类,缓存立即清除,下次访问分类页时自动重建。
五、缓存商品列表(带参数)
商品列表比分类复杂,因为涉及筛选参数(分类、搜索关键词、排序、页码)。缓存键需要包含这些参数。
编辑 apps/products/views.py 中的 sku_list 视图:
bash
def sku_list(request):
query = request.GET.get('q', '').strip()
category_id = request.GET.get('category_id')
sort_option = request.GET.get('sort', '-create_time')
page_number = request.GET.get('page', 1)
# 构建缓存键(包含所有查询参数)
cache_key = f'sku_list:q={query}:cat={category_id}:sort={sort_option}:page={page_number}'
# 尝试从缓存获取
cached_data = cache.get(cache_key)
if cached_data is not None:
# 缓存命中,直接返回
return render(request, 'products/sku_list.html', cached_data)
# 缓存未命中,执行原查询逻辑
skus = SKU.objects.filter(is_active=True).select_related('spu__category').prefetch_related('images')
# ... 搜索过滤、分类过滤、排序逻辑不变 ...
if query:
skus = skus.filter(
Q(name__icontains=query) |
Q(spu__name__icontains=query) |
Q(spu__brand__icontains=query) |
Q(spu__desc__icontains=query)
)
# ... 其余过滤排序分页代码同上 ...
context = {
'page_obj': page_obj,
'current_category': current_category,
'top_categories': top_categories,
'query': query,
'sort_option': sort_option,
}
# 写入缓存(2 分钟过期,适合列表页)
cache.set(cache_key, context, timeout=120)
return render(request, 'products/sku_list.html', context)
缓存键示例:
bash
sku_list:q=iPhone:cat=2:sort=price_asc:page=1
这样不同筛选条件互不干扰,各自缓存各自的页面数据。
六、缓存商品详情页
商品详情页访问频率最高,但数据也相对稳定(价格调整、库存变化不频繁)。我们给详情页加上短时缓存。
编辑 apps/products/views.py 中的 spu_detail 视图:
bash
def spu_detail(request, spu_id):
cache_key = f'spu_detail:{spu_id}'
# 尝试从缓存获取
cached_data = cache.get(cache_key)
if cached_data is not None:
# 浏览量在缓存命中时仍要递增(不能用缓存数据)
# 直接查库做原子更新,但不影响页面渲染
active_skus = SKU.objects.filter(spu_id=spu_id, is_active=True)
active_skus.update(views=F('views') + 1)
return render(request, 'products/spu_detail.html', cached_data)
spu = get_object_or_404(SPU.objects.prefetch_related('skus__images'), pk=spu_id)
# 浏览量 +1
active_skus = spu.skus.filter(is_active=True)
active_skus.update(views=F('views') + 1)
# 重新获取更新后的 SKU 列表
skus = spu.skus.filter(is_active=True).prefetch_related('images')
specs_data = {}
for sku in skus:
for key, value in sku.specs.items():
if key not in specs_data:
specs_data[key] = set()
specs_data[key].add(value)
specs_list = {k: list(v) for k, v in specs_data.items()}
default_sku = skus.first()
context = {
'spu': spu,
'skus': skus,
'specs': specs_list,
'default_sku': default_sku,
}
# 缓存 5 分钟(300 秒)
cache.set(cache_key, context, timeout=300)
return render(request, 'products/spu_detail.html', context)
注意 :浏览量递增是写操作,不能被缓存"吞掉"。即使缓存命中,我们仍然执行
update(views=F('views') + 1),保证浏览量实时更新。
更新策略:当 SKU 的库存、价格发生变化时,通过信号清除对应 SPU 的详情缓存。
在 apps/products/models.py 中添加:
bash
@receiver(post_save, sender=SKU)
def clear_spu_cache_on_sku_change(sender, instance, **kwargs):
"""SKU 变更时清除对应 SPU 的详情缓存"""
cache.delete(f'spu_detail:{instance.spu_id}')
七、缓存模板片段(选择性缓存)
有些页面,大部分内容可以缓存,但个别区域需要动态更新(如导航栏的用户名、购物车数量)。Django 提供了 模板片段缓存。
在 templates/base.html 中,我们可以缓存整个商品分类导航(因为它变化极低),但保留用户信息动态显示:
bash
{% load cache %}
...
<!-- 缓存分类导航,1小时过期 -->
{% cache 3600 category_nav %}
{% for cat in top_categories %}
<a class="nav-link" href="{% url 'products:sku_list' %}?category_id={{ cat.id }}">{{ cat.name }}</a>
{% endfor %}
{% endcache %}
但注意,模板缓存需要在 settings.py 的 TEMPLATES 中启用缓存相关的 context processor。或者直接在视图层做缓存更可控。我们以视图层缓存为主,模板片段缓存作为补充手段。
八、使用 Redis 计数器优化浏览量
在第 15 篇,我们直接在详情页用 F('views') + 1 更新数据库。如果并发很高,这会给数据库带来大量写压力。更优方案是 用 Redis 做计数器,定时批量回写数据库。
这里给出一个进阶思路(本系列不作强制实现,感兴趣可自行扩展):
-
每次访问详情页,执行
redis.incr(f'views:{sku_id}')。 -
编写一个 Celery 定时任务,每 5 分钟将 Redis 中的浏览量汇总写入数据库。
-
读取浏览量时,先从 Redis 取实时值,加上数据库中的基值。
这个方案适合超高并发场景,我们当前阶段保持 F 表达式更新即可。
九、监控缓存命中率
我们可以通过 Django 的缓存 API 来粗略统计命中率。在开发阶段,可以在视图中添加临时日志:
bash
import logging
logger = logging.getLogger('cache')
# 在 sku_list 中:
if cached_data is not None:
logger.info(f'缓存命中: {cache_key}')
else:
logger.info(f'缓存未命中: {cache_key}')
生产环境可以使用 django-redis 提供的统计命令,或 Redis 的 INFO stats 查看 keyspace_hits 和 keyspace_misses:
bash
redis-cli INFO stats | grep keyspace
控制台输出:
bash
keyspace_hits:1234
keyspace_misses:56
命中率 = 1234 / (1234 + 56) ≈ 95.6%,相当理想。
十、测试缓存效果
10.1 启动服务
确保 Redis 运行中,然后启动 Django:
bash
python manage.py runserver
10.2 测试分类缓存
-
首次访问
/products/categories/,控制台有 SQL 查询日志(如果有开启)。 -
刷新页面,控制台不再出现 SQL 查询,页面秒开。
-
进入 Admin 修改一个分类名称,保存。
-
再次访问分类页,显示最新数据(缓存已自动清除)。
终端对比:
bash
# 首次:
[28/May/2026 09:15:00] "GET /products/categories/ HTTP/1.1" 200 3456
# SQL: SELECT ... FROM tb_category ...
# 缓存命中:
[28/May/2026 09:15:05] "GET /products/categories/ HTTP/1.1" 200 3456
# 无 SQL 查询
10.3 测试详情页缓存
-
访问 iPhone 15 详情页
/products/spu/1/,首次加载正常。 -
刷新页面,响应速度明显加快。
-
查看浏览量:每次刷新都有递增(缓存不影响浏览量更新)。
-
在 Admin 中修改该 SPU 下任意 SKU 的价格,详情页再次访问时数据已更新(信号清除了缓存)。
十一、缓存最佳实践总结
十二、总结与下集预告
今天我们用 Redis 为项目装上了缓存加速引擎:
-
配置了 Django 的 Redis 缓存后端;
-
实现了分类树、商品列表、商品详情页的多级缓存;
-
通过信号自动清除缓存,保证数据一致性;
-
了解了模板片段缓存和 Redis 计数器的进阶用法。
现在,商品相关页面的访问速度有了质的飞跃。但缓存只是性能优化的一环,代码中的异常处理和日志记录同样关键。第 25 篇 ,我将带大家系统搭建 Django 日志与异常处理 体系,包括分级日志、异常邮件告警、请求追踪等,让项目的运维能力更上一层楼。
想了解更多也可以去其它平台搜索「IT策士」,一起升级 IT 思维 !
本文为《Django 从 0 到 1 打造完整电商平台》系列第 24 篇。