274 lines
6.7 KiB
Markdown
274 lines
6.7 KiB
Markdown
# 性能优化指南
|
||
|
||
:::tip 说明
|
||
本文介绍本项目中常见的性能优化策略,涵盖后端 ORM 查询、缓存、分页、前端加载等方面。
|
||
:::
|
||
|
||
## ORM 查询优化
|
||
|
||
### N+1 查询问题
|
||
|
||
N+1 查询是最常见的性能问题:查询 N 条记录时,每条记录又触发一次关联查询。
|
||
|
||
```python
|
||
# ❌ 错误: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 vs prefetch_related
|
||
|
||
| 方法 | 适用场景 | 实现方式 |
|
||
|------|---------|---------|
|
||
| `select_related` | 外键关联(ForeignKey) | JOIN 查询,一次 SQL |
|
||
| `prefetch_related` | 多对多/反向关联 | 两次查询,Python 层合并 |
|
||
|
||
### 只查询需要的字段
|
||
|
||
```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')
|
||
```
|
||
|
||
### 避免循环查询
|
||
|
||
```python
|
||
# ❌ 错误:循环中查询数据库
|
||
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` | 查询字段都在索引中 |
|
||
|
||
### 索引设计原则
|
||
|
||
```python
|
||
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 分析慢查询
|
||
```
|
||
|
||
## 分页优化
|
||
|
||
### 传统分页 vs 游标分页
|
||
|
||
```python
|
||
# 传统分页(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 字段必须有索引
|
||
```
|
||
|
||
## Redis 缓存策略
|
||
|
||
### 缓存穿透
|
||
|
||
查询不存在的数据,每次都打到数据库:
|
||
|
||
```python
|
||
# 解决方案:缓存空值
|
||
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
|
||
```
|
||
|
||
### 缓存击穿
|
||
|
||
热点数据过期时,大量请求打到数据库:
|
||
|
||
```python
|
||
# 解决方案:互斥锁
|
||
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)
|
||
```
|
||
|
||
### 缓存雪崩
|
||
|
||
大量缓存同时过期,请求全部打到数据库:
|
||
|
||
```python
|
||
# 解决方案:随机 TTL
|
||
import random
|
||
base_timeout = 3600
|
||
random_offset = random.randint(0, 600)
|
||
cache.set(key, data, timeout=base_timeout + random_offset)
|
||
```
|
||
|
||
## 前端性能优化
|
||
|
||
### 路由懒加载
|
||
|
||
```typescript
|
||
// 路由懒加载:按需加载页面组件
|
||
const routes = [
|
||
{
|
||
path: '/system/user',
|
||
component: () => import('@/views/system/user/index.vue'),
|
||
},
|
||
];
|
||
```
|
||
|
||
### 组件按需导入
|
||
|
||
```typescript
|
||
// Ant Design Vue 按需导入
|
||
import { Button, Table, Form } from 'ant-design-vue';
|
||
```
|
||
|
||
### 图片懒加载
|
||
|
||
```vue
|
||
<img v-lazy="imageUrl" alt="图片" />
|
||
```
|
||
|
||
### 代码分割
|
||
|
||
```typescript
|
||
// Vite 自动代码分割
|
||
// 大型依赖单独打包
|
||
import(/* webpackChunkName: "editor" */ '@wangeditor/editor');
|
||
```
|
||
|
||
## Gunicorn Worker 配置
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
## 数据库连接池
|
||
|
||
```python
|
||
# application/settings.py
|
||
DATABASES = {
|
||
'default': {
|
||
'ENGINE': 'django.db.backends.mysql',
|
||
'CONN_MAX_AGE': 600, # 连接存活时间(秒)
|
||
'CONN_HEALTH_CHECKS': True, # 连接健康检查
|
||
}
|
||
}
|
||
```
|
||
|
||
## 慢查询排查
|
||
|
||
```bash
|
||
# 开启 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、数据库连接池
|
||
```
|