直接用SQLAlchemy同步执行会阻塞Tornado的IOLoop,因其底层DBAPI调用是同步的;推荐方案是run_on_executor配合独立session,避免引入不成熟的异步驱动或强行混用asyncio。
直接用 SQLAlchemy 同步执行 SQL 会阻塞 Tornado 的 IOLoop,所谓“异步框架里写同步数据库代码”,等于白搭。安全执行的关键不是“怎么写 SQL”,而是“怎么不卡住事件循环”。
因为 session.execute()、session.commit()、甚至 user.addresses(延迟加载)这些操作底层都走的是同步 DBAPI(如 pymysql 或 psycopg2),会真实阻塞线程。Tornado 的协程无法自动让出控制权——它只对 await 显式支持的异步对象让步,而原生 SQLAlchemy 不是其中之一。
常见错误现象:
压测时并发请求响应时间线性增长(比如 10 个请求耗时约 10 秒),self.write() 拖到最后一刻才调用,IOLoop 看似“空转”但实际被绑死。
create_engine(..., echo=True) 开启后能看到日志卡在 execute 那一行不动,就是证据这是目前最轻量、最可控、兼容性最好的做法。不改 ORM 习惯,不引入新消息队列,也不依赖尚未成熟的异步驱动(如 asyncpg 对 PostgreSQL 有效,但 MySQL 的 aiomysql 生态弱、bug 多、不维护)。
立即学习“Python免费学习笔记(深入)”;
核心要点:
@tornado.gen.coroutine 或 async def(Tornado ≥6.0)包装 handler 方法tornado.concurrent.futures.ThreadPoolExecutor 实例,不能每次 new 一个示例片段:
from tornado.concurrent import run_on_executorfrom concurrent.futures import ThreadPoolExecutorfrom sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmaker<p>engine = create_engine('mysql+pymysql://...')<br />Session = sessionmaker(bind=engine)executor = ThreadPoolExecutor(max_workers=4)</p><p>class UserHandler(tornado.web.RequestHandler):@run_on_executor(executor=executor)def _db_query(self, user_id):with Session() as session:return session.execute("SELECT * FROM users WHERE id = :id", {"id": user_id}).fetchone()</p><pre class="brush:php;toolbar:false;">async def get(self): user_id = self.get_argument("id") row = await self._db_query(user_id) self.write({"user": dict(row) if row else None})
Tornado 本身不基于 asyncio 事件循环(它用自研 IOLoop),强行混用 asyncio.run() 或把 create_async_engine() 塞进 Tornado handler 会导致:
AttributeError: 'NoneType' object has no attribute 'execute'
如果你真要用 SQLAlchemy 2.0 异步特性,该换框架——用 FastAPI 或 Starlette,它们和 asyncio 是同源设计。在 Tornado 里硬套,只会让问题更隐蔽。
即使你放弃 SQLAlchemy、改用 aiomysql 手写 SQL,仍有几个关键限制:
aiomysql 已停止维护(最后 release 是 2022 年),Python 3.12+ 兼容性存疑max_prepared_stmt_count 限制Pool 的 close() 不是协程,需手动 await pool.wait_closed(),否则进程退出时连接泄漏换句话说:它能跑通简单查询,但经不起线上流量和长期维护考验。
真正容易被忽略的一点:无论用哪种方式,只要涉及多表 JOIN 或复杂 WHERE 条件,务必先在 MySQL CLI 里用 EXPLAIN 看执行计划。异步只是不卡线程,不能拯救慢 SQL——90% 的性能问题,根源在没加索引或写了全表扫描语句。