Become a sponsor

说明
本文介绍本项目中常见的性能优化策略,涵盖后端 ORM 查询、缓存、分页、前端加载等方面。
N+1 查询是最常见的性能问题:查询 N 条记录时,每条记录又触发一次关联查询。
# ❌ 错误:N+1 查询
articles = Article.objects.filter(is_delete=False) # 1 次查询
for article in articles:
category = Category.objects.get(id=article.category_id) # N 次查询
# ✅ 正确:使用 select_related(外键关联)
articles = Article.objects.filter(is_delete=False).select_related('category')
# ✅ 正确:使用 prefetch_related(多对多/反向关联)
roles = Role.objects.filter(is_delete=False).prefetch_related('menus')| 方法 | 适用场景 | 实现方式 |
|---|---|---|
select_related | 外键关联(ForeignKey) | JOIN 查询,一次 SQL |
prefetch_related | 多对多/反向关联 | 两次查询,Python 层合并 |
# ❌ 错误:SELECT * 查询所有字段
users = User.objects.filter(is_delete=False)
# ✅ 正确:只查询需要的字段
users = User.objects.filter(is_delete=False).values('id', 'username', 'realname')
# ✅ 正确:使用 only() 只加载指定字段
users = User.objects.filter(is_delete=False).only('id', 'username', 'realname')# ❌ 错误:循环中查询数据库
for role_id in role_ids:
role = Role.objects.get(id=role_id)
# ✅ 正确:批量查询
roles = Role.objects.filter(id__in=role_ids)| 类型 | 说明 | 适用场景 |
|---|---|---|
| 单列索引 | db_index=True | WHERE 条件字段 |
| 唯一索引 | unique=True | 唯一性约束字段 |
| 复合索引 | index_together | 多字段联合查询 |
| 覆盖索引 | include | 查询字段都在索引中 |
class Example(BaseModel):
name = models.CharField(max_length=100, db_index=True) # 常用查询字段加索引
status = models.IntegerField(db_index=True) # 状态字段加索引
category_id = models.IntegerField(db_index=True) # 外键字段加索引
class Meta:
db_table = 'django_example'
index_together = [
['status', 'is_delete'], # 复合索引:状态 + 软删除
]1. 不要过度索引:每个索引都会降低写入性能
2. 选择性高的字段优先:如 username 比 status 选择性更高
3. 复合索引顺序:选择性高的字段放前面
4. 定期分析:使用 EXPLAIN 分析慢查询# 传统分页(OFFSET):大偏移量时性能差
paginator = Paginator(queryset, page_size)
page = paginator.page(page_no) # OFFSET 100000 时很慢
# 游标分页(推荐大数据量):性能稳定
last_id = request.GET.get('lastId', 0)
queryset = Example.objects.filter(
is_delete=False, id__gt=last_id
).order_by('id')[:page_size]1. 小数据量(< 10万):传统分页即可
2. 大数据量(> 10万):使用游标分页
3. 避免 SELECT *:只查需要的字段
4. 避免 COUNT(*):大表 COUNT 很慢,可用缓存
5. 使用索引:ORDER BY 字段必须有索引查询不存在的数据,每次都打到数据库:
# 解决方案:缓存空值
def get_user(user_id):
cache_key = f'user:{user_id}'
cached = cache.get(cache_key)
if cached is not None:
return None if cached == 'NULL' else cached
user = User.objects.filter(id=user_id, is_delete=False).first()
if user:
cache.set(cache_key, user, timeout=3600)
else:
cache.set(cache_key, 'NULL', timeout=300) # 缓存空值,短 TTL
return user热点数据过期时,大量请求打到数据库:
# 解决方案:互斥锁
def get_hot_data(key):
cached = cache.get(key)
if cached:
return cached
# 加锁,只允许一个请求查询数据库
lock_key = f'lock:{key}'
if cache.add(lock_key, '1', timeout=10):
try:
data = query_database(key)
cache.set(key, data, timeout=3600)
return data
finally:
cache.delete(lock_key)
else:
# 等待其他请求查询完成
time.sleep(0.1)
return get_hot_data(key)大量缓存同时过期,请求全部打到数据库:
# 解决方案:随机 TTL
import random
base_timeout = 3600
random_offset = random.randint(0, 600)
cache.set(key, data, timeout=base_timeout + random_offset)// 路由懒加载:按需加载页面组件
const routes = [
{
path: '/system/user',
component: () => import('@/views/system/user/index.vue'),
},
];// ElementPlus 按需导入
import { ElButton, ElTable, ElForm } from 'element-plus';<img v-lazy="imageUrl" alt="图片" />// Vite 自动代码分割
// 大型依赖单独打包
import(/* webpackChunkName: "editor" */ '@wangeditor/editor');# gunicorn.conf.py
import multiprocessing
# Worker 数量:CPU 核心数 * 2 + 1
workers = multiprocessing.cpu_count() * 2 + 1
# Worker 类型:sync(默认)/ gevent(异步)
worker_class = 'gevent'
# 超时时间
timeout = 120
# 最大请求数(防止内存泄漏)
max_requests = 1000
max_requests_jitter = 50# application/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'CONN_MAX_AGE': 600, # 连接存活时间(秒)
'CONN_HEALTH_CHECKS': True, # 连接健康检查
}
}# 开启 MySQL 慢查询日志
mysql -u root -p -e "
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
"
# 查看慢查询
tail -f /var/log/mysql/slow.log
# 分析查询计划
mysql -u root -p -e "EXPLAIN SELECT * FROM django_example WHERE status = 1;"性能优化的核心原则:
1. ORM 优化:避免 N+1 查询,使用 select_related/prefetch_related
2. 索引策略:WHERE/ORDER BY 字段加索引,避免过度索引
3. 分页优化:大数据量使用游标分页
4. 缓存策略:穿透/击穿/雪崩三种场景的解决方案
5. 前端优化:路由懒加载、组件按需导入、图片懒加载
6. 配置调优:Gunicorn Worker、数据库连接池