从接口契约开始控制性能
一个返回全部记录的接口,数据量小时看不出问题,数据增长后会同时放大数据库扫描、序列化、网络传输和客户端渲染成本。稳定的 API 应该从契约上限制单次返回量,并让调用方能够得到下一页。站内的 Django REST Framework 实战 适合先了解接口结构;本文重点放在数据访问和缓存边界。
分页、缓存和查询优化要一起设计:分页减少单次工作量,查询优化减少每条记录的额外查询,缓存避免短时间内重复执行相同工作。只加缓存而不限制结果集,往往只是把昂贵结果暂存在内存里。
选择合适的分页方式
后台列表通常使用页码分页,移动端连续加载可以使用 LimitOffset,按时间线滚动则更适合 CursorPagination。无论哪种方式,都要设置最大 page size,避免客户端传入一个极大的 page_size。
# api/pagination.py
from rest_framework.pagination import PageNumberPagination
class SmallPagePagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
# api/views.py
from rest_framework.generics import ListAPIView
from .pagination import SmallPagePagination
from .models import Article
from .serializers import ArticleListSerializer
class ArticleListView(ListAPIView):
serializer_class = ArticleListSerializer
pagination_class = SmallPagePagination
def get_queryset(self):
return (
Article.objects
.filter(status="published")
.select_related("author", "category")
.only("id", "title", "slug", "published_at", "author__id", "author__username", "category__id", "category__name")
.order_by("-published_at", "-id")
)
分页排序必须稳定。只按 published_at 排序时,同一时间发布的记录可能在页与页之间重复或遗漏;增加唯一的 id 作为第二排序键即可。only() 只保留序列化确实用到的字段,若代码访问了未加载字段,Django 会额外发起查询,所以应与 serializer 一起维护。
先识别 N+1 查询
列表中访问外键时使用 select_related,它通过 JOIN 在一次查询中取出一对一或多对一关系;多对多和一对多关系使用 prefetch_related,由 Django 分别查询后在内存中拼接。比如文章列表还要显示标签:
queryset = (
Article.objects
.filter(status="published")
.select_related("author", "category")
.prefetch_related("tags")
.order_by("-published_at", "-id")
)
不要在 serializer 的 get_ 方法里对每一行调用 Model.objects.get()。这会把一条列表查询变成 N+1 条查询。复杂聚合可以使用 annotate 在数据库中完成:
from django.db.models import Count
queryset = (
Article.objects
.filter(status="published")
.annotate(comment_count=Count("comments", distinct=True))
.order_by("-published_at", "-id")
)
数据模型和 ORM 的更多实践可参考 Django ORM 深度解析。如果一个接口包含多个昂贵聚合,先拆出列表与详情接口,避免让列表响应承担详情页的全部字段。
低级缓存应该缓存什么
缓存适合读取频繁、变化不频繁、可以接受短暂旧数据的结果。例如热门文章列表可以缓存序列化后的字典,而不是缓存 QuerySet 对象。QuerySet 依赖数据库连接和模型代码,直接缓存会让失效和序列化边界变得模糊。
# api/views.py
from django.core.cache import cache
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Article
from .serializers import ArticleListSerializer
class FeaturedArticleView(APIView):
cache_key = "api:featured-articles:v1"
def get(self, request):
data = cache.get(self.cache_key)
if data is None:
articles = (
Article.objects
.filter(status="published", is_featured=True)
.select_related("author")
.order_by("-published_at", "-id")[:10]
)
data = ArticleListSerializer(articles, many=True).data
cache.set(self.cache_key, data, timeout=300)
return Response(data)
缓存键要带版本号或筛选条件,避免不同接口互相覆盖。用户相关数据不能使用公共键;权限、语言、租户和排序条件都应进入键。缓存未命中时可能有并发请求同时回源,热点接口可使用互斥锁或短暂随机过期时间降低雪崩风险。
更新数据时主动失效
五分钟 TTL 只是兜底,不代表文章发布后必须等待五分钟才能在列表中出现。写入成功后删除相关键,或者递增一个版本号,让下一次读取自然生成新结果。失效操作应放在事务提交之后,避免数据库回滚而缓存已经被删除:
from django.core.cache import cache
from django.db import transaction
def publish_article(article):
article.status = "published"
article.save(update_fields=["status", "updated_at"])
transaction.on_commit(
lambda: cache.delete("api:featured-articles:v1")
)
如果缓存由多个进程共享,应使用 Redis 或 Memcached,而不是每个进程独立的本地内存缓存。缓存配置、后台任务和部署环境的关系也可结合 Django 异步任务实战 一起规划。
用测试防止查询退化
性能优化必须可验证。Django 的 assertNumQueries 可以把列表接口的查询数量固定下来;它不保证 SQL 永远最快,但能及时发现有人在 serializer 中引入 N+1。
from django.test import TestCase
from rest_framework.test import APIClient
from tests.factories import ArticleFactory
class ArticleListQueryTests(TestCase):
def test_list_query_count(self):
ArticleFactory.create_batch(20, status="published")
client = APIClient()
with self.assertNumQueries(3):
response = client.get("/api/articles/?page_size=20")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data["results"]), 20)
这里的 3 次查询取决于当前模型关系和 serializer,实际项目应先运行测试观察基线,再固定合理的数量。测试数据应包含重复作者、标签和空关系,才能覆盖真实的预取路径。
让数据库承担它擅长的工作
给常用过滤和排序字段建立合适索引,例如 status、published_at 的组合索引;不要为了每个字段都建索引,因为写入和迁移成本也会增加。使用 QuerySet.explain() 检查执行计划,确认索引真的被使用。查询优化还应关注返回字段、事务范围和连接池,而不是只看 Python 代码耗时。
最后,用访问日志记录 endpoint、状态码、分页大小和耗时,再按 P95 找慢接口。通用的缓存、数据库和前端协同方法可以参考 Web 与 API 性能优化,完整站点的加载指标则可结合 网站性能优化实战 观察。分页限制结果集、查询预取关系、缓存可复用结果,再用测试和日志持续验证,才能让接口在数据增长后保持稳定。
评论 (0)
暂无评论,快来抢沙发吧!