2、常见异步迁移框架:Alembic异步模式、SQLAlchemy async session、自定义异步迁移脚本
在异步化开发阶段,数据库迁移不再只是同步脚本的简单封装。异步迁移框架需要正确处理事件循环、连接池生命周期以及异步会话的上下文管理。本节聚焦三种主流实践:Alembic 原生异步模式、SQLAlchemy async session 的迁移适配以及自定义异步迁移脚本的编写与故障排查。
2.1 三种异步迁移方案对比
| 方案 | 核心机制 | 适用场景 | 常见故障点 |
|---|---|---|---|
| Alembic 异步模式 | 通过 run_async() 包装迁移函数,内部管理事件循环 |
项目已使用 Alembic 且需要最小改动迁移至异步 | 循环冲突、连接池未正确关闭 |
| SQLAlchemy async session | 使用 AsyncSession + async_engine 执行迁移操作 |
新项目或重构项目,完全异步栈 | 同步/异步混用、未使用 async with 管理会话 |
| 自定义异步迁移脚本 | 手动编写 asyncio.run() 入口,直接操作底层驱动 |
特殊迁移逻辑(如跨库、分片、数据清洗) | 事务边界模糊、异常未传播至主循环 |
2.2 Alembic 异步模式:run_async 的正确用法
Alembic 从 1.7+ 开始支持异步迁移。核心思路是在 env.py 中定义 run_migrations_online() 时,使用 asyncio.run() 调用异步上下文。故障高发区在于 事件循环嵌套 和 连接池未显式释放。
✅ 标准异步 env.py 模板(关键片段)
# alembic/env.py
from alembic import context
from sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection
async def run_migrations_online():
connectable = create_async_engine(
config.get_main_option("sqlalchemy.url"),
pool_size=5,
max_overflow=10,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose() # 必须显式释放连接池
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
# 入口
if context.is_offline_mode():
context.run_migrations()
else:
asyncio.run(run_migrations_online())
⚠️ 常见故障: 忘记调用
await connectable.dispose() 会导致连接池残留,在 CI/CD 环境中引发“连接数超限”错误。另外,不要在 run_async 内部再创建新的事件循环,否则会抛出 RuntimeError: You cannot use AsyncToSync in the same thread。
2.3 SQLAlchemy async session 迁移适配
当迁移脚本需要执行复杂数据操作(如批量更新、数据校验)时,直接使用 AsyncSession 比 Alembic 的 run_sync 更灵活。但必须注意:迁移脚本中的每个数据库操作都必须是异步的,不能混用同步 Session。
💡 最佳实践: 在迁移脚本中通过
op.get_bind() 获取同步连接,然后使用 AsyncConnection 包装。避免在同一个事务中同时使用 session.execute() 和 connection.execute()。
# 迁移脚本示例:使用 AsyncSession 执行数据迁移
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from sqlalchemy import text
async def upgrade_data(async_session: AsyncSession):
# 异步执行复杂数据操作
result = await async_session.execute(
text("UPDATE users SET status = 'active' WHERE last_login > NOW() - INTERVAL '30 days'")
)
await async_session.commit()
print(f"Updated {result.rowcount} rows")
def upgrade():
# 在 Alembic 迁移中调用
from alembic import op
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(op.get_context().config.get_main_option("sqlalchemy.url"))
async_session_factory = async_sessionmaker(engine, class_=AsyncSession)
async def run():
async with async_session_factory() as session:
await upgrade_data(session)
import asyncio
asyncio.run(run())
2.4 自定义异步迁移脚本架构
对于超出 Alembic 能力范围的迁移(如跨数据库、分片集群、调用外部 API 进行数据清洗),需要编写独立的异步迁移脚本。这类脚本的故障排查重点在于 任务编排 和 异常传播。
2.5 自定义脚本故障排查清单
- 事件循环冲突:确保整个迁移过程只有一个事件循环。不要在
async def内部调用asyncio.run(),改用await或asyncio.create_task()。 - 连接池泄漏:每个
async with engine.connect()必须配对await engine.dispose()。使用async with上下文管理器自动释放。 - 事务边界模糊:自定义脚本中每个独立迁移步骤应包裹在单独的事务中,避免一个步骤失败导致整个迁移回滚。
- 日志缺失:异步异常栈可能被事件循环吞没。在
main()入口添加try/except并输出完整 traceback。 - 幂等性缺失:迁移脚本应支持重复执行。使用
IF NOT EXISTS、CREATE OR REPLACE或先检查再操作。
🔧 快速诊断命令: 在自定义脚本入口添加
asyncio.run(main(), debug=True) 可以打印事件循环中所有未完成的协程,快速定位挂起任务。
2.6 三种方案的选型建议
根据项目阶段和团队异步经验,推荐以下选型路径:
- 已有同步迁移项目,逐步异步化 → 优先使用 Alembic 异步模式,改动最小,风险可控。
- 新建异步项目,全栈异步 → 使用 SQLAlchemy async session 配合 Alembic,保持代码风格一致。
- 复杂数据迁移、跨库操作、分片迁移 → 使用 自定义异步迁移脚本,但必须严格遵循任务编排和异常传播规范。
💡 经验之谈: 无论选择哪种方案,都建议在
development 阶段编写 迁移回滚脚本(downgrade 函数),并针对异步迁移编写专门的 集成测试,使用 pytest-asyncio 模拟迁移执行,提前暴露事件循环和连接池问题。