Django电商性能优化:FastDFS、页面静态化与Redis缓存实战
开源电商项目里这类标题一看就是练手级但藏着不少坑的经典组合。我做生鲜电商类Django项目时遇到过同样的三个问题商品图片越传越多本地磁盘和后续分布式部署根本扛不住首页是流量集中地每次请求都现查数据库再渲染模板响应慢还白消耗性能详情页和列表页频繁访问热点数据数据库压力全在重复查询上。这篇就围绕这三件事展开FastDFS怎么接入Django、首页页面静态化怎么做、Redis缓存怎么在首页和详情页真正落地。适合正在做电商类Django项目的开发者参考尤其想搞懂文件分布式存储和缓存、静态化三者如何配合的读者。1. 先搞清楚这套方案要解决什么问题1.1 电商项目里图片文件为什么不能一直存本地先说一个最直接的感受。天天生鲜这类项目商品图片、详情轮播图、规格图加起来少说几百上千张。最开始我图省事直接把图片存到项目目录下的media文件夹用Django内置的FileSystemStorage处理。开发阶段没问题但一到联调就暴露了图片一多项目服务器磁盘吃紧备份困难更麻烦的是万一后面要部署多台服务器用户上传到A机器的图片B机器上根本访问不到。文件存储这块单体项目的本地文件方案在分布式场景下天然不成立。FastDFS就是冲着这个问题去的。它本质是一个开源的轻量级分布式文件系统专门解决海量小文件的存储和访问图片、头像、附件这类场景特别合适。它由Tracker Server和Storage Server两部分组成Tracker负责调度、记录存储节点信息Storage真正存文件还支持同步复制。接入之后文件不再和Django应用实例绑定上传、删除、访问都走一套独立的存储层后续应用水平扩展也不影响已有图片的访问。这就是为什么电商项目里普遍用FastDFS或同类对象存储而不是继续把文件放在应用本地。1.2 首页为什么不能每次请求都现查现渲染生鲜电商的首页数据本身是固定集合导航分类、轮播图、秒杀活动、促销板块、新品推荐。这些东西由运营人员维护更新频率很低可能一天才改一两次。但用户访问频率极其高开门营业到打烊首页请求量能占到整个站点很大比例。如果每个请求都去数据库查十几张表再一层层渲染模板数据库和CPU都在做大量重复劳动完全没有必要。页面静态化解决的就是这个问题。思路不复杂把首页的模板和数据组装成完整的HTML字符串写入一个静态文件然后让Nginx这类服务器直接返回静态文件。用户请求到达时连Django进程都不用进Nginx直接丢一个文件回去响应速度从几百毫秒降到几十毫秒甚至更快。相当于把最热门的页面从动态计算改成静态读取这是电商高并发场景里一个很朴素的优化思路。1.3 缓存在这套方案里扮演什么角色静态化处理的是页面级的性能问题缓存处理的是数据级的重复查询问题。首页静态化之后首页本身不查数据库了但详情页、列表页、以及静态页面重新生成时还是需要大量读取数据库。比如用户每次打开商品详情页都要查SKU信息、查图片、查规格、查推荐商品。这些数据相对稳定短时间内不会变完全可以放到Redis里下次直接取省掉数据库查询。这里要理解一个层级关系静态化在Nginx层解决页面响应缓存Redis在应用层解决热点数据的重复计算数据库只负责最终的数据持久化。三者不是互相替代是配合关系。静态化负责最高频的首页缓存负责次高频的详情页数据组装和列表页新品推荐数据库承担低频写入和最终一致性。这套组合基本覆盖了生鲜电商项目里最典型的性能瓶颈场景。2. FastDFS接入Django的完整流程2.1 FastDFS的Tracker和Storage两个角色必须分清第一次接触FastDFS的人最容易在架构上绕晕。Tracker Server不是一个存储节点它是一个调度中心负责管理所有Storage Server的心跳、状态、文件同步信息客户端上传文件时先找Tracker拿一个可用的Storage地址再去那个Storage实际传输数据。下载或访问文件时Tracker会告诉客户端/浏览器文件在哪个Storage上但实际读取流量直接走Storage不经过Tracker。简单类比Tracker像外卖平台的调度系统告诉你哪个商家接单、哪个骑手配送但实际送餐的是骑手不是调度系统本身。实际操作中我建议在本地起一套完整的FastDFS环境一个Tracker、一个Storage、再给Storage配一个Nginx提供HTTP访问。配置里最重要的几个参数tracker的端口默认是22122storage的端口默认是23000client.conf文件里要指定tracker_server的IP和端口storage.conf里要配置storage的存储路径和group名称。这里最容易被忽略的是Django服务器上也要放一份client.confFastDFS的Python客户端要读取它才能连上Tracker。2.2 Django自定义文件存储类这个关键动作Django的FileField和ImageField支持通过DEFAULT_FILE_STORAGE配置指定一个自定义存储类。这意味着我们可以不用改动业务代码就能把文件上传路径从本地磁盘切换到FastDFS。这是Django一个很巧妙的设计框架层面预留了存储后端接口我们只需要实现这个接口的核心方法。具体来说要继承django.core.files.storage.Storage至少重写_open、_save、url三个方法还有exists和delete看业务情况决定。_open方法在Django需要读取文件内容时被调用对于FastDFS来说文件不在本地所以通常不真正打开文件可以直接返回一个符合文件对象接口的包装对象甚至抛出NotImplementedError只要业务里没有读取文件内容的需求就行。_save才是核心它接收上传的文件名和文件对象调用FastDFS客户端把内容传输上去返回FastDFS分配的文件ID一个组名加文件名的字符串。url方法负责生成访问链接把存储返回的file_id拼接上FastDFS的HTTP访问地址最终给模板里的图片src用。from django.core.files.storage import Storage from django.conf import settings from fdfs_client.client import Fdfs_client class FastDFSStorage(Storage): def __init__(self, base_urlNone, client_confNone): self.base_url base_url or settings.FDFS_BASE_URL self.client_conf client_conf or settings.FDFS_CLIENT_CONF def _open(self, name, moderb): pass def _save(self, name, content): client Fdfs_client(self.client_conf) ret client.upload_by_buffer(content.read()) if ret.get(Status) Upload successed.: return ret.get(Remote file_id) raise Exception(上传文件到FastDFS失败) def exists(self, name): return False def url(self, name): return self.base_url name我在实际项目中就是按这个思路实现存储类的。有个细节容易踩坑Fdfs_client每次上传都重新实例化连接开销不小最好在类里做成复用对象另外upload_by_buffer方法接收的是字节内容如果文件大要考虑一次性读入内存的开销商品图片一般几百KB以内问题不大。2.3 上传与访问URL的打通细节存储类写完之后配置项也要同步跟上。settings里需要加这样几项DEFAULT_FILE_STORAGE utils.fastdfs.storage.FastDFSStorage FDFS_CLIENT_CONF ./utils/fastdfs/client.conf FDFS_BASE_URL http://192.168.1.100:8888/FDFS_BASE_URL要和Storage的Nginx访问地址保持一致。比如我本机FastDFS的Storage配套Nginx监听8888端口访问路径是http://192.168.1.100:8888/group1/M00/00/00/xxxx.jpg那base_url就写成http://192.168.1.100:8888/存储类返回的file_id是group1/M00/00/00/xxxx.jpg两者拼接就是完整可访问URL。这一环如果配错最常见的现象就是图片上传成功、数据库里file_id也有但页面图片裂开定位半天发现是base_url端口或IP写错了。还有一点Storage上的Nginx配置里需要指定访问的存储根目录也就是storage.conf里store_path对应的路径。我这边配置是location /group1/M00 { alias /data/fastdfs/storage/data; }之类的映射。这一步不做即使Nginx端口通了也找不到实际文件。整套FastDFS接入完成之后可以用Django shell做一个快速验证创建一张带ImageField的模型保存一条记录看返回的图片URL能否在浏览器里打开。打不开就回头检查client.conf、storage路径、Nginx alias这三处。3. 页面静态化设计从模板到静态文件的完整链路3.1 静态化为什么特别适合生鲜电商首页我在决定做首页静态化之前先做了个简单的压力测试。同一个首页接口不做任何缓存时一次请求大概要执行几十条SQL平均耗时在200到400毫秒加上页面里轮播图、促销活动渲染模板语法解析耗时还会上升。用工具模拟100个并发请求数据库连接直接被打满部分请求超时。但静态文件方案下Nginx返回一个HTML文件基本不受应用层瓶颈影响同样的并发压测下响应时间稳定在10毫秒左右。这就是质的差别。生鲜电商的首页还有一个特点数据变更完全可以预测。早上运营把轮播图和秒杀活动调整一下午间促销做一个替换这些操作一天就几次。用户对首页数据实时性要求低但访问量极大。这种低频更新、高频访问的页面就是静态化最理想的场景。反观详情页商品信息更新可能由库存变化触发SKU价格可能调整虽然也不算特别频繁但变动频率和不可预测性比首页高所以详情页更适合用缓存而不是静态化。这个判断本身就是方案选型的核心逻辑。3.2 静态页面生成脚本的编写与触发机制静态化的实现并不神秘就是写一个脚本在Django的上下文中渲染首页模板把结果写入指定文件。要让脚本能够使用Django的ORM和模板引擎必须先设置Django环境变量并调用django.setup()。具体做法是import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, daily_fresh.settings) django.setup()环境准备好之后像视图函数里一样组装首页需要的数据——分类、轮播图、促销活动、新品推荐——然后调用模板渲染成字符串写入文件。我习惯把生成逻辑放到一个独立的工具模块里同时提供URL给它手动触发。触发机制可以分几种由管理后台保存商品或轮播图数据时自动调用由定时任务比如crontab每天定时生成由运营后台页面手动点击生成。这里有个经验不要只在数据变更时生成也要在服务器重启或发布新代码后重新生成一次避免模板更新而旧静态文件还在线上。3.3 首页数据组装与模板静态文件落盘我实现静态化时首先生成的是最新的数据之后再由访问过的用户去触发缓存的重建。核心是一种约定静态文件内容如果过期由下次访问触发一次更新同时把首页数据缓存起来备用。但要注意静态文件生成时涉及的促销活动、分类、轮播图这类数据在组装过程中不要直接写死要做到渲染后的HTML包含的是最新数据。简化流程大概是读取数据库组装上下文用模板引擎渲染把渲染结果写入静态目录下次有人通过URL访问静态页时服务器直接读文件文件不存在或已过期时重新生成。关键代码可以这样from django.template import loader from django.conf import settings def generate_static_index_html(): context { categories: get_categories(), banners: get_index_banners(), promotions: get_index_promotions(), new_skus: get_new_skus(), } template loader.get_template(index.html) html_bytes template.render(context).encode(utf-8) static_path os.path.join(settings.STATICFILES_DIRS[0], index.html) with open(static_path, w, encodingutf-8) as f: f.write(html_str) return static_path处理时有一点值得注意如果前端页面引用了静态资源CSS、JS、图片这些资源路径如果是相对路径移动到什么位置很关键如果是Django的{% static %}标签在渲染时要先执行collectstatic否则静态页里的图片、样式资源会指向不存在的目录。我用Nginx服务生成好的index.html时会把静态目录和Django静态资源目录统一映射到同一个server上下文里这样才能保证用户打开静态首页时样式不丢。我的测试经验是在生成函数的入口处先打印一下模板耗时如果超过300毫秒再去检查是否是模板中某个过滤器和查询语句卡住了。3.4 静态化页面的自动化刷新窗口静态化最怕的事情是页面更新不及时。比如运营上午改了活动但下午用户打开首页看到还是上午之前的页面。要控制这个问题得给静态化留一个刷新窗口并且把数据变更和重新生成绑定起来。我的做法是在管理后台的操作保存逻辑里统一调用一个re_generate_index_html()通知函数由它去触发静态文件重建。同时我会在缓存中做一个标记保存当前首页数据版本号用户访问静态页时如果发现版本号和最新缓存里的版本号不一致就异步发起重建任务。当然并不是所有操作都要立刻重建。比如商品库存变化是高频操作每次都重建首页反而拖垮性能。我的建议是只对影响首页展示的可见性数据变化触发重建包括轮播图、促销活动、分类调整。库存变化这类不影响首页结构的就不必触发重建否则静态化就从优化工具变成了性能负担。设计这个刷新窗口时把低频但重要的变化和高频但次要的变化分开对待静态化的收益才会最大化。4. 缓存设置与获取Redis在首页和详情页的正确姿势4.1 Django缓存框架与Redis连接的配置细节Django自带的django.core.cache提供了一套缓存接口底层实现可以自由替换。项目里我选择django-redis作为连接Redis的桥梁。配置起来比较直接CACHES { default: { BACKEND: django_redis.cache.RedisCache, LOCATION: redis://127.0.0.1:6379/2, OPTIONS: { CLIENT_CLASS: django_redis.client.DefaultClient, CONNECTION_POOL_KWARGS: {max_connections: 100}, } } }有几个细节在实际运行中会决定稳定性。第一个数据库编号不要用0我习惯用2或3避免和其他项目共用Redis时互相污染也能在一个Redis实例上隔离不同业务的key后续排查和清理更方便。第二个CONNECTION_POOL_KWARGS里的max_connections要和Redis服务端maxclients匹配否则高并发时会出现连接超限。第三个如果项目有异步任务或脚本需要访问缓存最好封装一个独立的工具函数统一传入缓存key前缀防止各处硬编码把key搞乱。4.2 首页缓存键设计与读写时机首页的Redis缓存和静态化并不冲突甚至可以说互为备份。具体的做法是静态化生成HTML时把渲染所需的context也写入Redis以index_page_data为键保存一份完整数据设置过期时间比如3600秒。这带来两个好处第一缓存没过期时如果Nginx上的静态文件被误删Django还可以从缓存中快速恢复先把context重新渲染成HTML再写回静态文件第二某些改动只涉及局部数据比如某个促销活动换图不需要重新查全库直接从缓存里取旧context改一部分再重新渲染成本低得多。读取缓存时的标准姿势是先查缓存命中直接返回未命中则查库并回填。我在项目中的代码大致是这样from django.core.cache import cache def get_index_context(): context cache.get(index_page_data) if context is None: context build_index_context() cache.set(index_page_data, context, 3600) return context这里有一个非常实用的技巧回填缓存的时候不仅写入数据还要写入一个独立的版本号key。这样运营在后台修改了首页数据后直接把index_page_version加一静态化脚本就会根据版本号变化判断是否需要重建而不是盲目等待缓存过期。这就把缓存、静态化、数据变更三者从各管各的变成了统一版本驱动。我的习惯是在首页视图的最前面先读缓存版本号如果版本号和上次一致直接返回静态文件如果版本号变了走重新生成逻辑。这个机制在排障时特别好用看到版本号变了但页面没更新至少能快速定位是重建脚本没执行还是文件写入失败。4.3 详情页缓存的分段设计详情页和首页不一样它不会把整个页面HTML全部缓存。原因是详情页中可能包含实时性较强的信息比如库存量、价格、促销状态如果整页缓存库存变化无法及时体现而且详情页的参数很多组合起来可能导致缓存key爆炸。更合理的做法是分段缓存商品基本信息缓存一份、商品图片列表缓存一份、商品SKU规格缓存一份、推荐商品缓存一份。组装页面时分别从缓存读取哪一段没有命中就只查对应的表然后回填这一段。分段缓存另一个好处是能控制失效粒度。比如价格调整时只需要更新SKU信息缓存不必清空图片缓存和推荐缓存。我在具体实现时key的设计是这样的sku:{id}:info存商品基本信息sku:{id}:images存图片列表sku:{id}:specs存规格和规格选项sku:{id}:recommend存同类推荐from django.core.cache import cache def get_sku_info(sku_id): key sku:%s:info % sku_id sku cache.get(key) if sku is None: sku GoodsSKU.objects.select_related(goods).get(idsku_id) cache.set(key, sku, 1800) return sku这里特别要注意一个细节缓存里存ORM对象还是存字典。我测试下来直接存ORM对象如果没做序列化django-redis默认会用pickle序列化虽然能存但取出来后缓存的对象和数据库对象分离后续修改可能有问题而且版本升级、模型字段变化时pickle反序列化可能失败。建议从数据库查出来之后把自己需要的字段序列化成字典或者简单的数据对象再放缓存取出来就是纯数据稳定且可控。4.4 浏览记录的缓存方案商品详情页还有一个高频功能是最近浏览很多电商项目里它是右栏的推荐区块。传统做法是查一张浏览记录表每次访问都写一条然后列表页再查最近几条。这个功能在全站访问量大时很容易成为数据库压力点因为它是一次页面访问、一次数据库写入。换成Redis之后可以用一个List结构存最近浏览的商品ID每次访问详情页时从列表头部插入当前商品ID然后截断只保留前N个读取时反向取出。from django.core.cache import cache def add_browsing_history(user_id, sku_id): key history:%s % user_id list_key cache.get(key) if list_key is None: list_key [] if sku_id in list_key: list_key.remove(sku_id) list_key.insert(0, sku_id) if len(list_key) 5: list_key list_key[:5] cache.set(key, list_key, 86400)这段逻辑本质上保证浏览记录永远只保留5条并且最新浏览的排在前面。如果项目已经有浏览记录表且存了所有历史数据那这个缓存列表更适合做一个近期浏览快捷入口真正的全量历史还是以数据库为准。Redis存储这种短列表场景很合适设计的时候只需要明确它是临时热点数据而不是持久化数据就不会混淆数据归属。5. 详情页、列表页核心业务逻辑拆解5.1 详情页的数据组装与模板渲染要点详情页的URL设计通常是/goods/{sku_id}一个SKU页面要展示的信息相对多SKU名称、副标题、价格、库存、销售计数、商品图片、规格参数、同类推荐、所属分类。组装这些数据时如果不注意很容易发生N1查询问题。比如先查SKU主表再循环查图片、再循环查规格、再循环查推荐SQL查询次数随数据量线性增长。我的做法是尽量用select_related和prefetch_related把关联查询一次性带出来。SKU关联商品用select_related商品图片是SKU的反向关联用prefetch_related推荐商品是商品的其他SKU先查出当前商品ID再一次性查出该商品下其他SKU。这样整个详情页的数据查询数量可以压缩到个位数。具体实现时可以写一个get_detail_context(sku_id)函数内部从数据库或缓存读取组装好一个字典再传给模板。模板渲染时有一个经验详情页的图片、规格选项、推荐SKU这三块最容易在模板里直接调对象.关系.属性链式访问每一层访问都隐含着一次数据库查询。调试时如果发现详情页接口执行了几十次SQL第一反应就该去把模板里的对象属性链改成视图函数里预先组装好的明确数据。这也是为什么我建议把上下文统一成字典结构而不是直接传ORM对象目的就是让模板只能用到预先查好的数据从设计上规避N1。5.2 列表页的排序、分页与新品推荐列表页是分类商品展示页URL通常是/list/{分类ID}/{页码}同时通过query string传递排序参数。排序方式一般是默认综合排序、按价格从低到高、按销量从高到低。Django实现排序不难一个order_by就能搞定但要注意的是价格排序可能出现相同价格情况此时最好再加一个次要排序字段保证排序结果稳定否则分页时可能出现同一商品在多个页里重复出现或漏掉。分页部分我用django.core.paginator.Paginator处理每页显示比如12个SKU。有一个坑就是页码参数类型query string里取出来的都是字符串如果不转成int直接用可能触发类型报错或者比较逻辑异常。一般我会在视图函数入口先做一次页面参数校验非法直接返回404。还有一个体验细节是分页控件页码多时不能全显示连续页要折叠我用一段自定义的页码切片逻辑根据当前页码动态生成页码列表比如只显示前两页、当前页的前后两页、最后两页中间用省略号代替。from django.core.paginator import Paginator, EmptyPage def page_list(page_num, total_page): if total_page 5: return range(1, total_page 1) page_list [] if page_num 3: page_list [1, 2, 3, 4, -1, total_page] elif page_num total_page - 2: page_list [1, -1, total_page - 3, total_page - 2, total_page - 1, total_page] else: page_list [1, -1, page_num - 1, page_num, page_num 1, -1, total_page] return page_list新品推荐在列表页通常是右侧展示的一个新品上架区块数据就是当前分类下最新添加的几个SKU。这个数据更新频率不高完全可以用Redis缓存我设计成一个分类维度下的简单列表比如category:{id}:new_skus存上最新8个SKU的ID和基础信息缓存时间设成1小时。这样即使分类商品几千个新品推荐区域也不会拖慢整个列表页的响应速度。5.3 列表页与缓存配合的查询时序列表页的请求在并发高时同样会有压力。尤其是排序、分页这些动态参数比较多整个页面全量缓存不太合适但两类数据是可以稳定缓存的一是当前分类的新品推荐二是分类的基础信息名称、图片、子分类列表。每次进入列表页视图时先读这两个缓存项它们命中的话整个页面的大部分数据就不需要重新查库只有当前页的SKU数据还需要根据排序参数实时查。这样做的好处是合理分配了静态化和缓存的边界列表页的当前页数据仍然需要动态查询因为分页和排序是用户驱动的不能静态化但公共信息、推荐信息这种所有用户都一样的就可以缓存。这里顺带提一个我在项目中踩过的坑新品推荐缓存如果设置在分类页每个分页请求里都读没问题但如果把它放到一个自定义的context_processors里那所有页面请求都会查一次缓存浪费了连接后来我把这个推荐数据改成列表页视图函数内部读取避免全局模板上下文处理器的损耗。6. 常见问题与排查技巧实录6.1 FastDFS上传成功但图片无法访问这类问题几乎每个人都遇到过。上传成功说明Django到Tracker和Storage的数据通道是好的图片无法访问问题基本出在访问链路上。按顺序检查这几个点首先确认FDFS_BASE_URL里的IP和端口是不是Storage节点Nginx的地址其次确认Storage节点上的Nginx中location配置的alias路径和storage.conf中的store_path一致最后确认防火墙或安全组是否放行了Nginx端口。还有一个容易被忽视的问题FastDFS返回的file_id里包含group名称比如group1/M00/00/00/wKg...jpg。如果Nginx配置成location /group1/M00/ { alias /data/fastdfs/storage/data/; }注意alias结尾的斜杠少一个斜杠可能导致路径拼接错误Nginx报404或者403。这类问题排查时可以先用curl直接请求图片完整URL再在FastDFS服务器上用fdfs_test工具验证文件是否真实存在逐步缩小范围。6.2 静态页面一直不更新或空白最常见的原因是生成脚本没有被触发或者生成了但写到了错误的目录。先在服务器上手动执行一次生成函数看输出的HTML文件内容和实际数据库数据是否一致如果一致再看Nginx的root或alias是不是指向了这个文件。还有一种情况是生成了文件但用户访问到的还是旧的Nginx缓存给静态文件加个响应头Cache-Control: no-cache或者文件名带内容hash可以解决。另外我发现很多排障卡在我手动调用后台生成了页面还是旧内容这个现象上结果定位半天发现是Nginx配置里location规则被更具体的选中了请求根本没有落到生成的文件上。针对这种场景最有效的排查方式是在Nginx的access log里记录实际返回的file路径确认最终静态页来源。这一步能快速判断问题是在文件生成层还是Nginx路由层。6.3 缓存穿透、击穿与雪崩的初体验首页和详情页引入Redis后一个现实问题是缓存故障的影响范围。如果Redis服务挂了所有请求会直接落到数据库用户多时瞬间把数据库压垮这就是新手最容易遇见的缓存雪崩。预防手段有几个缓存过期时间加随机偏移避免同一时刻大面积失效热点key不设过期时间靠后台任务主动更新极端情况下加本地内存缓存兜底Redis挂了至少还有一层。缓存穿透则是查询一个必定不存在的key比如用户访问一个不存在的SKU ID每次都绕过缓存直接查库。我的处理是查库结果为空时也写入缓存存一个空值或特殊标记并设置短过期时间。还有缓存击穿某个热点key在过期瞬间被大量请求打到数据库解决思路是加互斥锁只让一个请求去查库重建缓存其他请求等待或返回旧值。做这些防护不复杂但一定要在项目里预留好实现位置否则上线后遇到突发流量再补就晚了。6.4 分页页码错乱与排序不稳定的修复列表页的分页错乱很多时候不是Paginator的问题而是排序不唯一。数据库查询在没有明确次级排序时返回顺序是不确定的最后一条记录可能在两页之间跳变。解决方式就是order_by(price, id)或order_by(-sales, id)ID作为稳定次序的锚点。还有页码参数异常的情况前端传入负数或超级大数字时Django的Paginator会抛异常需要在视图里统一拦截并返回空页或第一页。我在做列表页的时候还发现少数商品在某次价格修改后排序顺序确实变了而浏览器的用户可能停留在旧的页码内容里缓存里也没有更新。这类问题没有完美方案但可以在响应头里标注正确的Cache-Control并给页面URL加上排序参数让浏览器识别为不同URL避免串页。6.5 缓存和Redis内存增长的处理Redis缓存用起来之后如果不加清理策略内存会缓慢上涨特别是详情页缓存和浏览记录这类带用户维度信息的key。生产环境Redis我一般设置maxmemory-policy allkeys-lru这样即使缓存量大也是优先淘汰最新访问的数据不会OOM。另一种做法是给所有自定义key统一加项目前缀比如daily_fresh:这样排查时通过scan命令可以快速枚举出所有业务key避免用keys *阻塞Redis。浏览记录这类用户维度缓存过期时间不宜设得太长一天足够了。如果用户量很大建议定期清理超过N天未活跃的浏览记录key。我还试过给缓存写入时增加一个简单的容量估算当某类key总量超限时自动缩短过期时间这种做法对控制内存比较有效但需要额外维护一套容量统计数据项目不大时用LRU策略就够了。6.6 实际排障的常用命令与工具把常用命令记录下来对排查问题效率提升很明显。FastDFS客户端工具fdfs_upload_file、fdfs_download_file、fdfs_file_info可以直接测试Storage上文件的读写fdfs_monitor可以查看Tracker记录的Storage状态确认节点是否在线、同步是否正常。查看Tracker和Storage日志时重点看在报错时间点附近有没有连接拒绝、超时异常、磁盘空间不足的提示。Redis侧常用info memory看内存使用用scan遍历key用ttl查看缓存剩余时间。如果怀疑某个key的数据不对get key直接取值再对比数据库数据。Django侧最实用的工具就是django-debug-toolbar它能显示每个请求执行的SQL数量和耗时以及cache命中情况所有性能问题的定位几乎都是从这块面板开始的。结尾这套FastDFS加页面静态化加Redis缓存的组合我在生鲜电商项目里跑过一段时间之后一个很深的体会是各项技术单独拎出来都不复杂难的是搞清楚它们各自负责的层次以及如何用一个统一的数据版本机制把它们串起来。静态化负责最高频的首页流量缓存负责次高频的详情页和公共数据FastDFS把文件从应用实例中解放出来。三者各司其职但又在数据更新后如何重新生成、如何失效、如何回填这些细节上交织在一起。我个人的建议是动手编码前先画清楚一条线什么变化触发静态化重建什么变化只更新缓存什么变化什么都不用做。这条线理清了项目跑起来就会顺很多理不清就会出现改完后台数据前台半天不更新的问题。实际上这套方案后续还可以扩展比如把静态页生成改成Celery异步任务、把缓存升级为多级缓存、给FastDFS加集群和高可用但底层思路不会变。先把手上的链路跑通再谈优化这个顺序错不了。