feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra

This commit is contained in:
async-upgrade
2026-09-06 14:26:17 +08:00
parent 9a6577f71e
commit 8f488fcaaa
55 changed files with 2224 additions and 1513 deletions
+74
View File
@@ -0,0 +1,74 @@
# ADRF 异步化转换规范(chunyu_project 视图层)
## 目标
将 DRF 同步视图迁移到 ADRF(Asynchronous Django REST Framework)+ Django 5.x 异步 ORM,实现从视图到数据库的全链路非阻塞。
## 核心规则
### 1. 导入替换
- `from rest_framework.views import APIView` → `from adrf.views import APIView`
- `from rest_framework import generics` / `rest_framework.generics.XXX` → `from adrf import generics`(或 `from adrf.generics import ListCreateAPIView` 等)
- `from rest_framework.viewsets import ModelViewSet/ReadOnlyModelViewSet` → `from adrf.viewsets import ModelViewSet, ReadOnlyModelViewSet`
- `from rest_framework.mixins import ...` → `from adrf import mixins`
- 保留:`Response`, `status`, `permissions`, `filters`, `pagination`, `serializers`, `django_filters`, `drf_yasg` 的导入不变。
- `get_object_or_404` → `from adrf.generics import aget_object_or_404`(在 async 上下文中使用)
### 2. 处理器异步化
- 视图类中的 HTTP 方法处理器改为 `async def`(get/post/put/patch/delete/list/retrieve/create/update/destroy 及自定义 action)。
- ADRF 特性:类内任意一个处理器是 async,dispatch 即走事件循环;残留的同步处理器会被自动 `sync_to_async` 兜底(不会崩,但会占线程池——尽量全部转完)。
### 3. 同步 ORM → 异步 ORM(Django 5.x,逐个替换,不可遗漏)
| 同步 | 异步 |
|---|---|
| `Model.objects.get(...)` | `await Model.objects.aget(...)` |
| `filter(...).first()` | `await Model.objects.filter(...).afirst()` |
| `filter(...).exists()` | `await Model.objects.filter(...).aexists()` |
| `filter(...).count()` | `await Model.objects.filter(...).acount()` |
| `Model.objects.create(...)` | `await Model.objects.acreate(...)` |
| `obj.save()` | `await obj.asave()` |
| `obj.delete()` | `await obj.adelete()` |
| `queryset.update(...)` | `await queryset.aupdate(...)` |
| `for x in queryset:` | `async for x in queryset:` |
| `list(queryset)` / 切片后遍历 | `results = [x async for x in queryset[start:end]]` |
| `get_object_or_404(...)` | `await aget_object_or_404(...)` |
| `len(queryset)`(避免) | `acount()` |
注意:
- **切片**:`queryset[0:10]` 本身是惰性的,切片可以保留;但取值/遍历必须 async。
- **values()/values_list()**:`[x async for x in Model.objects.filter(...).values(...)]` 合法。
- **select_related/prefetch_related 链**:保留,遍历时 async。
- **聚合**:`await Model.objects.aggregate(...)`(Django 5.x 支持 aggregate 的异步版本是 `Model.objects.acount` 等;`aggregate()` 无原生异步 —— 用 `await sync_to_async(Model.objects.aggregate)(...)`)。
- **事务** `transaction.atomic()`:在 async 上下文中改为 `await sync_to_async(fn)()` 整体包裹,或使用 `transaction.atomic` 的异步支持 `async with transaction.acreate_agent`——最稳妥是 `await sync_to_async(sync_business_fn)()`。
- **Q 对象/复杂查询**:构造部分是同步的,保留。
### 4. 外部 HTTP 调用
- `import requests` + `requests.get/post(...)` → 改用 `aiohttp`(已安装):
```python
import aiohttp
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
async with session.get(url, params=..., headers=...) as resp:
data = await resp.json()
```
- 文件上传(如百度语音/图片识别)用 `aiohttp.FormData()`。
- 若改写风险过大(复杂 multipart),可用 `await sync_to_async(requests.post)(...)` 兜底,但要加注释 `# TODO: aiohttp 化`。
### 5. 必须保持不变
- 所有 `@swagger_auto_schema` 装饰器原样保留(drf-yasg 只附加元数据,与 async 兼容)。
- 所有 permission_classes、authentication_classes、pagination/filter 配置不变。
- URL 路由文件(urls.py)不改 —— as_view() 不变。
- 返回的 JSON 结构、状态码、错误信息完全不变(前端依赖这些契约)。
- Celery `.delay()` 调用保留(可在 async 中直接调用;如需保险用 `await sync_to_async(task.delay)(...)`)。
- `request.data` / `request.query_params` / `request.user` 用法不变(ADRF 的 AsyncRequest 兼容)。
### 6. 缓存操作
- `cache.get/set` 是同步网络 I/O → `await sync_to_async(cache.get)(key)` 或保持 django-redis 同步调用并用 sync_to_async 包裹。
- 简单做法:`from asgiref.sync import sync_to_async`,然后 `await sync_to_async(cache.set)(key, val, ttl)`。
### 7. 文件/验证
- 每改完一个文件执行:`python -m py_compile <file>` 确保语法正确。
- 不要运行服务器/测试(由主会话统一验证)。
- 不确定某个 ORM 调用是否有异步版本时,用 `sync_to_async` 包裹并加 `# TODO: async ORM` 注释 —— 宁可兜底也不要留下裸同步 ORM 调用(会抛 SynchronousOnlyOperation)。
### 8. 输出要求
- 逐文件报告:改了哪些处理器、哪些 ORM 调用、哪些外部 HTTP、是否有 sync_to_async 兜底点。
- 列出任何你不敢改的复杂点(如嵌套事务、信号)。
+57
View File
@@ -0,0 +1,57 @@
# ADRF 原生异步序列化器转换规范(第二阶段:100% 异步化)
## 目标
将 serializer 层的同步 ORM 与 MethodField 全部原生异步化,消灭视图层 `sync_to_async(serializer...)` 兜底。
## 核心 API(adrf 0.1.14)
```python
# 导入替换
from rest_framework.serializers import (
ModelSerializer, Serializer, SerializerMethodField, ...
)
# → 全部改为
from adrf.serializers import (
ModelSerializer, Serializer, SerializerMethodField, ...
)
# adrf.serializers 同时重导出了全部异步化字段(CharField/IntegerField/... 均已支持)
```
### 1. MethodField 异步化
```python
class XSerializer(ModelSerializer):
foo = SerializerMethodField()
async def get_foo(self, obj): # async def 即可,adrf.fields.SerializerMethodField 支持
count = await Related.objects.filter(x=obj).acount()
return count
```
### 2. 序列化输出
```python
# 视图中
data = await serializer.adata # 异步属性(替代 serializer.data)
# 或在 adrf mixins/泛型内部使用 adrf.mixins.get_data(serializer)
```
- `adata` 内部逐字段异步调 to_representation,MethodField 异步方法会被正确 await。
- **many=True** 同样支持:`data = await serializer.adata`(ListSerializer 已被 adrf BaseSerializer.many_init 覆盖)。
- nested Serializer:嵌套的 serializer 也必须来自 adrf.serializers,否则其 .data 是同步求值。
### 3. 写路径
```python
await serializer.asave() # 替代 sync_to_async(serializer.save)
instance = await serializer.asave() # 返回 instance(同 DRF .save() 语义)
# serializer.create(...) → 改写为 async def acreate(self, validated_data),内部用 await Model.objects.acreate(...)
# serializer.update(...) → async def aupdate(self, instance, validated_data),内部 aget/asave/aupdate
```
- `is_valid()` 是纯 CPU 校验(无 DB 除非 validator 带查询)——保持同步调用即可;若 validators 内部有 DB 查询(如 UniqueValidator 会查库),视图侧仍需 `await sync_to_async(serializer.is_valid)()` 或改自定义 validator 为 async。**UniqueValidator 场景保留 sync_to_async 包裹 is_valid。**
### 4. 约束
- **不得**在同步方法(get_queryset、get_serializer_class、validate 等 hooks)中调用 ORM——保持现状。
- `validate(self, attrs)` 内的 DB查询(若存在)→ 改 `async def validate`(adrf 支持异步 validate?——不支持!validate 由 is_valid 同步调用。validate 内的 DB查询必须改用 CustomValidator 异步类或保留视图侧包裹)。遇到时在报告中列出。
- ModelSerializer 字段声明(fields=..., read_only_fields 等)不变。
### 5. 每文件验证
`python -m py_compile <file>` 必须 0 退出。不运行服务器。
### 6. 报告要求
逐文件列出:改动的 MethodField 数、acreate/aupdate 重写数、残留的 sync_to_async 必要点(含原因)。
+151
View File
@@ -0,0 +1,151 @@
# 可乐平台 异步化架构(ADRF + Granian + PostgreSQL + Nginx)
## 架构总览
```
┌──────────┐ :8080 ┌─────────────────────┐
│ 浏览器 │──────────▶│ Nginx (frontend) │
└──────────┘ │ ├─ / → React SPA 静态产物 (build/, 动静分离)
│ ├─ /api 等前缀 ─┐
│ └─ /ws/ ─┤ 反向代理
└──────────────────┼───┘
▼
┌───────────────────────────────┐
│ Granian (Rust) :8000 │
│ Django 5.2 ASGI + ADRF │
│ ├─ 异步视图 (async def) │
│ ├─ 异步 ORM (aget/afilter) │
│ └─ Channels (WebSocket) │
└───────┬──────────────┬────────┘
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PostgreSQL 16 │ │ Redis 7 │
│ psycopg3 异步 │ │ cache/session│
│ ORM 驱动 │ │ celery/ws层 │
└──────────────┘ └──────────────┘
```
## 技术选型
| 层 | 技术 | 说明 |
|---|---|---|
| Web 框架 | Django 5.2 + DRF 3.17 | 原生异步能力 |
| 异步视图 | **ADRF 0.1.14** | 所有 API 视图跑在异步事件循环上 |
| 应用服务器 | **Granian 2.8.2 (Rust)** | 替代 Daphne,ASGI + WebSocket,吞吐量显著提升 |
| 数据库 | **PostgreSQL 16** | Django 5 异步 ORM 必备(MySQL 后端不支持异步) |
| DB 驱动 | **psycopg3 (binary) 3.2.13** | 支持异步连接 |
| 缓存/队列 | Redis 7 | 缓存、Session、Celery broker、Channels layer |
| 前端 | React 19 + Vite 8 | 独立 SPA,与 Django 模板层完全解耦 |
| 反代/静态 | Nginx | SPA 静态资源直出 + API 反向代理 |
## 关键设计决策
1. **全链路异步**:视图 `async def` → ORM `aget/afirst/aexists/acount/acreate/asave/aupdate/adelete` → PostgreSQL(psycopg3)。`CONN_MAX_AGE=0`(异步 ORM 不支持持久连接)。
2. **ADRF 兼容性**:`view_is_async` 自动检测处理器;纯 CPU 视图(图形验证码、图像压缩、文本 diff)保持同步 `def`,ADRF 自动 `sync_to_async` 线程池兜底。
3. **外部 HTTP 调用 aiohttp 化**:weather / air_quality / currency / getIpData 全部改为 `aiohttp.ClientSession`,无阻塞 I/O。
4. **Redis 缓存 / Celery / SMTP / 密码哈希**:同步客户端用 `sync_to_async` 包裹(不阻塞事件循环)。
5. **事务兜底**:`transaction.atomic` + `select_for_update` 段(钱包、任务领取)抽成同步闭包整体 `sync_to_async` 执行,保住行锁语义。
6. **数据库可切换**:`DB_ENGINE` 环境变量支持 `django.db.backends.postgresql`(默认)与 `django.db.backends.mysql`(本地过渡)。
## 前后端分离
- 开发:`pnpm dev`(Vite HMR :5173),`BACKEND_URL` 环境变量指定后端(默认 `http://127.0.0.1:8002`,Docker 后端用 `http://127.0.0.1:8000`)。API 全部走 Vite proxy,无 CORS 负担。
- 生产:`pnpm build` 产物由 Nginx 容器直出(`try_files → index.html` SPA 回退),带指纹资源 1 年 immutable 缓存,index.html no-cache。
- 前后端仅通过 RESTful API + WebSocket 交互。
## 快速开始
### 一键部署(生产形态)
```bash
docker compose up -d --build
# 前端 http://localhost:8080
# 后端 API http://localhost:8000
# Swagger http://localhost:8080/swagger/
```
### 本地开发
```bash
# 1. 启动数据层
docker compose up -d postgres redis
# 2. 后端(异步栈)
cd chunyu_project
export DB_ENGINE=django.db.backends.postgresql DB_HOST=localhost DB_PORT=5432 \
DB_NAME=chunyu DB_USER=chunyu DB_PASSWORD=chunyu_pg_pass \
REDIS_HOST=localhost
python manage.py migrate
granian --interface asgi --host 0.0.0.0 --port 8000 chunyu_project.asgi:application
# 3. 前端(Vite HMR)
cd chunyu_project_react
BACKEND_URL=http://127.0.0.1:8000 pnpm dev # http://localhost:5173
```
## 异步化转换统计
- 覆盖 16 个应用、45 个视图文件:`api` `user` `article` `learn` `chat` `message` `history` `search` `bug` `tool` `weather` `air_quality` `currency` `shorturl` `logs` `apidirectory` `app`
- 处理器全部 `async def`(约 90+ 个);ORM 调用全部异步化
- 兜底点(`sync_to_async`):serializer is_valid/save/data、Redis cache、SMTP/邮件任务、密码哈希、文件存储 I/O、`transaction.atomic` 事务段、Celery `.delay`
- 转换规范见 `chunyu_project/docs/ADRF_CONVERSION_GUIDE.md`
## 验证记录
- ✅ `python manage.py check` 0 错误
- ✅ URLCONF 全量导入成功(所有视图模块可加载)
- ✅ Granian 启动 + 冒烟:`/`、`/article/articles/`、`/learn/courses/`、`/api/apidirectory/categories/`、`/api/weather/?city=Beijing`、`/api/air-quality/?city=Shanghai`、`/api/currency/rates/?base=USD`、`/api/image-captcha/`、`/api/captcha/generate/`、`/swagger.json`(331KB) 全部 200
- ✅ `docker compose config` 校验通过
- ✅ Nginx 配置语法 `nginx -t` 通过
## 服务器部署(192.168.5.7 生产/联调环境)
本地 Windows 宿主虚拟化暂时不可用(Hypervisor 未加载,见下),已将整套异步架构部署至本地 Linux 服务器:
| 项 | 值 |
|---|---|
| 服务器 | Debian,16 核(`lan-19216857` SSH profile) |
| 部署目录 | `/mnt/sda/chunyu` |
| 访问地址 | `http://192.168.5.7:18080`(前端 SPA + API 反代) |
| 后端直连 | `http://192.168.5.7:18000`(Granian) |
| PostgreSQL | `192.168.5.7:15432`(容器内 5432) |
| Redis | `192.168.5.7:16379`(容器内 6379) |
端口重映射原因:服务器既有服务占用 5432/6379/8080,覆盖文件 `docker-compose.server.yml` 将宿主端口改为 15432/16379/18000/18080,容器间通信仍走内部网络不受影响。
部署命令:
```bash
cd /mnt/sda/chunyu
docker compose -f docker-compose.yml -f docker-compose.server.yml up -d --build
```
服务器端到端验证记录(全部通过):
- ✅ 四容器运行:chunyu-backend(healthy) / chunyu-frontend / chunyu-postgres(healthy) / chunyu-redis(healthy)
- ✅ SPA 静态资源 Nginx 直出 200;`/api/apidirectory/categories/`、`/article/articles/`、`/swagger.json` 经反代 200
- ✅ aiohttp 外部 API(天气)经反代 200
- ✅ 异步 ORM 写路径:短链创建成功落库(PostgreSQL 66 张迁移表)
- ✅ Redis PONG(缓存/Session 层)
- ✅ WebSocket:Granian + Channels,JWT 认证后 accept 成功(匿名拒绝为原有业务设计,ChatConsumer 强制认证)
- ✅ 并发冒烟:80 并发 × 5 轮 = 400 请求,**400/400 成功,187.9 RPS**(穿透 Nginx → Granian → PostgreSQL 全链路)
- ✅ Windows 局域网访问 `http://192.168.5.7:18080/*` 全部 200
> 说明:`DJANGO_DEBUG` 插值默认 True(局域网无 TLS;False 会触发 `SECURE_SSL_REDIRECT` 301)。正式上 TLS 后设 `DJANGO_DEBUG=False`。
> 本地 Windows 宿主:2026-09-05 22:22 重启后 HypervisorPresent=False(BCD hypervisorlaunchtype 被关),Docker Desktop 不可用;管理员执行 `bcdedit /set "{current}" hypervisorlaunchtype auto` 重启后可恢复本地 Docker。
## 环境变量
见 `docker.env.example`。核心项:
| 变量 | 默认 | 说明 |
|---|---|---|
| `DB_ENGINE` | postgresql | django.db.backends.postgresql / mysql |
| `DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD` | postgres/5432/chunyu/chunyu | 数据库连接 |
| `REDIS_HOST/REDIS_PORT/REDIS_PASSWORD` | redis/6379 | Redis 连接 |
| `POSTGRES_PASSWORD` | chunyu_pg_pass | PostgreSQL root 密码 |
| `DJANGO_DEBUG` | False | 生产必须 False |
| `CORS_ALLOWED_ORIGINS` | localhost:8080/5173 | 前端来源 |
## 已知限制
1. **serializer 层仍有同步 ORM**(经 sync_to_async 兜底,功能正确但该段走线程池):`chat/serializers.py` ConversationSerializer 的 MethodField、`bug` 列表 get_images_count、若干 `serializer.create()`。后续可对 serializer 原生异步化。
2. **Celery worker 不在默认编排内**(views 已适配:同步调用失败自动降级 submit_task 异步队列)。如需启用:`docker compose up -d` 后手动 `docker compose run backend celery -A chunyu_project worker -l info`。
3. Channels ASGI Lifespan 在 Granian 下有 warning(`asginl` 接口可消除),不影响 WebSocket 功能。
4. 纯 CPU 视图(验证码/压缩/diff)保持同步,ADRF 自动线程池兜底。