← 返回题目列表

Django 的异步视图和异步 ORM 怎么用?真的能提升性能吗?

困难 第 20 / 27 题 更新于 2026/08/01
Django异步ASGI异步ORMsync_to_async

简化版

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.0ASGI 支持asgi.py),但视图仍是同步
3.1async 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,或 gunicornUvicornWorker。② 异步 ORM 的 a 前缀方法目前是「线程池包装」而不是原生异步驱动await Model.objects.aget(...) 内部仍然通过 sync_to_async 在线程里执行同步查询。所以它的价值是**「不阻塞事件循环」(让同一个 worker 能同时处理其他请求),而不是「数据库访问本身变快了」——数据库的并发能力仍受连接数和线程池大小限制。真正的原生异步数据库支持还在 Django 的路线图上。③ SynchronousOnlyOperation 是保护而不是 bug:Django 检测到你在异步上下文里直接执行同步 ORM 就会抛这个异常,因为那会阻塞整个事件循环**。常见的隐蔽触发点是惰性关联访问——obj.author.nameobj.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 的包装——因为 psycopg2mysqlclient 这些数据库驱动本身是同步的,查询实际在线程池的线程里执行。这带来的差别很实际:它确实让你的事件循环不被阻塞(同一个 worker 可以在等待查询时处理其他请求),但数据库访问本身没有变快,而且并发度受限于线程池大小和数据库连接数——不是「异步了就能无限并发查库」。此外由于 sync_to_async 默认 thread_sensitive=True(为了保证连接和事务的一致性),同一个请求内的多次数据库操作实际上是串行的。原生异步数据库驱动的支持还在 Django 的路线图上。
  • 误区:SynchronousOnlyOperation 只要把查询改成 aget 就解决了。 改查询只解决了显式的那一处,真正隐蔽的是惰性关联访问obj.author.nameobj.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.AsyncClientaiohttp),并且复用客户端实例(每次新建会重新握手,失去连接池的意义)。同理,异步视图里也不能用同步的 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 项目,该怎么渐进式引入异步? 分五步走。① 先把部署换成 ASGIuvicorn myproject.asgi:applicationgunicorn -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 时服务更多请求」的能力