Django 现在支持异步视图,很多 Python 后端团队一升级到 ASGI,就想把接口全改成 async def。我支持这个方向,但不支持一把梭。最常见的事故是:异步视图里直接调用同步 ORM,开发环境偶尔报 SynchronousOnlyOperation,线上则表现成 P99 飙升、连接池吃紧、事件循环被拖住。
这篇文章不讲 Django 入门,也不搬官方 API 列表。我用一个订单详情页的排障过程说明:什么时候 async view 真有收益,ORM 怎么隔离,sync_to_async 怎么用,事务边界怎么收口,以及上线前我会检查哪些指标。
业务场景:异步详情页越改越慢
假设订单详情页需要同时取订单、调用会员服务、调用风控服务。团队把视图改成异步以后,外部 HTTP 调用确实可以并发等待,但代码里还保留了同步 ORM。
# views.py
async def order_detail(request, order_id: int):
order = Order.objects.select_related("user").get(pk=order_id)
risk = await risk_client.get_score(order.user_id)
member = await member_client.get_profile(order.user_id)
return JsonResponse({"order": order.no, "risk": risk, "member": member})
这段代码的问题不是“语法不 async”,而是边界混乱:异步视图运行在事件循环里,同步 ORM 访问会碰到 Django 的异步安全保护;即便绕过去,也可能把线程切换、连接复用和事务行为搞得很难预测。
先判断 async view 是否值得
我不会为了“看起来现代”改 async。Django async view 适合 I/O 等待多、可并发发起外部调用的接口,比如聚合多个 HTTP 服务、WebSocket、长轮询、轻量异步缓存访问。如果接口主要是 ORM 查询和模板渲染,同步视图反而更简单稳定。
上线前我会把接口分成三类:纯同步 ORM 密集型继续同步;外部 I/O 密集型考虑 async;混合型则把同步 ORM 封装到边界函数里,不让业务代码到处直接调用。
方案一:能用异步 ORM 就直接 await
Django 已经提供了一批异步 QuerySet 方法,比如 aget、acreate、aupdate 等。简单查询可以直接用异步 ORM 表达,代码边界最干净。
async def order_detail(request, order_id: int):
order = await Order.objects.select_related("user").aget(pk=order_id)
risk, member = await asyncio.gather(
risk_client.get_score(order.user_id),
member_client.get_profile(order.user_id),
)
return JsonResponse({"order": order.no, "risk": risk, "member": member})
这里的收益来自两个地方:ORM 调用遵守异步边界,两个外部服务可以并发等待。注意,asyncio.gather 不是越多越好,下游服务和连接池都有容量,生产里仍然要配超时、限流和熔断策略。
方案二:复杂 ORM 放进同步函数,再用 sync_to_async 包起来
现实项目里,很多查询不是一行 aget 能解决。比如带事务、复杂预取、历史库兼容、老代码复用。这个时候我更愿意把 ORM 逻辑封装成同步函数,再在 async view 里用 sync_to_async 调用。
from asgiref.sync import sync_to_async
def load_order_snapshot(order_id: int) -> dict:
order = (
Order.objects
.select_related("user")
.prefetch_related("items")
.get(pk=order_id)
)
return {"no": order.no, "user_id": order.user_id, "item_count": len(order.items.all())}
async def order_detail(request, order_id: int):
snapshot = await sync_to_async(load_order_snapshot, thread_sensitive=True)(order_id)
risk = await risk_client.get_score(snapshot["user_id"])
return JsonResponse({"order": snapshot, "risk": risk})
我喜欢这种写法,因为它把“同步世界”和“异步世界”分得很清楚。视图负责异步编排,函数负责 ORM 读取。出了问题也容易定位:到底是数据库慢,还是外部服务慢。
事务别跨过 await
我见过最危险的写法,是在事务里混入外部异步调用。事务打开后等待下游 HTTP,数据库连接被占住,锁时间变长,一旦下游抖动,数据库也跟着遭殃。
# 不推荐:事务边界里等待外部服务
async def pay_callback(request):
with transaction.atomic():
order = Order.objects.get(no=request.POST["order_no"])
result = await payment_client.confirm(order.no)
order.status = result.status
order.save()
更稳的做法是把事务内的数据库更新封装到同步函数,外部调用放在事务外。需要强一致时,用状态机、幂等键和补偿任务来兜底,不要用一个跨网络的长事务硬扛。
def mark_paid(order_no: str, status: str) -> None:
with transaction.atomic():
order = Order.objects.select_for_update().get(no=order_no)
order.status = status
order.save(update_fields=["status"])
async def pay_callback(request):
result = await payment_client.confirm(request.POST["order_no"])
await sync_to_async(mark_paid, thread_sensitive=True)(request.POST["order_no"], result.status)
return JsonResponse({"ok": True})
诊断步骤:先找同步边界泄漏
Django async 改造出问题时,我通常这样查:
- 检查 async view 里是否直接出现
Model.objects.get、filter、save、transaction.atomic。 - 检查同步中间件是否让请求在 sync/async 之间频繁切换。
- 按接口记录 P95/P99、数据库查询耗时、外部服务耗时和线程池排队时间。
- 压测混合流量:一个慢外部服务是否拖慢纯数据库接口。
- 确认数据库连接数、事务持续时间和慢查询是否同步上升。
上线检查清单
- 只有 I/O 等待明显的接口才改 async view,ORM 密集型接口保持同步也可以。
- async view 中禁止直接调用同步 ORM,简单查询优先用异步 ORM 方法。
- 复杂 ORM 和事务逻辑封装成同步函数,用
sync_to_async(..., thread_sensitive=True)调用。 - 事务边界内不等待外部 HTTP,不把数据库锁交给下游服务质量决定。
- 同步中间件逐个盘点,避免 ASGI 下频繁上下文切换。
- 压测观察 P99、线程池等待、数据库连接数、事务时长、异常日志。
- 保留回滚到同步视图或 WSGI 路径的方案,先灰度非核心接口。
总结
Django 的 async 支持不是“把 def 改成 async def”这么简单。它真正考验的是边界感:哪里可以异步等待,哪里必须同步收口,哪里要用线程敏感模式保护 ORM,哪里不能让事务跨过网络调用。
我的经验是,Django async 改造要小步走。先挑外部 I/O 明显的接口,画清 ORM 边界,压测指标看懂以后再扩大范围。这样 async view 才是生产优化,而不是给老系统添一层新的不确定性。

Python FastAPI 实战:别把耗时任务塞进请求生命周期
