Django 的异步视图和异步 ORM 怎么用?真的能提升性能吗?
简化版
Django 的异步支持是分阶段落地的:3.0 支持 ASGI、3.1 支持 async def 视图和中间件、4.1 才有了异步 ORM(aget/acreate/async for)、4.2+ 继续完善。写异步视图很简单——把 def view(request) 改成 async def view(request) 就行,Django 会自动识别;但能不能真正获得并发收益,取决于「整条链路上有没有同步阻塞点」。三个必须知道的现实:① 同步中间件会让异步视图退化——请求链路里只要有一个同步中间件,Django 就会在它两侧做 sync/async 的转换(用线程池),异步视图的收益被大幅抵消;② 异步 ORM 方法(4.1+)目前只是「在线程池里跑同步查询」的包装——await Model.objects.aget(...) 底层仍然走 sync_to_async,数据库驱动本身还是同步的,所以它解决的是「不阻塞事件循环」而不是「数据库访问变快了」;③ 在异步上下文里直接调用同步 ORM 会抛 SynchronousOnlyOperation——这是 Django 的保护机制,必须用 a 前缀方法或 sync_to_async 包一层。真正适合 Django 异步的场景是「视图里要等外部 IO」:调多个下游 HTTP API、访问 Redis、处理 WebSocket/SSE 长连接——这些场景下异步能让单个 worker 同时服务大量请求。而如果视图主要是「查几次数据库然后渲染模板」,异步带来的收益很有限,反而增加了复杂度和踩坑面。核心记忆:异步视图要配 ASGI 部署;SynchronousOnlyOperation 说明你在异步里调了同步 ORM;异步 ORM 目前是线程池包装,收益主要在外部 IO 而不是数据库。
详细版
Django 异步能力时间线:
| 版本 | 能力 |
|---|---|
| 3.0 | ASGI 支持(asgi.py),但视图仍是同步 |
| 3.1 | async def 视图、异步中间件、sync_to_async/async_to_sync |
| 4.0 | 异步测试客户端 AsyncClient |
| 4.1 | 异步 ORM 接口(aget/acreate/afirst/async for)、异步 Client/信号 |
| 4.2 | 异步 Model.arefresh_from_db() 等补全 |
| 5.0 | 异步认证(alogin/alogout)、异步 StreamingHttpResponse |
| 5.1+ | 继续补全(缓存的异步接口等) |
# ① ★异步视图:直接写 async def★
from django.http import JsonResponse
import httpx
async def dashboard(request):
async with httpx.AsyncClient() as client:
# ★并发调用多个下游(异步真正的收益点)★
a, b = await asyncio.gather(
client.get("https://api1/..."),
client.get("https://api2/..."))
return JsonResponse({"a": a.json(), "b": b.json()})
# ② ★异步 ORM(4.1+):a 前缀方法★
async def detail(request, pk):
obj = await Article.objects.aget(pk=pk) # ★aget★
count = await Article.objects.filter(published=True).acount()
await Article.objects.acreate(title="x")
async for a in Article.objects.filter(hot=True): # ★async for 迭代★
...
return JsonResponse({"title": obj.title, "count": count})
# 常用异步方法:aget / acreate / aupdate / adelete / afirst / alast
# acount / aexists / aaggregate / abulk_create
# aget_or_create / aupdate_or_create / arefresh_from_db(4.2+)
# ③ ★在异步里调同步 ORM → SynchronousOnlyOperation★
async def bad(request):
obj = Article.objects.get(pk=1) # ✗ ★SynchronousOnlyOperation★
return ...
# ✓ 两种解法
from asgiref.sync import sync_to_async
async def good(request):
obj = await Article.objects.aget(pk=1) # ✓ 用 a 方法
obj2 = await sync_to_async(Article.objects.get)(pk=2) # ✓ 包一层
# ★关联对象访问也是同步 ORM!★
author = await sync_to_async(lambda: obj.author.name)() # ★惰性关联要注意★
# ✓ 更好:查询时就 select_related
obj3 = await Article.objects.select_related("author").aget(pk=3)
name = obj3.author.name # ★已加载,安全★
# ④ ★同步视图里调异步代码★
from asgiref.sync import async_to_sync
def sync_view(request):
data = async_to_sync(fetch_all)(urls)
return JsonResponse(data)
# ⑤ 异步中间件(★3.1+★)
class MyMiddleware:
async_capable = True
sync_capable = False # ★只支持异步★
def __init__(self, get_response):
self.get_response = get_response
markcoroutinefunction(self) # 5.0+ 用这个标记
async def __call__(self, request):
response = await self.get_response(request)
return response
# ⑥ asgi.py + 部署
# asgi.py(django-admin startproject 自动生成)
application = get_asgi_application()
# 部署:uvicorn myproject.asgi:application
# gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker
⚠️ 三个必须澄清的认知:① 异步视图必须用 ASGI 服务器部署才有意义。如果仍然跑在 WSGI(
gunicorn myproject.wsgi)下,Django 会用async_to_sync把你的异步视图包成同步执行——不但没有并发收益,还多了一层事件循环的开销,比纯同步视图更慢。要用uvicorn/daphne/hypercorn,或gunicorn配UvicornWorker。② 异步 ORM 的a前缀方法目前是「线程池包装」而不是原生异步驱动:await Model.objects.aget(...)内部仍然通过sync_to_async在线程里执行同步查询。所以它的价值是**「不阻塞事件循环」(让同一个 worker 能同时处理其他请求),而不是「数据库访问本身变快了」——数据库的并发能力仍受连接数和线程池大小限制。真正的原生异步数据库支持还在 Django 的路线图上。③SynchronousOnlyOperation是保护而不是 bug:Django 检测到你在异步上下文里直接执行同步 ORM 就会抛这个异常,因为那会阻塞整个事件循环**。常见的隐蔽触发点是惰性关联访问——obj.author.name、obj.tags.all()、模板里的关联遍历看起来只是属性访问,实际上会触发一次同步查询,必须提前select_related/prefetch_related,或用sync_to_async包起来。
完整版教学
一、Django 异步支持的演进
★ 时间线(★答题时能说出版本节点很加分★):
★3.0(2019)★ ASGI 支持
- 项目生成 asgi.py,可以用 ASGI 服务器部署
- ★但视图、ORM、中间件仍然全是同步的★
- 价值:为后续铺路 + 支持 Channels 的 WebSocket
★3.1(2020)★ 异步视图 + 异步中间件
- 可以写 async def view(request)
- Django 自动识别并适配(★同步/异步可以混用★)
- 提供 sync_to_async / async_to_sync(来自 asgiref)
- ★但 ORM 仍是同步的★ → 视图里查库必须 sync_to_async
★4.1(2022)★ ★异步 ORM 接口★
- aget / acreate / afirst / acount / aexists / async for …
- 异步测试客户端、异步信号
- ★底层仍是线程池包装★
★4.2 / 5.0★ 继续补全
- arefresh_from_db、异步认证(alogin/alogout)、
异步 StreamingHttpResponse
★未来★:原生异步数据库驱动(★路线图上,尚未落地★)
★ 为什么演进这么慢:
Django 是★同步设计★的框架(2005 年诞生,早于 asyncio)
→ ORM、中间件、认证、模板、缓存全是同步 API
→ ★"函数着色"问题★:要异步化就得把整条链路都改
→ Django 选择了"渐进式":让同步和异步★可以混用★,
在边界处用 asgiref 自动转换
★ 代价:★转换本身有开销★,而且很容易在不经意间退化成同步
★ 核心机制:asgiref 的自动适配
Django 会检查每个组件是同步还是异步:
- 视图:asyncio.iscoroutinefunction(view)
- 中间件:async_capable / sync_capable 属性
→ ★不匹配时自动用 sync_to_async / async_to_sync 转换★
→ ★转换 = 起线程或起事件循环 = 有成本★
★ 一个关键推论:
★"能不能享受异步收益"取决于整条链路★
ASGI 服务器 → 中间件链 → 视图 → ORM/外部调用
★任何一环是同步的,那一段就要走线程池转换★
Django 的异步支持是分阶段渐进落地的:3.0 支持 ASGI 部署(但视图和 ORM 仍是同步)、3.1 支持 async def 视图和异步中间件(ORM 还是同步,查库要 sync_to_async)、4.1 才有异步 ORM 接口(aget/async for 等,底层仍是线程池包装)、4.2/5.0 继续补全异步认证和流式响应。演进慢的根本原因是 Django 是同步设计的框架(2005 年诞生,早于 asyncio),ORM、中间件、认证、模板、缓存全是同步 API,全面异步化会撞上「函数着色」问题——所以 Django 选择了渐进式路线:让同步和异步可以混用,在边界处用 asgiref 自动转换。核心机制是 Django 会检查每个组件是同步还是异步(视图看是不是协程函数、中间件看 async_capable/sync_capable 属性),不匹配时自动转换——而转换本身是有成本的(起线程或起事件循环)。由此得出关键推论:能不能享受异步收益取决于整条链路,任何一环是同步的,那一段就要走线程池。
二、异步视图怎么写
★ 最简单的形式:
async def my_view(request):
return HttpResponse("hello")
→ Django 自动识别(★不需要任何装饰器或配置★)
★ 类视图:
class MyView(View):
async def get(self, request): # ★4.1+ 支持异步类视图方法★
...
★ 注意:★不能同时定义 get 和 async def get★;
同一个类里的所有 handler 要么全同步、要么全异步
★ ★真正的收益场景:并发的外部 IO★
✗ 同步版本(串行):
def dashboard(request):
a = requests.get(url1).json() # 200ms
b = requests.get(url2).json() # 200ms
c = requests.get(url3).json() # 200ms
return JsonResponse({...}) # ★总计 600ms★
✓ 异步版本(并发):
async def dashboard(request):
async with httpx.AsyncClient() as client:
a, b, c = await asyncio.gather(
client.get(url1), client.get(url2), client.get(url3))
return JsonResponse({...}) # ★总计 ~200ms★
★ 这才是异步视图最有说服力的用例:★聚合多个下游★
★ 其他适合的场景:
✓ ★长连接★:SSE(Server-Sent Events)、WebSocket(配 Channels)
✓ ★慢下游★:调用第三方 API、AI 模型推理接口
✓ ★高并发但每个请求很轻★:健康检查、简单代理转发
✗ ★纯数据库 CRUD★ → 收益有限(见后文)
✗ ★CPU 密集★ → 异步完全帮不上(★还会阻塞事件循环★)
★ 混用同步/异步的规则:
同步视图 + 同步中间件 → 全同步(WSGI 或 ASGI 都能跑)
异步视图 + 异步中间件 → ★全异步(收益最大)★
异步视图 + ★同步中间件★ → ★在中间件处转换(收益打折)★
同步视图 + 异步中间件 → 同样要转换
★ 在异步视图里做 CPU 密集任务:
✗ async def view(request):
result = heavy_compute() # ★阻塞整个事件循环★
✓ result = await asyncio.to_thread(heavy_compute) # IO 型/会释放 GIL 的
✓ 真正的 CPU 密集 → ★交给 Celery 等任务队列★
★ 流式响应(5.0+ 支持异步):
async def stream(request):
async def gen():
for i in range(100):
yield f"data: {i}\n\n".encode()
await asyncio.sleep(1)
return StreamingHttpResponse(gen(), content_type="text/event-stream")
★ 这是 SSE 推送的标准做法;★必须用 ASGI 部署★
写异步视图很简单——把 def 改成 async def,Django 会自动识别(类视图 4.1+ 也支持,但同一个类里的 handler 要么全同步要么全异步)。真正有说服力的收益场景是「并发的外部 IO」:同步版本串行调用三个下游 API 要 600ms,异步版本用 asyncio.gather 并发只要约 200ms——这是异步视图最典型的用例。其他适合的场景是长连接(SSE、WebSocket)和慢下游(第三方 API、AI 推理)。不适合的是纯数据库 CRUD(收益有限,见后文)和 CPU 密集任务(异步完全帮不上,反而会阻塞整个事件循环——正确做法是 asyncio.to_thread 或干脆交给 Celery)。混用规则要记住:异步视图配同步中间件会在中间件处发生转换、收益打折。
三、异步 ORM 的真相
★ 4.1+ 提供的 a 前缀方法:
await Model.objects.aget(pk=1)
await Model.objects.acreate(...)
await Model.objects.filter(...).acount() / .aexists() / .afirst() / .alast()
await Model.objects.aupdate(...) / .adelete()
await Model.objects.aget_or_create(...) / .aupdate_or_create(...)
await Model.objects.abulk_create([...])
await Model.objects.aaggregate(Sum("x"))
async for obj in Model.objects.filter(...): # ★异步迭代★
await obj.asave() / await obj.adelete() / await obj.arefresh_from_db()
★ ★真相:它们目前是"线程池包装"★
Django 的实现大致是:
async def aget(self, *args, **kwargs):
return await sync_to_async(self.get)(*args, **kwargs)
→ ★数据库驱动(psycopg2 / mysqlclient)本身是同步的★
→ 查询实际在★线程池★里执行
★ 这意味着什么:
✓ ★不阻塞事件循环★——同一个 worker 能继续处理其他请求
✗ ★数据库访问本身没有变快★
✗ ★并发度受线程池大小限制★(不是无限的)
✗ ★连接数仍然是瓶颈★(每个并发查询占一个连接)
★ 那还有意义吗?★有,但要理解边界★:
场景:一个视图查 1 次库(20ms)+ 调 1 个下游 API(300ms)
同步:整个 worker 被占用 320ms
异步:查库时线程池占用 20ms、调 API 时事件循环空闲
→ ★同一个 worker 可以同时处理几十个这样的请求★
→ ★收益来自"外部 IO 期间不占 worker",而不是数据库本身★
场景:一个视图查 10 次库,没有外部调用
→ ★异步收益很小★(10 次都在线程池里,还多了转换开销)
→ ★不如优化查询本身(select_related、缓存)★
★ ★SynchronousOnlyOperation:最常遇到的报错★
async def view(request):
obj = Article.objects.get(pk=1) # ✗ 抛 SynchronousOnlyOperation
★ Django 的保护机制:检测到异步上下文里执行同步 ORM 就报错
(否则会★阻塞整个事件循环★)
★ 隐蔽的触发点(★最容易踩★):
① ★惰性关联访问★
obj = await Article.objects.aget(pk=1)
print(obj.author.name) # ✗ ★这是一次同步查询!★
✓ 提前 select_related:
obj = await Article.objects.select_related("author").aget(pk=1)
② ★反向关系 / 多对多★
async for tag in obj.tags.all(): # ✓ 4.1+ 支持异步迭代
tags = await sync_to_async(list)(obj.tags.all()) # 或包一层
③ ★模板渲染里的关联访问★
→ 模板里 {{ obj.author.name }} 会触发查询
→ ★用 render 渲染模板本身也是同步的★ → 整体要 sync_to_async
④ ★信号处理器里的 ORM 操作★
⑤ 第三方包内部的同步查询
★ 事务的限制(★重要★):
★transaction.atomic() 不支持异步上下文★
✗ async def view(request):
async with transaction.atomic(): # ✗ 不支持
✓ 把整个事务块包进 sync_to_async:
@sync_to_async
def do_transaction():
with transaction.atomic():
...
await do_transaction()
★ 原因:事务是绑定在★数据库连接★上的,而连接是 thread-local 的
→ 异步下"哪个线程执行"不确定 → 事务边界无法保证
★ 这也是 sync_to_async 默认 thread_sensitive=True 的原因
异步 ORM 的真相必须讲清楚:4.1+ 的 a 前缀方法目前是「线程池包装」——底层仍通过 sync_to_async 在线程里执行同步查询,因为 psycopg2/mysqlclient 这些驱动本身是同步的。所以它的价值是**「不阻塞事件循环」(同一个 worker 能继续处理其他请求),而不是「数据库访问变快」——并发度仍受线程池大小和数据库连接数限制。收益的边界很清晰:视图里「查 1 次库 + 调 1 个 300ms 的下游」时异步收益明显(外部 IO 期间 worker 空闲,能同时处理几十个请求);而「查 10 次库、没有外部调用」时异步收益很小,不如优化查询本身**。最常遇到的报错是 SynchronousOnlyOperation,它的隐蔽触发点是惰性关联访问(obj.author.name 看似属性访问实则一次查询,要提前 select_related)、模板渲染、信号处理器。还有个重要限制:transaction.atomic() 不支持异步上下文(事务绑定在 thread-local 的数据库连接上),必须把整个事务块包进 sync_to_async。
四、中间件与请求链路
★ 中间件的三种能力标记:
class MyMiddleware:
sync_capable = True # ★默认 True★
async_capable = True # ★默认 False(老式中间件)★
Django 根据这两个属性决定怎么调用:
只支持同步 → ★在异步链路里用 sync_to_async 包装★
只支持异步 → 在同步链路里用 async_to_sync 包装
两者都支持 → ★直接用,无转换开销★
★ ★为什么"一个同步中间件毁掉整条异步链路"★:
请求链路:
ASGI → 中间件1(异步) → 中间件2(★同步★) → 中间件3(异步) → 异步视图
↓
★这里要 sync_to_async★
→ 后面的一切都在★线程池的那个线程★里执行
→ 异步视图也被 async_to_sync 包回去执行
→ ★收益基本抵消,还多了两次转换开销★
✓ 检查方式:Django 启动时会记录中间件的适配情况
★DEBUG 下可以看到 "Asynchronous handler adapted for middleware"★
★ Django 内置中间件的支持情况:
★大部分内置中间件已经同时支持同步和异步★
(SecurityMiddleware、SessionMiddleware、CommonMiddleware、
CsrfViewMiddleware、AuthenticationMiddleware…)
★ 但第三方中间件★很多只支持同步★
→ 装第三方包前检查它有没有 async_capable = True
★ 写一个同时支持两种模式的中间件:
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
class HybridMiddleware:
sync_capable = True
async_capable = True
def __init__(self, get_response):
self.get_response = get_response
self.async_mode = iscoroutinefunction(get_response)
if self.async_mode:
markcoroutinefunction(self) # ★5.0+ 的标记方式★
def __call__(self, request):
if self.async_mode:
return self.__acall__(request)
# 同步逻辑
response = self.get_response(request)
return response
async def __acall__(self, request):
response = await self.get_response(request)
return response
★ 认证与 session:
request.user 的获取是★惰性★的
→ 在异步视图里访问 request.user ★可能触发同步查询★
✓ 5.0+:await request.auser() # ★异步获取用户★
✓ 老版本:user = await sync_to_async(lambda: request.user)()
★ 其他同步组件:
★模板渲染★:render() 是同步的 → 返回 HTML 的视图用异步收益不大
★缓存★:Django 的 cache API 在 5.1 前主要是同步(部分后端有异步接口)
★邮件★:send_mail 是同步的
★文件存储★:Storage API 同步
→ ★这些都要 sync_to_async 包装★
中间件是异步链路里最容易「破功」的地方:Django 根据中间件的 sync_capable/async_capable 属性决定怎么调用,只支持同步的中间件在异步链路里会被 sync_to_async 包装——后面的一切(包括你的异步视图)都会在线程池的那个线程里执行,收益基本抵消还多了两次转换开销。好消息是 Django 的内置中间件大部分已经同时支持两种模式,但很多第三方中间件只支持同步——装包前值得检查有没有 async_capable = True。写自己的中间件时可以做成混合模式(用 iscoroutinefunction 判断,5.0+ 用 markcoroutinefunction 标记)。还要注意几个仍然是同步的组件:request.user 的获取是惰性的(异步视图里访问可能触发同步查询,5.0+ 可以用 await request.auser())、模板渲染是同步的(所以返回 HTML 的视图用异步收益不大)、缓存/邮件/文件存储 API 也大多是同步的。
五、性能:异步在 Django 里到底值不值
★ 先算清楚一次请求的时间构成:
典型的 Django 视图(返回 JSON):
中间件处理 1ms
URL 解析 0.1ms
★数据库查询★ 20ms(3 次查询)
业务逻辑 2ms
序列化 2ms
★外部 API 调用★ 0ms 或 300ms ← ★关键变量★
→ 没有外部调用时:★总耗时 ~25ms,其中数据库占 80%★
→ 有外部调用时:★总耗时 ~325ms,其中外部调用占 92%★
★ 两种场景的收益对比:
场景 A:★纯数据库 CRUD(无外部调用)★
同步 + WSGI:每个 worker 处理一个请求,25ms 释放
异步 + ASGI:查库仍在线程池 → ★并发度受线程池限制★
→ ★收益很小,甚至因为转换开销而略慢★
✓ 正确的优化方向:★减少查询次数、加索引、加缓存、加 worker★
场景 B:★需要调用慢下游(300ms)★
同步:一个 worker 被占用 325ms → ★16 个 worker 只能扛 ~50 QPS★
异步:事件循环在等待期间处理其他请求
→ ★同一进程可以同时"在飞"几百个请求★
→ ★收益巨大★
场景 C:★长连接(SSE / WebSocket)★
同步:★每个连接占一个 worker★ → 1000 个连接需要 1000 个 worker(不现实)
异步:★一个进程轻松扛几千连接★
→ ★这是 ASGI 的杀手级场景★
★ ★一个务实的结论★:
★Django 异步的价值主要在"等待外部 IO"和"长连接",
而不是"让数据库变快"。★
→ 如果你的瓶颈是数据库,★先优化查询和加缓存★,
换异步不会有帮助(反而增加复杂度)
★ 部署方式的选择:
┌────────────────────┬────────────────────────────────────┐
│ 纯同步项目 │ ★WSGI(gunicorn + gthread/sync)★ │
│ 有异步视图/长连接 │ ★ASGI(uvicorn / daphne)★ │
│ 混合 │ ASGI(★同步视图会在线程池执行★) │
└────────────────────┴────────────────────────────────────┘
★ ASGI 下的同步视图:Django 会用 sync_to_async 在线程池里跑
→ ★线程池大小很关键★(默认由 asgiref 决定,可通过环境变量调整)
★ 数据库连接的注意事项:
★Django 的数据库连接是 thread-local 的★
→ 异步下,查询在线程池的不同线程里执行 → ★每个线程一个连接★
→ ★CONN_MAX_AGE 的持久连接在异步下行为更复杂★
→ 并发高时容易★打满数据库连接数★
✓ 用连接池(django-db-connection-pool / pgbouncer)
✓ ★sync_to_async 默认 thread_sensitive=True★:
所有调用在同一个线程执行 → ★保证连接和事务的一致性★
但也意味着★数据库操作实际是串行的★
★ 什么时候用 Celery 而不是异步视图:
✓ ★耗时超过几秒的任务★(发邮件、生成报表、调用 AI)
✓ 需要重试、限流、优先级
✓ CPU 密集
→ ★异步视图解决的是"等待",任务队列解决的是"耗时"★
要算清楚一次请求的时间构成才能判断异步值不值。场景 A(纯数据库 CRUD):耗时的 80% 是数据库查询,而异步 ORM 仍在线程池里跑——收益很小甚至因转换开销略慢,正确的优化方向是减少查询、加索引、加缓存、加 worker。场景 B(调用 300ms 的慢下游):同步时一个 worker 被占用 325ms、16 个 worker 只能扛约 50 QPS,异步则同一进程可以同时「在飞」几百个请求——收益巨大。场景 C(SSE/WebSocket 长连接):同步下每个连接占一个 worker 完全不现实,异步一个进程轻松扛几千连接——这是 ASGI 的杀手级场景。所以务实的结论是:Django 异步的价值主要在「等待外部 IO」和「长连接」,而不是「让数据库变快」。还有两个部署要点:ASGI 下的同步视图会在线程池里跑(线程池大小很关键)、Django 的数据库连接是 thread-local 的(异步下每个线程一个连接,高并发容易打满连接数,sync_to_async 默认 thread_sensitive=True 保证一致性但也让数据库操作实际串行)。
六、实践建议与常见坑
★ 决策流程:
┌──────────────────────────────────────────────────────┐
│ 视图里有慢的外部 IO(调 API、AI 推理)? │
│ → ★是:用异步视图 + httpx.AsyncClient + gather★ │
│ 需要长连接(SSE / WebSocket)? │
│ → ★是:ASGI + 异步视图 / Django Channels★ │
│ 主要是数据库 CRUD + 模板渲染? │
│ → ★否:保持同步,去优化查询和缓存★ │
│ 耗时任务(几秒以上)? │
│ → ★用 Celery,不是异步视图★ │
└──────────────────────────────────────────────────────┘
★ 常见坑速查:
① ★SynchronousOnlyOperation★
→ 异步里调了同步 ORM;注意★惰性关联访问★是隐藏的查询
✓ 用 a 方法 / 提前 select_related / sync_to_async 包一层
② ★用 WSGI 部署异步视图★
→ 没有并发收益,还多一层转换 → ★必须用 ASGI★
③ ★同步中间件拖累整条链路★
→ 检查第三方中间件的 async_capable
④ ★transaction.atomic() 不能用在异步上下文★
→ 整个事务块包进 sync_to_async
⑤ ★在异步视图里跑 CPU 密集任务★
→ 阻塞事件循环 → 用 to_thread 或 Celery
⑥ ★用同步 HTTP 库(requests)★
→ ★直接阻塞事件循环★ → 换 httpx.AsyncClient / aiohttp
⑦ ★数据库连接打满★
→ 异步高并发下每个线程一个连接 → 用连接池 / 限并发
⑧ ★测试要用 AsyncClient★
→ from django.test import AsyncClient
response = await client.get("/")
★ 一个完整的异步视图示例(★可直接抄★):
import asyncio, httpx
from django.http import JsonResponse
from asgiref.sync import sync_to_async
async def order_detail(request, pk):
# ① ★ORM 用 a 方法 + 提前 select_related★
order = await (Order.objects
.select_related("user", "address")
.aget(pk=pk))
# ② ★并发调用外部服务★
async with httpx.AsyncClient(timeout=5) as client:
logistics, risk = await asyncio.gather(
client.get(f"https://logistics/{order.no}"),
client.get(f"https://risk/{order.user_id}"),
return_exceptions=True) # ★部分失败不影响整体★
# ③ ★需要事务的写操作包进 sync_to_async★
@sync_to_async
def mark_viewed():
with transaction.atomic():
Order.objects.filter(pk=pk).update(view_count=F("view_count") + 1)
await mark_viewed()
return JsonResponse({
"id": order.pk,
"user": order.user.name, # ★已 select_related,安全★
"logistics": _safe(logistics),
"risk": _safe(risk),
})
★ 迁移建议(★渐进式★):
① ★先把部署换成 ASGI★(同步视图照常工作)
② 挑一两个"调用外部 API 最多"的视图改成异步,压测对比
③ 确认中间件链路都支持异步
④ 逐步扩大范围;★不要为了异步而异步★
★ 一句话总结:
★"Django 的异步不是'让 Django 变快'的开关,
而是'让单个 worker 能在等待外部 IO 时服务更多请求'的能力。
数据库是瓶颈时它帮不上忙;外部调用和长连接是瓶颈时它价值巨大。"★
决策流程很清晰:有慢的外部 IO 就用异步视图(配 httpx.AsyncClient + gather)、需要长连接就上 ASGI、主要是数据库 CRUD 就保持同步去优化查询、耗时任务用 Celery。八个常见坑里最容易踩的是:SynchronousOnlyOperation(注意惰性关联访问是隐藏的查询)、用 WSGI 部署异步视图(没有收益还多一层转换)、同步中间件拖累整条链路、在异步视图里用 requests(直接阻塞事件循环,必须换 httpx.AsyncClient)、以及数据库连接打满。那个完整示例展示了正确写法:ORM 用 a 方法并提前 select_related、外部调用用 gather 并发且 return_exceptions=True、需要事务的写操作整块包进 sync_to_async。迁移建议是渐进式:先把部署换成 ASGI(同步视图照常工作)→ 挑一两个外部调用最多的视图改造并压测对比 → 确认中间件支持异步 → 逐步扩大,不要为了异步而异步。
记忆钩子:「Django 的异步是★分阶段落地★的:★3.0 支持 ASGI、3.1 支持 async def 视图和异步中间件、4.1 才有异步 ORM(aget/acreate/async for)、5.0 有异步认证★;演进慢是因为 Django 是同步设计的框架(早于 asyncio),选择了『同步异步可混用、在边界用 asgiref 自动转换』的渐进路线。★三个必须澄清的认知★:①★异步视图必须用 ASGI 部署★(uvicorn/daphne),跑在 WSGI 下 Django 会用 async_to_sync 包成同步执行——★没有并发收益还更慢★;②★异步 ORM 的 a 前缀方法目前只是『线程池包装』★(底层仍 sync_to_async,因为 psycopg2 等驱动是同步的)→ 它解决的是★『不阻塞事件循环』而不是『数据库变快』★,并发度仍受线程池和连接数限制;③★SynchronousOnlyOperation 是保护不是 bug★——最隐蔽的触发点是★惰性关联访问★(obj.author.name 看着像属性访问、实则一次同步查询),要提前 select_related 或用 sync_to_async 包。★收益判断看时间构成★:纯数据库 CRUD(查询占 80%)→ ★异步收益很小,该去优化查询和缓存★;调用 300ms 的慢下游 → ★收益巨大★(同步下 16 个 worker 只扛 50 QPS,异步能同时在飞几百个请求);★SSE/WebSocket 长连接是 ASGI 的杀手级场景★(同步下每连接占一个 worker 完全不现实)。其他关键坑:★一个只支持同步的中间件会让整条异步链路退化★(后面全在线程池那个线程里跑,检查第三方包的 async_capable)、★transaction.atomic() 不支持异步上下文★(事务绑在 thread-local 的连接上,要整块包进 sync_to_async)、★异步视图里用 requests 会直接阻塞事件循环★(换 httpx.AsyncClient)、★CPU 密集要用 to_thread 或 Celery★、★request.user 是惰性的★(5.0+ 用 await request.auser())、测试用 ★AsyncClient★。一句话:★Django 异步不是『让 Django 变快』的开关,而是『让单个 worker 在等待外部 IO 时服务更多请求』的能力★。」
七、常见误区与追问
- 误区:把视图改成
async def就能提升性能。 有三个前提缺一不可。① 必须用 ASGI 服务器部署——仍跑在 WSGI 下时,Django 会用async_to_sync把异步视图包成同步执行,不但没有并发收益,还多了一层事件循环的创建开销,比纯同步视图更慢。② 视图里必须真的有「等待」——如果只是查几次数据库然后渲染模板,异步几乎没有收益(数据库查询在线程池里,模板渲染是同步的)。③ 整条链路都要支持异步——一个只支持同步的中间件就会让后面的一切退回线程池执行。所以正确的做法是先判断瓶颈在哪:瓶颈是外部 API 调用或长连接时异步价值巨大,瓶颈是数据库时应该去优化查询、加索引、加缓存,而不是换异步。 - 误区:Django 4.1 有了异步 ORM,数据库访问就是真异步了。 目前的
a前缀方法(aget/acreate/acount等)本质是sync_to_async的包装——因为psycopg2、mysqlclient这些数据库驱动本身是同步的,查询实际在线程池的线程里执行。这带来的差别很实际:它确实让你的事件循环不被阻塞(同一个 worker 可以在等待查询时处理其他请求),但数据库访问本身没有变快,而且并发度受限于线程池大小和数据库连接数——不是「异步了就能无限并发查库」。此外由于sync_to_async默认thread_sensitive=True(为了保证连接和事务的一致性),同一个请求内的多次数据库操作实际上是串行的。原生异步数据库驱动的支持还在 Django 的路线图上。 - 误区:
SynchronousOnlyOperation只要把查询改成aget就解决了。 改查询只解决了显式的那一处,真正隐蔽的是惰性关联访问:obj.author.name、obj.tags.all()、obj.comment_set.count()这些看起来只是属性访问,实际上每一次都会触发一次同步数据库查询——在异步上下文里就会抛异常。同类的隐藏触发点还有:模板渲染中的关联遍历({{ obj.author.name }},而且render()本身也是同步的)、信号处理器里的 ORM 操作、request.user的惰性加载(5.0+ 可用await request.auser())、以及第三方包内部的同步查询。正确的应对是提前用select_related/prefetch_related把需要的关联一次查出来(这样后续访问是内存操作),或者把整段涉及同步 ORM 的逻辑用sync_to_async包成一个函数。 - 误区:在异步视图里用
requests库调用外部 API 也没问题,反正视图是异步的。requests是同步阻塞的——调用它时整个事件循环会被卡住,那一刻这个进程里所有其他请求的协程都停止推进:其他用户的请求不被处理、asyncio.sleep到期的任务不被唤醒、心跳和超时判断全部失真。一个 300ms 的requests.get()就意味着整个 worker 有 300ms 完全无响应——这比同步视图更糟(同步视图至少有多个 worker 分担)。必须换成异步 HTTP 客户端(httpx.AsyncClient或aiohttp),并且复用客户端实例(每次新建会重新握手,失去连接池的意义)。同理,异步视图里也不能用同步的 Redis 客户端、同步的文件读写、time.sleep()——这些都要换异步版本或用asyncio.to_thread包装。 - 误区:异步视图里可以像同步视图一样用
with transaction.atomic():。transaction.atomic()不支持异步上下文。根本原因是 Django 的数据库连接是 thread-local 的,而事务状态绑定在连接上——异步执行时,同一个协程的不同await之间可能落在线程池的不同线程上,事务的边界就无法保证(可能出现「BEGIN 在一个线程、COMMIT 在另一个线程」的情况)。正确做法是把整个需要事务的逻辑写成一个同步函数,再用sync_to_async包起来调用——这样整块逻辑在同一个线程里完整执行(thread_sensitive=True保证了这一点)。这也提醒我们:涉及事务的写操作在异步视图里最好整体下沉到一个同步函数中,而不是零散地用a方法逐条写。 - 追问:为什么一个同步中间件会让整条异步链路失效? 因为 Django 的请求处理是层层包裹的:ASGI handler → 中间件 1 → 中间件 2 → … → 视图。Django 在启动时会根据每个中间件的
sync_capable/async_capable属性决定如何衔接:遇到只支持同步的中间件,就用sync_to_async把「它自己和它内层的所有东西」包起来——于是从这个中间件往内,包括你的异步视图,都会在线程池的某个线程里同步执行(异步视图会被async_to_sync转回同步)。结果是:并发收益基本消失,还额外付出了两次转换的开销(起线程、跨线程调度)。Django 的内置中间件大部分已经同时支持两种模式,问题通常出在第三方包上——安装前值得检查源码里有没有async_capable = True。DEBUG 模式下 Django 也会记录中间件的适配情况。 - 追问:Django 异步和 Celery 该怎么分工? 两者解决的是不同的问题:异步视图解决「等待」——请求需要等外部 IO(调 API、查缓存、等下游)时,让 worker 在等待期间去服务其他请求,但请求本身仍然要在超时时间内返回响应;Celery 解决「耗时」——任务本身要跑几秒到几十分钟(发邮件、生成报表、导出大文件、调用 AI 模型、图片处理),这类任务根本不应该占用 HTTP 请求周期,而应该「提交任务 → 立即返回任务 ID → 前端轮询或推送结果」。判断标准很简单:如果这件事必须在响应返回前完成,且主要时间花在等待外部 IO 上 → 异步视图;如果它可以异步完成、或者耗时长到用户不该等 → Celery。另外 CPU 密集型任务两者都不适合(异步会阻塞事件循环、Celery worker 也会被占满),需要专门的进程池或独立的计算服务。实践中两者常常配合:异步视图快速接收请求并提交 Celery 任务,然后用 SSE/WebSocket 推送进度。
- 追问:一个现有的同步 Django 项目,该怎么渐进式引入异步? 分五步走。① 先把部署换成 ASGI(
uvicorn myproject.asgi:application或gunicorn -k UvicornWorker)——同步视图在 ASGI 下照常工作(Django 会在线程池里跑它们),这一步风险很低但要关注线程池大小(默认值可能不适合你的并发量)并压测确认没有退化。② 挑一两个「外部调用最多」的视图改成异步——通常是聚合页、仪表盘、需要调多个下游的接口,改造后用asyncio.gather并发调用,压测对比 QPS 和 P99,用数据证明收益。③ 检查中间件链路——确认自己写的和第三方的中间件都支持异步,否则第 ② 步的收益会被抵消。④ 逐步扩大范围,同时把同步 HTTP 客户端换成httpx.AsyncClient、注意select_related避免SynchronousOnlyOperation。⑤ 长连接需求(SSE/WebSocket)单独评估,可能需要引入 Django Channels。最重要的原则是不要为了异步而异步——每一步都要有压测数据支撑,收益不明显就停下来。
八、加强记忆
Django 的异步支持是分阶段落地的:3.0 支持 ASGI、3.1 支持 async def 视图和异步中间件、4.1 才有异步 ORM(aget/acreate/async for)、5.0 有异步认证和流式响应;演进慢是因为 Django 是同步设计的框架(诞生早于 asyncio),它选择了「同步异步可混用、在边界用 asgiref 自动转换」的渐进路线。三个必须澄清的认知:① 异步视图必须用 ASGI 部署(uvicorn/daphne),跑在 WSGI 下 Django 会用 async_to_sync 包成同步执行——没有并发收益还更慢;② 异步 ORM 的 a 前缀方法目前只是「线程池包装」(底层仍是 sync_to_async,因为 psycopg2 等驱动是同步的)→ 它解决的是**「不阻塞事件循环」而不是「数据库变快」,并发度仍受线程池和连接数限制;③ SynchronousOnlyOperation 是保护而不是 bug——最隐蔽的触发点是惰性关联访问**(obj.author.name 看着像属性访问、实则一次同步查询),要提前 select_related 或用 sync_to_async 包起来。收益判断要看时间构成:纯数据库 CRUD(查询占 80%)→ 异步收益很小,应该去优化查询和缓存;调用 300ms 的慢下游 → 收益巨大(同步下 16 个 worker 只能扛约 50 QPS,异步能同时在飞几百个请求);SSE/WebSocket 长连接是 ASGI 的杀手级场景(同步下每个连接占一个 worker 完全不现实)。其他关键坑:一个只支持同步的中间件会让整条异步链路退化(它和内层的一切都在线程池的那个线程里跑,要检查第三方包的 async_capable)、transaction.atomic() 不支持异步上下文(事务绑定在 thread-local 的连接上,要整块包进 sync_to_async)、异步视图里用 requests 会直接阻塞事件循环(换 httpx.AsyncClient 并复用实例)、CPU 密集要用 to_thread 或 Celery(异步视图解决「等待」、Celery 解决「耗时」)、request.user 是惰性的(5.0+ 用 await request.auser())、测试要用 AsyncClient。一句话:Django 异步不是「让 Django 变快」的开关,而是「让单个 worker 在等待外部 IO 时服务更多请求」的能力。