Django 从 0 到 1 打造完整电商平台:商品缓存优化(Redis)

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.pyTEMPLATES 中启用缓存相关的 context processor。或者直接在视图层做缓存更可控。我们以视图层缓存为主,模板片段缓存作为补充手段。


八、使用 Redis 计数器优化浏览量

在第 15 篇,我们直接在详情页用 F('views') + 1 更新数据库。如果并发很高,这会给数据库带来大量写压力。更优方案是 用 Redis 做计数器,定时批量回写数据库

这里给出一个进阶思路(本系列不作强制实现,感兴趣可自行扩展):

  1. 每次访问详情页,执行 redis.incr(f'views:{sku_id}')

  2. 编写一个 Celery 定时任务,每 5 分钟将 Redis 中的浏览量汇总写入数据库。

  3. 读取浏览量时,先从 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_hitskeyspace_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 测试分类缓存
  1. 首次访问 /products/categories/,控制台有 SQL 查询日志(如果有开启)。

  2. 刷新页面,控制台不再出现 SQL 查询,页面秒开。

  3. 进入 Admin 修改一个分类名称,保存。

  4. 再次访问分类页,显示最新数据(缓存已自动清除)。

终端对比:

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 测试详情页缓存
  1. 访问 iPhone 15 详情页 /products/spu/1/,首次加载正常。

  2. 刷新页面,响应速度明显加快。

  3. 查看浏览量:每次刷新都有递增(缓存不影响浏览量更新)。

  4. 在 Admin 中修改该 SPU 下任意 SKU 的价格,详情页再次访问时数据已更新(信号清除了缓存)。


十一、缓存最佳实践总结


十二、总结与下集预告

今天我们用 Redis 为项目装上了缓存加速引擎:

  • 配置了 Django 的 Redis 缓存后端;

  • 实现了分类树、商品列表、商品详情页的多级缓存;

  • 通过信号自动清除缓存,保证数据一致性;

  • 了解了模板片段缓存和 Redis 计数器的进阶用法。

现在,商品相关页面的访问速度有了质的飞跃。但缓存只是性能优化的一环,代码中的异常处理和日志记录同样关键。第 25 篇 ,我将带大家系统搭建 Django 日志与异常处理 体系,包括分级日志、异常邮件告警、请求追踪等,让项目的运维能力更上一层楼。

想了解更多也可以去其它平台搜索「IT策士」,一起升级 IT 思维 !


本文为《Django 从 0 到 1 打造完整电商平台》系列第 24 篇。

相关推荐
橙子圆1233 小时前
Redis知识9之集群
数据库·redis·缓存
鱼鳞_3 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存
JavaWeb学起来5 小时前
Django学习教程(一)Django介绍和环境准备
django·python web·web框架·django教程
小马爱打代码5 小时前
Spring源码 第三篇:Spring 源码深度拆解:循环依赖 + 三级缓存
java·spring·缓存
程序员老邢6 小时前
《技术底稿 42》查新功能通用化改造:从单一期刊到多源命中,缓存与表结构一次重构
java·后端·缓存·重构·技术底稿
IT策士7 小时前
Django 从 0 到 1 打造完整电商平台:使用 Celery 异步发送邮件/短信
后端·python·django
向日的葵0069 小时前
linux中Redis8.X安装教程(附带RedisInsight安装教程)
linux·运维·服务器·数据库·redis
ULIi096kr10 小时前
Redis 分布式锁进阶第七十二篇
数据库·redis·分布式