在电商平台中,商品列表和详情页的访问频率极高,每次请求都查询数据库会带来巨大的性能瓶颈。本文将带你使用 Django 内置缓存框架与 Redis,为商品页面构建多级缓存,并实现智能失效策略,让项目具备生产级的性能基础。

一、缓存策略:数据与更新方案的选择

在动手编码前,先明确哪些数据适合缓存。电商项目中,商品分类树、商品列表(带筛选条件)、商品详情页(图片、规格等) 都是典型的“读多写少”场景,非常适合缓存。而库存、订单等强一致性数据则不适合。

缓存更新策略通常有三种:Cache Aside(旁路缓存)、Read/Write Through(读写穿透)、Write Behind Caching(异步回写)。本系列采用 Django 缓存框架默认的 Cache Aside 模式:先读缓存,缓存未命中则查数据库并写入缓存,更新数据库时主动清除缓存。这种模式实现简单,且能有效保证数据一致性。

二、配置 Django 的 Redis 缓存后端

首先确保已安装 Redis 和 Python 客户端。若未安装,执行命令:pip install django-redis。然后在 settings.py 中添加缓存配置:

# ==================== 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)。我们规划:

  • :Celery Broker

  • :Celery Result Backend

  • :Django 缓存

各司其职,互不干扰。

配置完成后,Django 的缓存 API 将自动使用 Redis 作为后端。你可以通过 cache.set()cache.get() 进行读写,无需关心底层连接细节。

三、缓存商品分类树:信号驱动的自动失效

商品分类变化极低,是最适合缓存的模块。在分类视图中加入缓存逻辑:

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。CACHE_TTL 设置 1 小时过期,过期后自动重建。

但当管理员在 Admin 中修改分类时,缓存需立即清除。通过 Django 信号实现:

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')

这样,新增、修改或删除分类后,缓存自动失效,下次访问时重建。相比手动清除,信号机制更可靠、更自动化。

四、缓存商品列表与详情页:参数化键与短时缓存

商品列表涉及筛选参数(分类、关键词、排序、页码),缓存键需包含这些参数。在列表视图中:

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)

缓存键示例:

sku_list:q=iPhone:cat=2:sort=price_asc:page=1

不同筛选条件互不干扰,各自缓存自己的页面数据。

商品详情页访问频率最高,但数据相对稳定。为其设置短时缓存:

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)

注意:浏览量递增是写操作,不能被缓存“吞掉”。即使缓存命中,我们仍然执行 ,保证浏览量实时更新。

当 SKU 的库存或价格发生变化时,通过信号清除对应 SPU 的详情缓存:

@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 提供 模板片段缓存,可缓存整个商品分类导航,同时保留用户信息动态显示:

{% 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 %}

但模板缓存需在 TEMPLATES 中启用相关 context processor。更可控的方式是以视图层缓存为主,模板片段缓存作为补充。

对于浏览量更新,若并发高,可用 Redis 计数器 优化:每次访问执行 INCR,再通过 Celery 定时任务批量回写数据库。当前阶段保持 F() 表达式更新即可,此方案适合超高并发场景。

六、监控与测试:验证缓存效果

可通过 Django 的缓存 API 粗略统计命中率。在生产环境,使用 redis-cliINFO stats 命令查看 keyspace_hitskeyspace_misses

redis-cli INFO stats | grep keyspace

控制台输出示例:

keyspace_hits:1234
keyspace_misses:56

命中率 = 1234 / (1234 + 56) ≈ 95.6%,相当理想。

测试步骤:

  1. 启动服务:
  2. python manage.py runserver
  3. 首次访问分类页,控制台有 SQL 查询日志;刷新后无 SQL 查询,页面秒开。
  4. 在 Admin 修改分类名称,保存后再次访问,显示最新数据(缓存已清除)。
  5. 终端对比:
  6. # 首次:
    [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 查询
  7. 访问详情页,首次加载正常;刷新后响应速度明显加快;浏览量每次刷新递增(缓存不影响更新)。
  8. 修改 SKU 价格后,详情页数据即时更新(信号清除了缓存)。

结语:缓存为王,但需善用

通过本文,我们为电商项目装上了 Redis 缓存加速引擎:配置了缓存后端,实现了分类、列表、详情页的多级缓存,并通过信号自动清除缓存保证一致性。缓存是性能优化的关键一环,但需结合业务场景合理设计失效策略,避免缓存雪崩、穿透等问题。

下一篇文章,我将带你搭建 Django 日志与异常处理体系,让项目的运维能力更上一层楼。

db 0db 1db 2update(views=F('views') + 1)