DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

asyncpg 并行查询报 another operation is in progress:别共享已借出的连接

asyncpg 并行任务报 another operation is in progress,先查实际连接是否共享。独立任务分别借池连接,同一事务内顺序执行,并区分 GINO 的 reuse 与原生 asyncpg 接口。

把查询放进 asyncio.gather 后,asyncpg 报 cannot perform operation: another operation is in progress,先查并行任务是否拿着同一条已借出的连接。连接池能提供多条连接,不意味着从池里借出的一条连接可以同时执行多项查询。

这项错误来自客户端连接的操作状态检查。不能只凭它断定 PostgreSQL 锁住了某行,也不能靠给数据库加索引、提高连接数或 sleep 一秒解决连接共享。取消和释放阶段也可能出现相近错误,应保留最早异常及完整堆栈。

检查任务拿到的连接,不只看外层有 pool

查看 create_task、gather 与查询之间的调用链:连接是否先在请求入口借出,然后作为参数、对象属性或上下文传给多个任务?底层 ORM 是否默认复用当前请求的连接?某个方法表面上叫 acquire,也可能是封装层的复用接口。

尤其不要读取 _stmt_exclusive_section 等私有状态,判断“目前空闲”后就把连接交给任务。状态检查与真正查询之间有 await 时,事件循环可切换到另一任务,原来的空闲判断立即失效。即使单线程,也有这种交错;删掉状态保护更不能让协议支持并行查询。

同一连接上的两条操作都按顺序 await,仍在取消或回池 reset 时出错,则继续检查未结束的后台任务、异常清理与具体驱动版本。不能把所有同名报错都认定为显式 gather 共享;也不要手工清空私有 waiter、跳过 reset 或吞掉清理异常来让连接继续回池。

独立查询由每个任务自己借连接

若任务本来可以独立执行,每个任务进入自己的池租借范围,并在该范围内等待查询完成。以下为原生 asyncpg 的结构示例,pool 已由应用创建并管理,item_ids 也来自已验证的业务输入:

import asyncio

async def fetch_item(pool, item_id):
    async with pool.acquire() as connection:
        return await connection.fetchrow(
            "SELECT item_id, state FROM app_items WHERE item_id = $1",
            item_id,
        )

results = await asyncio.gather(
    *(fetch_item(pool, item_id) for item_id in item_ids)
)

表与列名是示例;保留真实租户或权限筛选。并发任务超过可用连接时,需要等待租借,不能把池大小当成无限并发。大批任务应按业务负载设置并发范围,避免同时堆积大量等待或给服务器制造连接压力。

连接对象不要逃出 async with 交给尚未完成的任务。发生异常或取消时,也要由任务管理代码确认相关工作已经结束,之后才按应用生命周期关闭连接池。返回了某个任务的结果,不代表其他任务都已停止访问数据库。

一笔事务内,查询顺序 await

若两项修改必须属于同一笔数据库事务,不应为了并行给它们各借一条连接:这会变成独立事务。应在同一连接和事务上下文内顺序等待操作,例如:

async with pool.acquire() as connection:
    async with connection.transaction():
        first = await connection.fetchval(first_sql, *first_args)
        second = await connection.fetchval(second_sql, *second_args)

first_sql、second_sql 与参数是项目已有的受控语句,示例仅说明 await 与事务的位置。不能把两个调用先生成协程,再放进 gather,就期待它们在同一事务连接上自动排队。

如果多项结果可由一条查询取得,可评估保持原语义的查询合并。它属于 SQL 与业务设计调整,不是修改 asyncpg 的协议检查。写事务失败后按原事务管理方式结束,再评估整笔操作能否重做,不无条件重复已有外部副作用。

GINO 的 reuse 不能传给原生 asyncpg

使用 GINO 或其他封装时,核对实际对象类型与该版本的连接复用规则。封装可能把当前连接保存在请求或任务上下文中,子任务又继承同一来源,导致看似多次 acquire,实际仍共享连接。

原生 asyncpg 的 Pool.acquire 提供 timeout 等公共参数,不提供 GINO 风格的 reuse 参数。不要照搬 pool.acquire(reuse=False) 到原生池;也不要仅因封装中关闭复用,就认定所有 CRUD 方法都已经绑定了新连接。应从正式接口确认每个独立任务实际使用哪条连接,并在测试环境检查完整调用路径。

验收连接生命周期与业务结果

保存原任务调度和连接管理代码,先在隔离数据中覆盖多个并行任务、池容量不足时的等待、任务异常与取消、事务失败以及后续连接复用。成功标准是同一连接不再被重叠操作,任务结束后连接按正常流程回池,业务结果符合原来的事务要求。

如果错误只在取消后发生,记录驱动版本和第一条异常,再对照该版本的清理规则安排兼容更新。不要把暂时增加延时或禁用清理当作验收成功。

需要回退时恢复本次调度与连接配置,先停止或排空受影响任务。代码回退不会撤销已经提交的写入;由不同连接完成的部分成功需要依据事务记录与业务身份核对,不能只看 gather 抛了异常就当作全部未执行。